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:
@@ -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()} />}
|
||||||
|
|||||||
@@ -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: tab === key ? 28 : 0,
|
||||||
width: 32,
|
height: 3,
|
||||||
height: 3,
|
borderRadius: 1.5,
|
||||||
borderRadius: 1.5,
|
backgroundColor: '#1d9bf0',
|
||||||
backgroundColor: '#1d9bf0',
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -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()} />}
|
||||||
|
|||||||
@@ -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,58 +247,69 @@ 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,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActionButton
|
<View style={{ flex: 1, alignItems: 'flex-start' }}>
|
||||||
icon="chatbubble-outline"
|
<ActionButton
|
||||||
label={formatCount(0) /* replies count — not implemented yet */}
|
icon="chatbubble-outline"
|
||||||
onPress={onOpenDetail}
|
label={formatCount(0) /* replies count — not implemented yet */}
|
||||||
/>
|
onPress={onOpenDetail}
|
||||||
<Pressable
|
/>
|
||||||
onPress={onToggleLike}
|
</View>
|
||||||
disabled={busy}
|
<View style={{ flex: 1, alignItems: 'flex-start' }}>
|
||||||
hitSlop={8}
|
<Pressable
|
||||||
style={({ pressed }) => ({
|
onPress={onToggleLike}
|
||||||
flexDirection: 'row', alignItems: 'center', gap: 6,
|
disabled={busy}
|
||||||
opacity: pressed ? 0.5 : 1,
|
hitSlop={8}
|
||||||
})}
|
style={({ pressed }) => ({
|
||||||
>
|
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||||
<Animated.View style={{ transform: [{ scale: heartScale }] }}>
|
opacity: pressed ? 0.5 : 1,
|
||||||
<Ionicons
|
})}
|
||||||
name={localLiked ? 'heart' : 'heart-outline'}
|
|
||||||
size={16}
|
|
||||||
color={localLiked ? '#e0245e' : '#6a6a6a'}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: localLiked ? '#e0245e' : '#6a6a6a',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: localLiked ? '600' : '400',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{formatCount(localLikeCount)}
|
<Animated.View style={{ transform: [{ scale: heartScale }] }}>
|
||||||
</Text>
|
<Ionicons
|
||||||
</Pressable>
|
name={localLiked ? 'heart' : 'heart-outline'}
|
||||||
<ActionButton
|
size={16}
|
||||||
icon="eye-outline"
|
color={localLiked ? '#e0245e' : '#6a6a6a'}
|
||||||
label={formatCount(post.views)}
|
/>
|
||||||
/>
|
</Animated.View>
|
||||||
<View style={{ flex: 1 }} />
|
<Text
|
||||||
<ActionButton
|
style={{
|
||||||
icon="share-outline"
|
color: localLiked ? '#e0245e' : '#6a6a6a',
|
||||||
onPress={() => {
|
fontSize: 12,
|
||||||
// Placeholder — copy postID to clipboard in a future PR.
|
fontWeight: localLiked ? '600' : '400',
|
||||||
Alert.alert('Ссылка', `dchain://post/${post.post_id}`);
|
}}
|
||||||
}}
|
>
|
||||||
/>
|
{formatCount(localLikeCount)}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1, alignItems: 'flex-start' }}>
|
||||||
|
<ActionButton
|
||||||
|
icon="eye-outline"
|
||||||
|
label={formatCount(post.views)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={{ alignItems: 'flex-end' }}>
|
||||||
|
<ActionButton
|
||||||
|
icon="share-outline"
|
||||||
|
onPress={() => {
|
||||||
|
// Placeholder — copy postID to clipboard in a future PR.
|
||||||
|
Alert.alert('Ссылка', `dchain://post/${post.post_id}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
Reference in New Issue
Block a user