fix(feed): header on one line, avatar padding explicit, icon/label aligned

Three related layout polish issues.

1. Avatar + name + time splitting to two lines

   Fixed by collapsing the "·" separator + time into a single Text
   component (was three sibling Texts). RN occasionally lays out
   three-Text-sibling rows oddly when the name is long because each
   Text measures independently. With the separator glued to the time,
   there's one indivisible chip for "·  2h" that flexShrink:0 keeps on
   the row; the name takes the rest and truncates with "…" if needed.
   Also wrapped the name + time in a flex:1 Pressable (author tap
   target) so the row width is authoritative.

2. Avatar flush against the left edge

   The outer Pressable's style-function paddingLeft was being applied
   but the visual offset felt wrong because avatar has no explicit
   width. Added width:44 to the avatar's own Pressable wrapper so the
   flex-row layout reserves exactly the expected space and the 16-px
   paddingLeft on the outer Pressable is visible around the avatar's
   left edge.

3. Like / view count text visually below icon's centre

   RN Text includes extra top padding (Android) and baseline-anchors
   (iOS) that push the number glyph a pixel or two below the icon
   centre in a flex-row with alignItems:'center'. Added explicit
   lineHeight:16 (matching icon size) + includeFontPadding:false +
   textAlignVertical:'center' on both the heart button's Text and the
   shared ActionButton Text → numbers now sit on the same optical
   baseline as their icons on both platforms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
vsecoder
2026-04-18 21:18:29 +03:00
parent 38ae80f57a
commit 50aacced0b

View File

@@ -185,33 +185,34 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
onLongPress={onLongPress} onLongPress={onLongPress}
style={({ pressed }) => ({ style={({ pressed }) => ({
flexDirection: 'row', flexDirection: 'row',
paddingHorizontal: 16, paddingLeft: 16,
paddingRight: 16,
paddingVertical: compact ? 10 : 12, paddingVertical: compact ? 10 : 12,
backgroundColor: pressed ? '#080808' : 'transparent', backgroundColor: pressed ? '#080808' : 'transparent',
})} })}
> >
{/* Avatar column */} {/* Avatar — own tap target (opens author profile). Explicit width
<Pressable onPress={onOpenAuthor} hitSlop={4}> on the wrapper (width:44) so the flex-row sibling below computes
its remaining space correctly. */}
<Pressable onPress={onOpenAuthor} hitSlop={4} style={{ width: 44 }}>
<Avatar name={displayName} address={post.author} size={44} /> <Avatar name={displayName} address={post.author} size={44} />
</Pressable> </Pressable>
{/* Content column. overflow:'hidden' stops a long unbreakable {/* Content column. overflow:'hidden' prevents unbreakable tokens
token (URL, hashtag) from visually escaping the card — it'll from drawing past the right edge of the card. */}
be ellipsized or clipped instead. */}
<View style={{ flex: 1, marginLeft: 10, minWidth: 0, overflow: 'hidden' }}> <View style={{ flex: 1, marginLeft: 10, minWidth: 0, overflow: 'hidden' }}>
{/* Header: [name] · [time] … [menu] {/* Header row — name + time on ONE line.
All three on a single row with no wrap. The name shrinks if Two siblings: the author-link Pressable (flex:1, row, so it
too long (flexShrink:1 + numberOfLines:1), time never shrinks expands; name inside gets numberOfLines:1 + flexShrink:1 so
— so long handles get truncated with ellipsis while "2h" it truncates instead of wrapping) and the "· <time>" tail
stays readable. (flexShrink:0 — never truncates). Keeping the "·" inside the
time Text means they stay glued even if the inline layout
Whole header is inside a single Pressable so a tap anywhere decides to break weirdly. */}
on the header opens the author's profile — matches Twitter's <View style={{ flexDirection: 'row', alignItems: 'center' }}>
behaviour where the name-row is a big hit target. */}
<Pressable <Pressable
onPress={onOpenAuthor} onPress={onOpenAuthor}
hitSlop={{ top: 4, bottom: 2 }} hitSlop={{ top: 4, bottom: 2 }}
style={{ flexDirection: 'row', alignItems: 'center' }} style={{ flex: 1, flexDirection: 'row', alignItems: 'center', minWidth: 0 }}
> >
<Text <Text
numberOfLines={1} numberOfLines={1}
@@ -226,24 +227,23 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
{displayName} {displayName}
</Text> </Text>
<Text <Text
style={{ color: '#6a6a6a', fontSize: 13, marginHorizontal: 4 }}
numberOfLines={1} numberOfLines={1}
style={{
color: '#6a6a6a',
fontSize: 13,
marginLeft: 6,
flexShrink: 0,
}}
> >
· · {formatRelativeTime(post.created_at)}
</Text> </Text>
<Text </Pressable>
style={{ color: '#6a6a6a', fontSize: 13 }}
numberOfLines={1}
>
{formatRelativeTime(post.created_at)}
</Text>
<View style={{ flex: 1 }} />
{mine && ( {mine && (
<Pressable onPress={onLongPress} hitSlop={8}> <Pressable onPress={onLongPress} hitSlop={8} style={{ marginLeft: 8 }}>
<Ionicons name="ellipsis-horizontal" size={16} color="#6a6a6a" /> <Ionicons name="ellipsis-horizontal" size={16} color="#6a6a6a" />
</Pressable> </Pressable>
)} )}
</Pressable> </View>
{/* Body text with hashtag highlighting. {/* Body text with hashtag highlighting.
flexShrink:1 + explicit width:'100%' + paddingRight:4 keep flexShrink:1 + explicit width:'100%' + paddingRight:4 keep
@@ -336,6 +336,9 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
color: localLiked ? '#e0245e' : '#6a6a6a', color: localLiked ? '#e0245e' : '#6a6a6a',
fontSize: 12, fontSize: 12,
fontWeight: localLiked ? '600' : '400', fontWeight: localLiked ? '600' : '400',
lineHeight: 16,
includeFontPadding: false,
textAlignVertical: 'center',
}} }}
> >
{formatCount(localLikeCount)} {formatCount(localLikeCount)}
@@ -406,7 +409,22 @@ function ActionButton({ icon, label, onPress }: {
> >
<Ionicons name={icon} size={16} color="#6a6a6a" /> <Ionicons name={icon} size={16} color="#6a6a6a" />
{label && ( {label && (
<Text style={{ color: '#6a6a6a', fontSize: 12 }}>{label}</Text> <Text
style={{
color: '#6a6a6a',
fontSize: 12,
// Match icon height (16) as lineHeight so baseline-anchored
// text aligns with the icon's vertical centre. Without this,
// RN renders Text one pixel lower than the icon mid-point on
// most Android fonts, which looks sloppy next to the heart /
// eye glyphs.
lineHeight: 16,
includeFontPadding: false,
textAlignVertical: 'center',
}}
>
{label}
</Text>
)} )}
</Pressable> </Pressable>
); );