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,65 +185,65 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
onLongPress={onLongPress}
style={({ pressed }) => ({
flexDirection: 'row',
paddingHorizontal: 16,
paddingLeft: 16,
paddingRight: 16,
paddingVertical: compact ? 10 : 12,
backgroundColor: pressed ? '#080808' : 'transparent',
})}
>
{/* Avatar column */}
<Pressable onPress={onOpenAuthor} hitSlop={4}>
{/* Avatar — own tap target (opens author profile). Explicit width
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} />
</Pressable>
{/* Content column. overflow:'hidden' stops a long unbreakable
token (URL, hashtag) from visually escaping the card — it'll
be ellipsized or clipped instead. */}
{/* Content column. overflow:'hidden' prevents unbreakable tokens
from drawing past the right edge of the card. */}
<View style={{ flex: 1, marginLeft: 10, minWidth: 0, overflow: 'hidden' }}>
{/* Header: [name] · [time] … [menu]
All three on a single row with no wrap. The name shrinks if
too long (flexShrink:1 + numberOfLines:1), time never shrinks
— so long handles get truncated with ellipsis while "2h"
stays readable.
Whole header is inside a single Pressable so a tap anywhere
on the header opens the author's profile — matches Twitter's
behaviour where the name-row is a big hit target. */}
<Pressable
onPress={onOpenAuthor}
hitSlop={{ top: 4, bottom: 2 }}
style={{ flexDirection: 'row', alignItems: 'center' }}
>
<Text
numberOfLines={1}
style={{
color: '#ffffff',
fontWeight: '700',
fontSize: 14,
letterSpacing: -0.2,
flexShrink: 1,
}}
{/* Header row — name + time on ONE line.
Two siblings: the author-link Pressable (flex:1, row, so it
expands; name inside gets numberOfLines:1 + flexShrink:1 so
it truncates instead of wrapping) and the "· <time>" tail
(flexShrink:0 — never truncates). Keeping the "·" inside the
time Text means they stay glued even if the inline layout
decides to break weirdly. */}
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Pressable
onPress={onOpenAuthor}
hitSlop={{ top: 4, bottom: 2 }}
style={{ flex: 1, flexDirection: 'row', alignItems: 'center', minWidth: 0 }}
>
{displayName}
</Text>
<Text
style={{ color: '#6a6a6a', fontSize: 13, marginHorizontal: 4 }}
numberOfLines={1}
>
·
</Text>
<Text
style={{ color: '#6a6a6a', fontSize: 13 }}
numberOfLines={1}
>
{formatRelativeTime(post.created_at)}
</Text>
<View style={{ flex: 1 }} />
<Text
numberOfLines={1}
style={{
color: '#ffffff',
fontWeight: '700',
fontSize: 14,
letterSpacing: -0.2,
flexShrink: 1,
}}
>
{displayName}
</Text>
<Text
numberOfLines={1}
style={{
color: '#6a6a6a',
fontSize: 13,
marginLeft: 6,
flexShrink: 0,
}}
>
· {formatRelativeTime(post.created_at)}
</Text>
</Pressable>
{mine && (
<Pressable onPress={onLongPress} hitSlop={8}>
<Pressable onPress={onLongPress} hitSlop={8} style={{ marginLeft: 8 }}>
<Ionicons name="ellipsis-horizontal" size={16} color="#6a6a6a" />
</Pressable>
)}
</Pressable>
</View>
{/* Body text with hashtag highlighting.
flexShrink:1 + explicit width:'100%' + paddingRight:4 keep
@@ -336,6 +336,9 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
color: localLiked ? '#e0245e' : '#6a6a6a',
fontSize: 12,
fontWeight: localLiked ? '600' : '400',
lineHeight: 16,
includeFontPadding: false,
textAlignVertical: 'center',
}}
>
{formatCount(localLikeCount)}
@@ -406,7 +409,22 @@ function ActionButton({ icon, label, onPress }: {
>
<Ionicons name={icon} size={16} color="#6a6a6a" />
{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>
);