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:
@@ -190,86 +190,80 @@ 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. */}
|
||||
<Pressable onPress={onOpenAuthor} hitSlop={4} style={{ width: 44 }}>
|
||||
<Avatar name={displayName} address={post.author} size={44} />
|
||||
</Pressable>
|
||||
{/* ── 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. */}
|
||||
{/* 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', marginLeft: 10, minWidth: 0 }}
|
||||
>
|
||||
<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} style={{ marginLeft: 8 }}>
|
||||
<Ionicons name="ellipsis-horizontal" size={16} color="#6a6a6a" />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ── CONTENT (body, attachment, actions) — full card width ──── */}
|
||||
<Pressable
|
||||
onPress={onOpenDetail}
|
||||
onLongPress={onLongPress}
|
||||
style={({ pressed }) => ({
|
||||
flex: 1,
|
||||
marginLeft: 10,
|
||||
minWidth: 0,
|
||||
marginTop: 8,
|
||||
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' }}>
|
||||
<Pressable
|
||||
onPress={onOpenAuthor}
|
||||
hitSlop={{ top: 4, bottom: 2 }}
|
||||
style={{ flex: 1, flexDirection: 'row', alignItems: 'center', minWidth: 0 }}
|
||||
>
|
||||
<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} style={{ marginLeft: 8 }}>
|
||||
<Ionicons name="ellipsis-horizontal" size={16} color="#6a6a6a" />
|
||||
</Pressable>
|
||||
)}
|
||||
</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. */}
|
||||
{/* 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)}
|
||||
|
||||
Reference in New Issue
Block a user