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 `<Text>`, 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgeau3P52qsMTcnDeJm8Zn
This commit is contained in:
@@ -8,11 +8,19 @@ import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge'
|
|||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
||||||
|
import {IS_IOS} from '#/env'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {BetaBadge, BetaBadgeButton, useIsBetaBadgeVisible} from './BetaBadge'
|
import {BetaBadge, BetaBadgeButton, useIsBetaBadgeVisible} from './BetaBadge'
|
||||||
|
|
||||||
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
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<Size, number> = {
|
const verificationIconSizes: Record<Size, number> = {
|
||||||
xs: 10,
|
xs: 10,
|
||||||
sm: 12,
|
sm: 12,
|
||||||
@@ -51,11 +59,18 @@ export function ProfileBadges({
|
|||||||
size,
|
size,
|
||||||
style,
|
style,
|
||||||
allowFontScaling = true,
|
allowFontScaling = true,
|
||||||
|
inlineFontSize,
|
||||||
}: ViewStyleProp & {
|
}: ViewStyleProp & {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
interactive?: boolean
|
interactive?: boolean
|
||||||
size: Size
|
size: Size
|
||||||
allowFontScaling?: boolean
|
allowFontScaling?: boolean
|
||||||
|
/**
|
||||||
|
* Unscaled `fontSize` of the text this is rendered inline within. Set this
|
||||||
|
* when rendering inside a `<Text>` so the badges line up with the text
|
||||||
|
* instead of hanging off its baseline - see `inlineHeight` below.
|
||||||
|
*/
|
||||||
|
inlineFontSize?: number
|
||||||
}) {
|
}) {
|
||||||
const shadowed = useProfileShadow(profile)
|
const shadowed = useProfileShadow(profile)
|
||||||
const verification = useSimpleVerificationState({profile})
|
const verification = useSimpleVerificationState({profile})
|
||||||
@@ -84,6 +99,27 @@ export function ProfileBadges({
|
|||||||
const betaIconWidth = betaIconSizes[size] * scaleMultiplier
|
const betaIconWidth = betaIconSizes[size] * scaleMultiplier
|
||||||
const betaBadgeScaledPadding = betaBadgePadding[size] * scaleMultiplier
|
const betaBadgeScaledPadding = betaBadgePadding[size] * scaleMultiplier
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A `View` nested inside a `<Text>` 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 gap = isOnTheSmallSide ? a.gap_2xs : a.gap_xs
|
||||||
const padding = gap.gap / 2
|
const padding = gap.gap / 2
|
||||||
let visibleBadgeIndex = 0
|
let visibleBadgeIndex = 0
|
||||||
@@ -99,7 +135,8 @@ export function ProfileBadges({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.align_center, gap, style]}>
|
<View
|
||||||
|
style={[a.flex_row, a.align_center, gap, {height: inlineHeight}, style]}>
|
||||||
{interactive ? (
|
{interactive ? (
|
||||||
<>
|
<>
|
||||||
<VerificationCheckButton
|
<VerificationCheckButton
|
||||||
|
|||||||
@@ -258,12 +258,15 @@ let NotificationFeedItem = ({
|
|||||||
<ProfileBadges
|
<ProfileBadges
|
||||||
profile={firstAuthor.profile}
|
profile={firstAuthor.profile}
|
||||||
size="md"
|
size="md"
|
||||||
|
inlineFontSize={a.text_md.fontSize}
|
||||||
style={[
|
style={[
|
||||||
a.relative,
|
a.relative,
|
||||||
{
|
{
|
||||||
// weird stuff here
|
/*
|
||||||
|
* Empirical nudges to line the badges up with the text on
|
||||||
|
* Android and web. iOS is handled by `inlineFontSize` above.
|
||||||
|
*/
|
||||||
paddingTop: platform({android: 2}),
|
paddingTop: platform({android: 2}),
|
||||||
marginBottom: platform({ios: -6}),
|
|
||||||
top: platform({web: 2}),
|
top: platform({web: 2}),
|
||||||
paddingLeft: 3,
|
paddingLeft: 3,
|
||||||
paddingRight: 2,
|
paddingRight: 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user