fix(feed): card spacing, action-row distribution, tab strip, detail inset

- PostCard rows got cramped paddings and a near-invisible divider.
  Increased paddingTop 12→16, paddingBottom 12→18, paddingHorizontal
  14→16; divider colour #141414→#222222 so the seam between posts is
  legible on OLED blacks.
- Action row (chat / ❤ / view / share) used a fixed gap:32 + spacer.
  Reworked to four flex:1 cells with justifyContent: space-between,
  so the first three icons distribute evenly across the row and share
  pins to the right edge. Matches Twitter's layout where each action
  occupies a quarter of the row regardless of label width.
- Feed tab strip (Подписки / Для вас / В тренде) used flex:1 +
  gap:10 which bunched the three labels together visually. Switched
  to justifyContent: space-between + paddingHorizontal:20 so each
  tab hugs its label and the three labels spread to the edges with
  full horizontal breathing room.
- Post detail screen (/feed/[id]) and hashtag feed (/feed/tag/[tag])
  were missing the safe-area top inset — their headers butted right
  against the status bar / notch. Added useSafeAreaInsets().top as
  paddingTop on the outer View, matching the rest of the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
vsecoder
2026-04-18 20:20:18 +03:00
parent 93040a0684
commit 51bc0a1850
4 changed files with 86 additions and 71 deletions

View File

@@ -21,6 +21,7 @@ import {
} from 'react-native'; } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { router, useLocalSearchParams } from 'expo-router'; import { router, useLocalSearchParams } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Header } from '@/components/Header'; import { Header } from '@/components/Header';
import { IconButton } from '@/components/IconButton'; import { IconButton } from '@/components/IconButton';
@@ -32,6 +33,7 @@ import {
} from '@/lib/feed'; } from '@/lib/feed';
export default function PostDetailScreen() { export default function PostDetailScreen() {
const insets = useSafeAreaInsets();
const { id: postID } = useLocalSearchParams<{ id: string }>(); const { id: postID } = useLocalSearchParams<{ id: string }>();
const keyFile = useStore(s => s.keyFile); const keyFile = useStore(s => s.keyFile);
@@ -73,7 +75,7 @@ export default function PostDetailScreen() {
}, []); }, []);
return ( return (
<View style={{ flex: 1, backgroundColor: '#000000' }}> <View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
<Header <Header
divider divider
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />} left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}

View File

@@ -158,17 +158,18 @@ export default function FeedScreen() {
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}> <View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
<TabHeader title="Лента" /> <TabHeader title="Лента" />
{/* Tab strip — больше воздуха между табами. Горизонтальный padding {/* Tab strip — три таба, равномерно распределены по ширине
на контейнере + gap между Pressable'ами делает табы "breathable", (justifyContent: space-between). Каждый Pressable hug'ает
индикатор активной вкладки — тонкая полоска только под текстом, свой контент — табы НЕ тянутся на 1/3 ширины, а жмутся к
шириной примерно с лейбл. */} своему лейблу, что даёт воздух между ними. Индикатор активной
вкладки — тонкая полоска под лейблом. */}
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
paddingHorizontal: 12, justifyContent: 'space-between',
gap: 10, paddingHorizontal: 20,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#141414', borderBottomColor: '#1f1f1f',
}} }}
> >
{(Object.keys(TAB_LABELS) as TabKey[]).map(key => ( {(Object.keys(TAB_LABELS) as TabKey[]).map(key => (
@@ -176,33 +177,31 @@ export default function FeedScreen() {
key={key} key={key}
onPress={() => setTab(key)} onPress={() => setTab(key)}
style={({ pressed }) => ({ style={({ pressed }) => ({
flex: 1,
alignItems: 'center', alignItems: 'center',
paddingVertical: 16, paddingVertical: 16,
backgroundColor: pressed ? '#0a0a0a' : 'transparent', paddingHorizontal: 6,
opacity: pressed ? 0.6 : 1,
})} })}
> >
<Text <Text
style={{ style={{
color: tab === key ? '#ffffff' : '#6a6a6a', color: tab === key ? '#ffffff' : '#6a6a6a',
fontWeight: tab === key ? '700' : '500', fontWeight: tab === key ? '700' : '500',
fontSize: 14, fontSize: 15,
letterSpacing: -0.1, letterSpacing: -0.1,
}} }}
> >
{TAB_LABELS[key]} {TAB_LABELS[key]}
</Text> </Text>
{tab === key && (
<View <View
style={{ style={{
marginTop: 10, marginTop: 10,
width: 32, width: tab === key ? 28 : 0,
height: 3, height: 3,
borderRadius: 1.5, borderRadius: 1.5,
backgroundColor: '#1d9bf0', backgroundColor: '#1d9bf0',
}} }}
/> />
)}
</Pressable> </Pressable>
))} ))}
</View> </View>

View File

@@ -10,6 +10,7 @@ import {
} from 'react-native'; } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { router, useLocalSearchParams } from 'expo-router'; import { router, useLocalSearchParams } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Header } from '@/components/Header'; import { Header } from '@/components/Header';
import { IconButton } from '@/components/IconButton'; import { IconButton } from '@/components/IconButton';
@@ -18,6 +19,7 @@ import { useStore } from '@/lib/store';
import { fetchHashtag, fetchStats, type FeedPostItem } from '@/lib/feed'; import { fetchHashtag, fetchStats, type FeedPostItem } from '@/lib/feed';
export default function HashtagScreen() { export default function HashtagScreen() {
const insets = useSafeAreaInsets();
const { tag: rawTag } = useLocalSearchParams<{ tag: string }>(); const { tag: rawTag } = useLocalSearchParams<{ tag: string }>();
const tag = (rawTag ?? '').replace(/^#/, '').toLowerCase(); const tag = (rawTag ?? '').replace(/^#/, '').toLowerCase();
const keyFile = useStore(s => s.keyFile); const keyFile = useStore(s => s.keyFile);
@@ -75,7 +77,7 @@ export default function HashtagScreen() {
}, [keyFile]); }, [keyFile]);
return ( return (
<View style={{ flex: 1, backgroundColor: '#000000' }}> <View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
<Header <Header
divider divider
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />} left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}

