/** * 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, Image, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { router, useLocalSearchParams } from 'expo-router'; 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 { 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="Пост" /> {loading ? ( ) : error ? ( {error} ) : !post ? ( Пост удалён или больше недоступен ) : ( {/* Attachment preview (if any). For MVP we try loading from the CURRENT node — works when you're connected to the hosting relay. Cross-relay discovery (look up hosting_relay URL via /api/relays) is future work. */} {post.has_attachment && ( )} {/* Detailed stats block */} Информация о посте {post.hashtags && post.hashtags.length > 0 && ( <> Хештеги {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 AttachmentPreview({ postID }: { postID: string }) { // For MVP we hit the local node URL; if the body is hosted elsewhere // the image load will fail and the placeholder stays visible. const { getNodeUrl } = require('@/lib/api'); const url = `${getNodeUrl()}/feed/post/${postID}`; // The body is a JSON object, not raw image bytes. For now we just // show a placeholder — decoding base64 attachment → data-uri is a // Phase D improvement once we add /feed/post/{id}/attachment raw bytes. return ( Вложение: {url} Прямой просмотр вложений — в следующем релизе ); } function shortAddr(a: string, n = 6): string { if (!a) return '—'; return a.length <= n * 2 + 1 ? a : `${a.slice(0, n)}…${a.slice(-n)}`; } // Silence Image import when unused (reserved for future attachment preview). void Image;