/** * Post detail — full view of one post with stats, thread context, and a * lazy-rendered image attachment. * * Why a dedicated screen? * - PostCard in the timeline intentionally doesn't render attachments * (would explode initial render time with N images). * - Per-post stats (views, likes, liked_by_me) want a fresh refresh * on open; timeline batches but not at the per-second cadence a * reader expects when they just tapped in. * * Layout: * [← back · Пост] * [PostCard (full — with attachment)] * [stats bar: views · likes · fee] * [— reply affordance below (future)] */ import React, { useCallback, useEffect, useState } from 'react'; import { View, Text, ScrollView, ActivityIndicator, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { router, useLocalSearchParams } from 'expo-router'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Header } from '@/components/Header'; import { IconButton } from '@/components/IconButton'; import { PostCard } from '@/components/feed/PostCard'; import { useStore } from '@/lib/store'; import { fetchPost, fetchStats, bumpView, formatCount, formatFee, type FeedPostItem, type PostStats, } from '@/lib/feed'; export default function PostDetailScreen() { const insets = useSafeAreaInsets(); const { id: postID } = useLocalSearchParams<{ id: string }>(); const keyFile = useStore(s => s.keyFile); const [post, setPost] = useState(null); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = useCallback(async () => { if (!postID) return; setLoading(true); setError(null); try { const [p, s] = await Promise.all([ fetchPost(postID), fetchStats(postID, keyFile?.pub_key), ]); setPost(p); setStats(s); if (p) bumpView(postID); // fire-and-forget } catch (e: any) { setError(String(e?.message ?? e)); } finally { setLoading(false); } }, [postID, keyFile]); useEffect(() => { load(); }, [load]); const onStatsChanged = useCallback(async () => { if (!postID) return; const s = await fetchStats(postID, keyFile?.pub_key); if (s) setStats(s); }, [postID, keyFile]); const onDeleted = useCallback(() => { // Go back to feed — the post is gone. router.back(); }, []); return (
router.back()} />} title="Post" /> {loading ? ( ) : error ? ( {error} ) : !post ? ( Post deleted or no longer available ) : ( {/* `compact` tells PostCard to drop the 5-line body cap and render the attachment at its natural aspect ratio instead of the portrait-cropped timeline preview. */} {/* Detailed stats block */} Post details {post.hashtags && post.hashtags.length > 0 && ( <> Hashtags {post.hashtags.map(tag => ( router.push(`/(app)/feed/tag/${encodeURIComponent(tag)}` as never)} style={{ color: '#1d9bf0', fontSize: 13, paddingHorizontal: 8, paddingVertical: 3, backgroundColor: '#081a2a', borderRadius: 999, }} > #{tag} ))} )} )} ); } function DetailRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) { return ( {label} {value} ); } function shortAddr(a: string, n = 6): string { if (!a) return '—'; return a.length <= n * 2 + 1 ? a : `${a.slice(0, n)}…${a.slice(-n)}`; }