fix(feed): stack header + full-width content instead of avatar-sibling column

The previous layout put body text, attachments and action row inside
a column next to the avatar. Two recurring bugs came from that:

  1. The column's width = screen - 16 - 44 - 10 - 16 = screen - 86px.
     Long text or attachments computed against that narrower width,
     and on a few RN builds the measurement was off enough that text
     visibly ran past the card's right edge.
  2. The column visually looked weird: a photo rendered only 3/4 of
     the card width because the avatar stole 54px on the left.

Fix: make the card a vertical stack.

  ┌─────────────────────────────────────────┐
  │ [avatar] [name · time]          [menu]  │   ← HEADER row
  ├─────────────────────────────────────────┤
  │ body text, full card width              │   ← content column
  │ [attachment image, full card width]     │
  │ [action row, full card width]           │
  └─────────────────────────────────────────┘

Now body and media always occupy the full card width (paddingLeft:16
paddingRight:16 from the outer View), long lines wrap inside that,
and the earlier overflow-tricks / width-100% / paddingRight-4
band-aids aren't needed. Removed them.

Header row is unchanged structurally (avatar + name-row Pressable +
menu button) — just lifted into a dedicated View so the content
column below starts at the left card edge instead of alongside the
avatar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
vsecoder
2026-04-18 21:35:07 +03:00
parent 9be1b60ef1
commit ab98f21aac

View File

@@ -190,48 +190,33 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
we're not relying on it here either. Tap handling lives on the
content-column Pressable (covers ~90% of the card area) plus a
separate Pressable around the avatar. */}
{/* Card = vertical stack: [HEADER row with avatar+name+time] /
[FULL-WIDTH content column with body/image/actions]. Putting
content under the header (rather than in a column next to the
avatar) means body text and attachments occupy the full card
width — no risk of the text running next to the avatar and
clipping off the right edge. */}
<View
style={{
flexDirection: 'row',
paddingLeft: 16,
paddingRight: 16,
paddingVertical: compact ? 10 : 12,
}}
>
{/* 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. */}
{/* ── HEADER ROW: [avatar] [name · time] [menu] ──────────────── */}
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Pressable onPress={onOpenAuthor} hitSlop={4} style={{ width: 44 }}>
<Avatar name={displayName} address={post.author} size={44} />
</Pressable>
{/* Content column. Pressable so the card body is tappable →
detail; onLongPress routes to the context menu. overflow:
'hidden' prevents unbreakable tokens from drawing past the
right edge. */}
<Pressable
onPress={onOpenDetail}
onLongPress={onLongPress}
style={({ pressed }) => ({
flex: 1,
marginLeft: 10,
minWidth: 0,
overflow: 'hidden',
opacity: pressed ? 0.85 : 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' }}>
{/* Name + time take all remaining horizontal space in the
header, with the name truncating (numberOfLines:1 +
flexShrink:1) and the "· <time>" tail pinned to stay on
the same line (flexShrink:0). */}
<Pressable
onPress={onOpenAuthor}
hitSlop={{ top: 4, bottom: 2 }}
style={{ flex: 1, flexDirection: 'row', alignItems: 'center', minWidth: 0 }}
style={{ flex: 1, flexDirection: 'row', alignItems: 'center', marginLeft: 10, minWidth: 0 }}
>
<Text
numberOfLines={1}
@@ -257,6 +242,7 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
· {formatRelativeTime(post.created_at)}
</Text>
</Pressable>
{mine && (
<Pressable onPress={onLongPress} hitSlop={8} style={{ marginLeft: 8 }}>
<Ionicons name="ellipsis-horizontal" size={16} color="#6a6a6a" />
@@ -264,12 +250,20 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
)}
</View>
{/* Body text with hashtag highlighting.
flexShrink:1 + explicit width:'100%' + paddingRight:4 keep
long lines inside the content column on every platform. On
Android a few RN versions have been known to let the inner
Text spans overflow the parent by 1-2 px without an explicit
width declaration — hence the belt-and-braces here. */}
{/* ── CONTENT (body, attachment, actions) — full card width ──── */}
<Pressable
onPress={onOpenDetail}
onLongPress={onLongPress}
style={({ pressed }) => ({
marginTop: 8,
overflow: 'hidden',
opacity: pressed ? 0.85 : 1,
})}
>
{/* Body text with hashtag highlighting. Full card width now
(we moved it out of the avatar-sibling column) — no special
width tricks needed, normal wrapping just works. */}
{post.content.length > 0 && (
<Text
numberOfLines={bodyLines}
@@ -278,10 +272,6 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
color: '#ffffff',
fontSize: 15,
lineHeight: 20,
marginTop: 2,
width: '100%',
flexShrink: 1,
paddingRight: 4,
}}
>
{renderInline(post.content)}