feat(feed): image previews + inline header + 5-line truncation + drop comments
Server
node/api_feed.go: new GET /feed/post/{id}/attachment route. Returns
raw attachment bytes with the correct Content-Type so React Native's
<Image source={uri}> can stream them directly without the client
fetching + decoding base64 from the main /feed/post/{id} JSON (would
blow up memory on a 40-post timeline). Respects on-chain soft-delete
(410 when tombstoned). Cache-Control: public, max-age=3600, immutable
— attachments are content-addressed so aggressive caching is safe.
PostCard — rewritten header row
- Avatar + name + time collapsed into a single Pressable row with
flexDirection:'row'. Name gets flexShrink:1 + numberOfLines:1 so
long handles truncate with "…" mid-row instead of pushing the time
onto a second line. Time and separator dot both numberOfLines:1
with no flex — they never shrink, so "2h" stays readable.
- Whole header is one tap target → navigates to the author's profile.
PostCard — body truncation
- Timeline view (compact=false): numberOfLines={5} + ellipsizeMode:
'tail'. Long posts collapse to 5 lines with "…"; tapping the card
opens the detail view where the full body is shown.
- Detail view (compact=true): no line cap — full text, then full-
size attachment below.
PostCard — real image previews
- <Image source={{ uri: `${node}/feed/post/${id}/attachment` }}>
(feed layout).
- Timeline: aspectRatio: 4/5 + resizeMode:'cover' — portrait photos
get cropped so one tall image can't eat the whole feed.
- Detail: aspectRatio: 1 + resizeMode:'contain' so the full image
fits in its original proportions (crop-free).
PostCard — comments button removed
v2.0.0 doesn't implement replies; a dead button with label "0" was
noise. Action row now has 3 cells: heart (with live like count),
eye (views), share (pinned right). Spacing stays balanced because
each of the first two cells is still flex:1.
Post detail screen
- Passes compact prop so the PostCard above renders in full-body /
full-attachment mode.
- Dropped the old AttachmentPreview placeholder — PostCard now
handles images in both modes.
Tests
- go test ./... — all 7 packages green (blockchain / consensus /
identity / media / node / relay / vm).
- tsc --noEmit on client-app — 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
View, Text, ScrollView, ActivityIndicator, Image,
|
||||
View, Text, ScrollView, ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
@@ -99,21 +99,17 @@ export default function PostDetailScreen() {
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView>
|
||||
{/* `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. */}
|
||||
<PostCard
|
||||
compact
|
||||
post={{ ...post, likes: stats?.likes ?? post.likes, views: stats?.views ?? post.views }}
|
||||
likedByMe={stats?.liked_by_me ?? false}
|
||||
onStatsChanged={onStatsChanged}
|
||||
onDeleted={onDeleted}
|
||||
/>
|
||||
|
||||
{/* 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 && (
|
||||
<AttachmentPreview postID={post.post_id} />
|
||||
)}
|
||||
|
||||
{/* Detailed stats block */}
|
||||
<View
|
||||
style={{
|
||||
@@ -204,41 +200,7 @@ function DetailRow({ label, value, mono }: { label: string; value: string; mono?
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View
|
||||
style={{
|
||||
margin: 14,
|
||||
paddingVertical: 32,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#0a0a0a',
|
||||
borderWidth: 1,
|
||||
borderColor: '#1f1f1f',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Ionicons name="image-outline" size={32} color="#5a5a5a" />
|
||||
<Text style={{ color: '#8b8b8b', marginTop: 10, fontSize: 12 }}>
|
||||
Вложение: {url}
|
||||
</Text>
|
||||
<Text style={{ color: '#5a5a5a', marginTop: 4, fontSize: 10 }}>
|
||||
Прямой просмотр вложений — в следующем релизе
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user