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:
@@ -294,12 +294,60 @@ func feedPostRouter(cfg FeedConfig) http.HandlerFunc {
|
||||
feedPostStats(cfg)(w, r, postID)
|
||||
case "view":
|
||||
feedPostView(cfg)(w, r, postID)
|
||||
case "attachment":
|
||||
feedPostAttachment(cfg)(w, r, postID)
|
||||
default:
|
||||
jsonErr(w, fmt.Errorf("unknown sub-route %q", parts[1]), 404)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// feedPostAttachment handles GET /feed/post/{id}/attachment — returns the
|
||||
// raw attachment bytes with the correct Content-Type so clients can use
|
||||
// the URL directly as an <Image source={uri: ...}>.
|
||||
//
|
||||
// Why a dedicated endpoint? The /feed/post/{id} response wraps the body
|
||||
// as base64 inside JSON; fetching that + decoding for N posts in a feed
|
||||
// list would blow up memory. Native image loaders stream bytes straight
|
||||
// to the GPU — this route lets them do that without intermediate JSON.
|
||||
//
|
||||
// Respects on-chain soft-delete: returns 410 when the post is tombstoned.
|
||||
func feedPostAttachment(cfg FeedConfig) postHandler {
|
||||
return func(w http.ResponseWriter, r *http.Request, postID string) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonErr(w, fmt.Errorf("method not allowed"), 405)
|
||||
return
|
||||
}
|
||||
if cfg.GetPost != nil {
|
||||
if rec, _ := cfg.GetPost(postID); rec != nil && rec.Deleted {
|
||||
jsonErr(w, fmt.Errorf("post %s deleted", postID), 410)
|
||||
return
|
||||
}
|
||||
}
|
||||
post, err := cfg.Mailbox.Get(postID)
|
||||
if err != nil {
|
||||
jsonErr(w, err, 500)
|
||||
return
|
||||
}
|
||||
if post == nil || len(post.Attachment) == 0 {
|
||||
jsonErr(w, fmt.Errorf("no attachment for post %s", postID), 404)
|
||||
return
|
||||
}
|
||||
mime := post.AttachmentMIME
|
||||
if mime == "" {
|
||||
mime = "application/octet-stream"
|
||||
}
|
||||
w.Header().Set("Content-Type", mime)
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(post.Attachment)))
|
||||
// Cache for 1 hour — attachments are immutable (tied to content_hash),
|
||||
// so aggressive client-side caching is safe and saves bandwidth.
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600, immutable")
|
||||
w.Header().Set("ETag", `"`+postID+`"`)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(post.Attachment)
|
||||
}
|
||||
}
|
||||
|
||||
type postHandler func(w http.ResponseWriter, r *http.Request, postID string)
|
||||
|
||||
func feedGetPost(cfg FeedConfig) postHandler {
|
||||
|
||||
Reference in New Issue
Block a user