From 8b06b18cd5c11c5b29923d675111fd9eafa35cc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 08:21:33 +0000 Subject: [PATCH] Fix inline profile badge alignment in notifications on iOS new arch The verification/bot/beta badges rendered inline after the author name in a notification row ("liked by xyz") sat too high on iOS after the new architecture switch. The badges are a `View` nested inside a ``, so iOS lays them out as a text attachment. The old architecture measured the attachment box with `sizeThatFitsMinimumSize:` (which folds the inline view's margins into the box) and positioned it at `lineFragmentBottom - boxHeight + font.descender`, so the `marginBottom: -6` hack shifted the badges down. On the new architecture `ParagraphShadowNode` measures the box with `LayoutableShadowNode::measure`, which returns the node's own frame size with margins excluded, and then overwrites the box origin with the frame `RCTTextLayoutManager` computed at `lineTop + baseline - boxHeight`. Margins and `top` are both ignored, leaving the badges pinned to the text baseline. Box height is the only remaining lever, so add an `inlineFontSize` prop to `ProfileBadges` that constrains the container to the cap height of the surrounding text. The box then covers exactly the capital letters and the existing `align_center` centers the badges on them, regardless of which badges are visible or how tall they are. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Pgeau3P52qsMTcnDeJm8Zn --- src/components/ProfileBadges.tsx | 39 ++++++++++++++++++- .../notifications/NotificationFeedItem.tsx | 7 +++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/components/ProfileBadges.tsx b/src/components/ProfileBadges.tsx index cb877179ac..a7bc3417dd 100644 --- a/src/components/ProfileBadges.tsx +++ b/src/components/ProfileBadges.tsx @@ -8,11 +8,19 @@ import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' +import {IS_IOS} from '#/env' import type * as bsky from '#/types/bsky' import {BetaBadge, BetaBadgeButton, useIsBetaBadgeVisible} from './BetaBadge' export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' +/* + * Cap height of Inter as a fraction of its em size, from the font's OS/2 + * table. The system font (used when the "system font" setting is on) is within + * 0.015em of this, so a single constant covers both. + */ +const CAP_HEIGHT_RATIO = 0.7275 + const verificationIconSizes: Record = { xs: 10, sm: 12, @@ -51,11 +59,18 @@ export function ProfileBadges({ size, style, allowFontScaling = true, + inlineFontSize, }: ViewStyleProp & { profile: bsky.profile.AnyProfileView interactive?: boolean size: Size allowFontScaling?: boolean + /** + * Unscaled `fontSize` of the text this is rendered inline within. Set this + * when rendering inside a `` so the badges line up with the text + * instead of hanging off its baseline - see `inlineHeight` below. + */ + inlineFontSize?: number }) { const shadowed = useProfileShadow(profile) const verification = useSimpleVerificationState({profile}) @@ -84,6 +99,27 @@ export function ProfileBadges({ const betaIconWidth = betaIconSizes[size] * scaleMultiplier const betaBadgeScaledPadding = betaBadgePadding[size] * scaleMultiplier + /* + * A `View` nested inside a `` is laid out as a text attachment on iOS, + * and on the new architecture the attachment box is placed at + * `lineTop + baseline - boxHeight` (`RCTTextLayoutManager`), pinning its + * bottom edge to the text baseline. `margin` and `top` have no effect there: + * `ParagraphShadowNode` measures the box with `LayoutableShadowNode::measure` + * (frame size only, margins excluded) and then overwrites its origin. The old + * architecture folded margins into the measured box and offset it by the + * font's descender, which is why the pre-new-arch fix was a negative + * `marginBottom`. + * + * The box height is the only lever left, so constrain it to the cap height of + * the surrounding text. That lands the box exactly over the capital letters, + * and `align_center` then centers the badges on them - independent of which + * badges are visible and how tall they are. + */ + const inlineHeight = + IS_IOS && inlineFontSize + ? inlineFontSize * scaleMultiplier * CAP_HEIGHT_RATIO + : undefined + const gap = isOnTheSmallSide ? a.gap_2xs : a.gap_xs const padding = gap.gap / 2 let visibleBadgeIndex = 0 @@ -99,7 +135,8 @@ export function ProfileBadges({ }) return ( - + {interactive ? ( <>