Add minimal header to profiles (#8936)

This commit is contained in:
Samuel Newman
2025-11-20 18:31:41 +02:00
committed by GitHub
parent dfe9f9fa07
commit 13c4ef83f1
5 changed files with 425 additions and 303 deletions
+13 -2
View File
@@ -1,5 +1,11 @@
import {useMemo} from 'react'
import {type GestureResponderEvent, View} from 'react-native'
import {
type GestureResponderEvent,
type StyleProp,
type TextStyle,
View,
type ViewStyle,
} from 'react-native'
import {
moderateProfile,
type ModerationOpts,
@@ -283,9 +289,13 @@ function InlineNameAndHandle({
export function Name({
profile,
moderationOpts,
style,
textStyle,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
style?: StyleProp<ViewStyle>
textStyle?: StyleProp<TextStyle>
}) {
const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName(
@@ -294,7 +304,7 @@ export function Name({
)
const verification = useSimpleVerificationState({profile})
return (
<View style={[a.flex_row, a.align_center, a.max_w_full]}>
<View style={[a.flex_row, a.align_center, a.max_w_full, style]}>
<Text
emoji
style={[
@@ -303,6 +313,7 @@ export function Name({
a.leading_snug,
a.self_start,
a.flex_shrink,
textStyle,
]}
numberOfLines={1}>
{name}
@@ -18,9 +18,11 @@ import {SubscribeProfileDialog} from './SubscribeProfileDialog'
export function SubscribeProfileButton({
profile,
moderationOpts,
disableHint,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
disableHint?: boolean
}) {
const {_} = useLingui()
const requireEmailVerification = useRequireEmailVerification()
@@ -69,7 +71,7 @@ export function SubscribeProfileButton({
return (
<>
<Tooltip.Outer
visible={showTooltip}
visible={showTooltip && !disableHint}
onVisibleChange={onDismissTooltip}
position="bottom">
<Tooltip.Target>
@@ -23,7 +23,6 @@ import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
import {usePreferencesQuery} from '#/state/queries/preferences'
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, tokens, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {type DialogOuterProps, useDialogControl} from '#/components/Dialog'
@@ -34,6 +33,7 @@ import {
import {Link} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {ProfileHeaderDisplayName} from './DisplayName'
import {EditProfileDialog} from './EditProfileDialog'
@@ -63,28 +63,17 @@ let ProfileHeaderLabeler = ({
const t = useTheme()
const {_} = useLingui()
const {currentAccount, hasSession} = useSession()
const requireAuth = useRequireAuth()
const playHaptic = useHaptics()
const cantSubscribePrompt = Prompt.usePromptControl()
const isSelf = currentAccount?.did === profile.did
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
const {data: preferences} = usePreferencesQuery()
const {
mutateAsync: toggleSubscription,
variables,
reset,
} = useLabelerSubscriptionMutation()
const isSubscribed =
variables?.subscribe ??
preferences?.moderationPrefs.labelers.find(l => l.did === profile.did)
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
useUnlikeMutation()
const [likeUri, setLikeUri] = useState<string>(labeler.viewer?.like || '')
const [likeUri, setLikeUri] = useState(labeler.viewer?.like || '')
const [likeCount, setLikeCount] = useState(labeler.likeCount || 0)
const onToggleLiked = useCallback(async () => {
@@ -108,56 +97,12 @@ let ProfileHeaderLabeler = ({
_(
msg`There was an issue contacting the server, please check your internet connection and try again.`,
),
'xmark',
{type: 'error'},
)
logger.error(`Failed to toggle labeler like`, {message: e.message})
}
}, [labeler, playHaptic, likeUri, unlikeMod, likeMod, _])
const editProfileControl = useDialogControl()
const onPressSubscribe = useCallback(() => {
requireAuth(async (): Promise<void> => {
playHaptic()
const subscribe = !isSubscribed
try {
await toggleSubscription({
did: profile.did,
subscribe,
})
logger.metric(
subscribe
? 'moderation:subscribedToLabeler'
: 'moderation:unsubscribedFromLabeler',
{},
{statsig: true},
)
} catch (e: any) {
reset()
if (e.message === 'MAX_LABELERS') {
cantSubscribePrompt.open()
return
}
logger.error(`Failed to subscribe to labeler`, {message: e.message})
}
})
}, [
playHaptic,
requireAuth,
toggleSubscription,
isSubscribed,
profile,
cantSubscribePrompt,
reset,
])
const isMe = useMemo(
() => currentAccount?.did === profile.did,
[currentAccount, profile],
)
return (
<ProfileHeaderShell
profile={profile}
@@ -170,75 +115,7 @@ let ProfileHeaderLabeler = ({
<View
style={[a.flex_row, a.justify_end, a.align_center, a.gap_xs, a.pb_lg]}
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}
/>
</>
) : !isAppLabeler(profile.did) ? (
<>
<Button
testID="toggleSubscribeBtn"
label={
isSubscribed
? _(msg`Unsubscribe from this labeler`)
: _(msg`Subscribe to this labeler`)
}
onPress={onPressSubscribe}>
{state => (
<View
style={[
{
paddingVertical: 9,
paddingHorizontal: 12,
borderRadius: 6,
gap: 6,
backgroundColor: isSubscribed
? state.hovered || state.pressed
? t.palette.contrast_50
: t.palette.contrast_25
: state.hovered || state.pressed
? tokens.color.temp_purple_dark
: tokens.color.temp_purple,
},
]}>
<Text
style={[
{
color: isSubscribed
? t.palette.contrast_700
: t.palette.white,
},
a.font_semi_bold,
a.text_center,
a.leading_tight,
]}>
{isSubscribed ? (
<Trans>Unsubscribe</Trans>
) : (
<Trans>Subscribe to Labeler</Trans>
)}
</Text>
</View>
)}
</Button>
</>
) : null}
<ProfileMenu profile={profile} />
<HeaderLabelerButtons profile={profile} />
</View>
<View style={[a.flex_col, a.gap_2xs, a.pt_2xs, a.pb_md]}>
<ProfileHeaderDisplayName profile={profile} moderation={moderation} />
@@ -265,7 +142,6 @@ let ProfileHeaderLabeler = ({
testID="toggleLikeBtn"
size="small"
color="secondary"
variant="solid"
shape="round"
label={_(msg`Like this labeler`)}
disabled={!hasSession || isLikePending || isUnlikePending}
@@ -318,7 +194,6 @@ let ProfileHeaderLabeler = ({
</>
)}
</View>
<CantSubscribePrompt control={cantSubscribePrompt} />
</ProfileHeaderShell>
)
}
@@ -349,3 +224,132 @@ function CantSubscribePrompt({
</Prompt.Outer>
)
}
export function HeaderLabelerButtons({
profile,
minimal = false,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
/** disable the subscribe button */
minimal?: boolean
}) {
const {_} = useLingui()
const t = useTheme()
const {currentAccount} = useSession()
const requireAuth = useRequireAuth()
const playHaptic = useHaptics()
const editProfileControl = useDialogControl()
const {data: preferences} = usePreferencesQuery()
const {
mutateAsync: toggleSubscription,
variables,
reset,
} = useLabelerSubscriptionMutation()
const isSubscribed =
variables?.subscribe ??
preferences?.moderationPrefs.labelers.find(l => l.did === profile.did)
const cantSubscribePrompt = Prompt.usePromptControl()
const isMe = currentAccount?.did === profile.did
const onPressSubscribe = () =>
requireAuth(async (): Promise<void> => {
playHaptic()
const subscribe = !isSubscribed
try {
await toggleSubscription({
did: profile.did,
subscribe,
})
logger.metric(
subscribe
? 'moderation:subscribedToLabeler'
: 'moderation:unsubscribedFromLabeler',
{},
{statsig: true},
)
} catch (e: any) {
reset()
if (e.message === 'MAX_LABELERS') {
cantSubscribePrompt.open()
return
}
logger.error(`Failed to subscribe to labeler`, {message: e.message})
}
})
return (
<>
{isMe ? (
<>
<Button
testID="profileHeaderEditProfileButton"
size="small"
color="secondary"
onPress={editProfileControl.open}
label={_(msg`Edit profile`)}
style={a.rounded_full}>
<ButtonText>
<Trans>Edit Profile</Trans>
</ButtonText>
</Button>
<EditProfileDialog profile={profile} control={editProfileControl} />
</>
) : !isAppLabeler(profile.did) && !minimal ? (
// hidden in the minimal header, because it's not shadowed so the two buttons
// can get out of sync. if you want to reenable, you'll need to add shadowing
// to the subscribed state -sfn
<Button
testID="toggleSubscribeBtn"
label={
isSubscribed
? _(msg`Unsubscribe from this labeler`)
: _(msg`Subscribe to this labeler`)
}
onPress={onPressSubscribe}>
{state => (
<View
style={[
{
paddingVertical: 9,
paddingHorizontal: 12,
borderRadius: 6,
gap: 6,
backgroundColor: isSubscribed
? state.hovered || state.pressed
? t.palette.contrast_50
: t.palette.contrast_25
: state.hovered || state.pressed
? tokens.color.temp_purple_dark
: tokens.color.temp_purple,
},
]}>
<Text
style={[
{
color: isSubscribed
? t.palette.contrast_700
: t.palette.white,
},
a.font_semi_bold,
a.text_center,
a.leading_tight,
]}>
{isSubscribed ? (
<Trans>Unsubscribe</Trans>
) : (
<Trans>Subscribe to Labeler</Trans>
)}
</Text>
</View>
)}
</Button>
) : null}
<ProfileMenu profile={profile} />
<CantSubscribePrompt control={cantSubscribePrompt} />
</>
)
}
@@ -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"
/>
</>
)
}
+64 -7
View File
@@ -1,4 +1,4 @@
import React, {memo, useState} from 'react'
import {memo, useMemo, useState} from 'react'
import {type LayoutChangeEvent, StyleSheet, View} from 'react-native'
import Animated, {
runOnJS,
@@ -10,18 +10,30 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {
type AppBskyActorDefs,
type AppBskyLabelerDefs,
moderateProfile,
type ModerationOpts,
type RichText as RichTextAPI,
} from '@atproto/api'
import {useIsFocused} from '@react-navigation/native'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isNative} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {atoms as a, useTheme} from '#/alf'
import {ProfileHeaderLabeler} from './ProfileHeaderLabeler'
import {ProfileHeaderStandard} from './ProfileHeaderStandard'
import {Header} from '#/components/Layout'
import * as ProfileCard from '#/components/ProfileCard'
import {
HeaderLabelerButtons,
ProfileHeaderLabeler,
} from './ProfileHeaderLabeler'
import {
HeaderStandardButtons,
ProfileHeaderStandard,
} from './ProfileHeaderStandard'
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
const t = useTheme()
@@ -75,6 +87,7 @@ let ProfileHeader = ({setMinimumHeight, ...props}: Props): React.ReactNode => {
<MinimalHeader
onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
profile={props.profile}
labeler={props.labeler}
hideBackButton={props.hideBackButton}
/>
)}
@@ -85,18 +98,28 @@ let ProfileHeader = ({setMinimumHeight, ...props}: Props): React.ReactNode => {
ProfileHeader = memo(ProfileHeader)
export {ProfileHeader}
const MinimalHeader = React.memo(function MinimalHeader({
const MinimalHeader = memo(function MinimalHeader({
onLayout,
profile: profileUnshadowed,
labeler,
hideBackButton = false,
}: {
onLayout: (e: LayoutChangeEvent) => void
profile: AppBskyActorDefs.ProfileViewDetailed
labeler?: AppBskyLabelerDefs.LabelerViewDetailed
hideBackButton?: boolean
}) {
const t = useTheme()
const insets = useSafeAreaInsets()
const ctx = usePagerHeaderContext()
const profile = useProfileShadow(profileUnshadowed)
const moderationOpts = useModerationOpts()
const moderation = useMemo(
() => (moderationOpts ? moderateProfile(profile, moderationOpts) : null),
[moderationOpts, profile],
)
const [visible, setVisible] = useState(false)
const [minimalHeaderHeight, setMinimalHeaderHeight] = React.useState(0)
const [minimalHeaderHeight, setMinimalHeaderHeight] = useState(insets.top)
const isScreenFocused = useIsFocused()
if (!ctx) throw new Error('MinimalHeader cannot be used on web')
const {scrollY, headerHeight} = ctx
@@ -156,8 +179,42 @@ const MinimalHeader = React.memo(function MinimalHeader({
paddingTop: insets.top,
},
animatedStyle,
]}
/>
]}>
<Header.Outer noBottomBorder>
{hideBackButton ? <Header.MenuButton /> : <Header.BackButton />}
<Header.Content align="left">
{moderationOpts ? (
<ProfileCard.Name
profile={profile}
moderationOpts={moderationOpts}
textStyle={[a.font_bold]}
/>
) : (
<ProfileCard.NamePlaceholder />
)}
<Header.SubtitleText>
{sanitizeHandle(profile.handle, '@')}
</Header.SubtitleText>
</Header.Content>
{!profile.associated?.labeler
? moderationOpts &&
moderation && (
<View style={[a.flex_row, a.justify_end, a.gap_xs]}>
<HeaderStandardButtons
profile={profile}
moderation={moderation}
moderationOpts={moderationOpts}
minimal
/>
</View>
)
: labeler && (
<View style={[a.flex_row, a.justify_end, a.gap_xs]}>
<HeaderLabelerButtons profile={profile} minimal />
</View>
)}
</Header.Outer>
</Animated.View>
)
})
MinimalHeader.displayName = 'MinimalHeader'