View File

@@ -173,11 +173,12 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
onLongPress={onLongPress} onLongPress={onLongPress}
style={({ pressed }) => ({ style={({ pressed }) => ({
flexDirection: 'row', flexDirection: 'row',
paddingHorizontal: 14, paddingHorizontal: 16,
paddingVertical: compact ? 10 : 12, paddingTop: compact ? 12 : 16,
paddingBottom: compact ? 14 : 18,
backgroundColor: pressed ? '#080808' : 'transparent', backgroundColor: pressed ? '#080808' : 'transparent',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#141414', borderBottomColor: '#222222',
})} })}
> >
{/* Avatar column */} {/* Avatar column */}
@@ -246,20 +247,27 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
</View> </View>
)} )}
{/* Action row */} {/* Action row — 4 evenly-spaced buttons (Twitter-style). Each is
wrapped in a flex: 1 container so even if one label is
wider than another, visual spacing between centres stays
balanced. */}
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginTop: 8, justifyContent: 'space-between',
gap: 32, marginTop: 12,
paddingRight: 4,
}} }}
> >
<View style={{ flex: 1, alignItems: 'flex-start' }}>
<ActionButton <ActionButton
icon="chatbubble-outline" icon="chatbubble-outline"
label={formatCount(0) /* replies count — not implemented yet */} label={formatCount(0) /* replies count — not implemented yet */}
onPress={onOpenDetail} onPress={onOpenDetail}
/> />
</View>
<View style={{ flex: 1, alignItems: 'flex-start' }}>
<Pressable <Pressable
onPress={onToggleLike} onPress={onToggleLike}
disabled={busy} disabled={busy}
@@ -286,11 +294,14 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
{formatCount(localLikeCount)} {formatCount(localLikeCount)}
</Text> </Text>
</Pressable> </Pressable>
</View>
<View style={{ flex: 1, alignItems: 'flex-start' }}>
<ActionButton <ActionButton
icon="eye-outline" icon="eye-outline"
label={formatCount(post.views)} label={formatCount(post.views)}
/> />
<View style={{ flex: 1 }} /> </View>
<View style={{ alignItems: 'flex-end' }}>
<ActionButton <ActionButton
icon="share-outline" icon="share-outline"
onPress={() => { onPress={() => {
@@ -300,6 +311,7 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
/> />
</View> </View>
</View> </View>
</View>
</Pressable> </Pressable>
); );
} }