Fix verified badge wrapping over the handle in the profile header

The profile header rendered `ProfileBadges` as an inline `View` inside the
display name `Text`. On Android the space reserved for an inline view is
converted with `PixelUtil.toPixelFromSP`, so the placeholder box is scaled by
the device font scale on top of the scaling `ProfileBadges` already applies
itself via `useNativeFontScale`. The reserved box therefore ends up larger
than the badge it stands in for, which pushes the badge onto a line of its
own and paints it below the name, over the start of the handle. The same
conversion places the badge at `baseline - reservedHeight`, so it also floats
above the text baseline.

Render the badges as a sibling of the name in a baseline-aligned row instead,
which is the pattern used everywhere else badges follow a name. Baseline
alignment keeps the badge bottom on the name's baseline, matching how the
inline view was positioned, and drops the iOS-only `marginTop` nudge that was
compensating for the inline layout.

Fixes #11238

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184o34rCfWuqtNWCJ9NeTrH
This commit is contained in:
Claude
2026-07-25 09:53:47 +00:00
parent d5e335e575
commit 00521b6d87
+13 -6
View File
@@ -4,7 +4,7 @@ import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {type Shadow} from '#/state/cache/types'
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
@@ -18,15 +18,24 @@ export function ProfileHeaderDisplayName({
const t = useTheme()
const {gtMobile} = useBreakpoints()
/*
* The badges are a sibling of the name rather than a child of it, because an
* inline view inside a `Text` is measured in SP on Android: the space
* reserved for it is multiplied by the device font scale a second time (once
* by `ProfileBadges` itself, then again by the platform), so the badge
* over-reserves, wraps onto a line of its own and gets painted over the
* handle below. Baseline alignment keeps the badge sitting on the name's
* baseline the way an inline view would.
*/
return (
<View>
<View style={[a.flex_row, a.align_baseline, a.gap_xs]}>
<Text
emoji
testID="profileHeaderDisplayName"
style={[
t.atoms.text,
gtMobile ? a.text_4xl : a.text_3xl,
a.self_start,
a.flex_shrink,
a.font_bold,
a.leading_tight,
]}>
@@ -34,10 +43,8 @@ export function ProfileHeaderDisplayName({
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)}
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
<ProfileBadges profile={profile} size="lg" interactive />
</View>
</Text>
<ProfileBadges profile={profile} size="lg" interactive />
</View>
)
}