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 {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {type Shadow} from '#/state/cache/types' 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 {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -18,15 +18,24 @@ export function ProfileHeaderDisplayName({
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() 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 ( return (
<View> <View style={[a.flex_row, a.align_baseline, a.gap_xs]}>
<Text <Text
emoji emoji
testID="profileHeaderDisplayName" testID="profileHeaderDisplayName"
style={[ style={[
t.atoms.text, t.atoms.text,
gtMobile ? a.text_4xl : a.text_3xl, gtMobile ? a.text_4xl : a.text_3xl,
a.self_start, a.flex_shrink,
a.font_bold, a.font_bold,
a.leading_tight, a.leading_tight,
]}> ]}>
@@ -34,10 +43,8 @@ export function ProfileHeaderDisplayName({
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'), moderation.ui('displayName'),
)} )}
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
<ProfileBadges profile={profile} size="lg" interactive />
</View>
</Text> </Text>
<ProfileBadges profile={profile} size="lg" interactive />
</View> </View>
) )
} }