feat(feed/chat): lazy-render + pagination for long scrolls
Server pagination
- blockchain.PostsByAuthor signature extended with beforeTs int64;
passing 0 keeps the previous "everything, newest first" behaviour,
non-zero skips posts with CreatedAt >= beforeTs so clients can
paginate older results.
- node.FeedConfig.PostsByAuthor callback type updated; the two
/feed endpoints that use it (timeline + author) now accept
`?before=<unix_seconds>` and forward it through. /feed/author
limit default dropped from 50 to 30 to match the client's page
size.
- node/api_common.go: new queryInt64 helper for parsing the cursor
param safely (matches the queryInt pattern already used).
Client infinite scroll (Feed tab)
- lib/feed.ts: fetchTimeline / fetchAuthorPosts accept
`{limit?, before?}` options. Old signatures still work for other
callers (fetchForYou / fetchTrending / fetchHashtag) — those are
ranked feeds that don't have a stable cursor so they stay
single-shot.
- feed/index.tsx: tracks loadingMore / exhausted state. onEndReached
(threshold 0.6) fires loadMore() which fetches the next 20 posts
using the oldest currently-loaded post's created_at as `before`.
Deduplicates on post_id before appending. Stops when the server
returns < PAGE_SIZE items. ListFooterComponent shows a small
spinner during paginated fetches.
- FlatList lazy-render tuning on all feed lists (index + hashtag):
initialNumToRender:10, maxToRenderPerBatch:8, windowSize:7,
removeClippedSubviews — first paint stays quick even with 100+
posts loaded.
Chat lazy render
- chats/[id].tsx FlatList: initialNumToRender:25 (~1.5 screens),
maxToRenderPerBatch:12, windowSize:10, removeClippedSubviews.
Keeps initial chat open snappy on conversations with thousands
of messages; RN re-renders a small window around the viewport
and drops the rest.
Tests
- chain_test.go updated for new PostsByAuthor signature.
- All 7 Go packages green.
- tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,20 @@ func queryInt(r *http.Request, key string, def int) int {
|
||||
return n
|
||||
}
|
||||
|
||||
// queryInt64 reads a non-negative int64 query param — typically a unix
|
||||
// timestamp cursor for pagination. Returns def when missing or invalid.
|
||||
func queryInt64(r *http.Request, key string, def int64) int64 {
|
||||
s := r.URL.Query().Get(key)
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil || n < 0 {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// queryIntMin0 parses a query param as a non-negative integer; returns 0 if absent or invalid.
|
||||
func queryIntMin0(r *http.Request, key string) int {
|
||||
s := r.URL.Query().Get(key)
|
||||
|
||||
@@ -72,7 +72,7 @@ type FeedConfig struct {
|
||||
GetPost func(postID string) (*blockchain.PostRecord, error)
|
||||
LikeCount func(postID string) (uint64, error)
|
||||
HasLiked func(postID, likerPub string) (bool, error)
|
||||
PostsByAuthor func(authorPub string, limit int) ([]*blockchain.PostRecord, error)
|
||||
PostsByAuthor func(authorPub string, beforeTs int64, limit int) ([]*blockchain.PostRecord, error)
|
||||
Following func(followerPub string) ([]string, error)
|
||||
}
|
||||
|
||||
@@ -440,13 +440,14 @@ func feedAuthor(cfg FeedConfig) http.HandlerFunc {
|
||||
jsonErr(w, fmt.Errorf("author pub required"), 400)
|
||||
return
|
||||
}
|
||||
limit := queryInt(r, "limit", 50)
|
||||
limit := queryInt(r, "limit", 30)
|
||||
beforeTs := queryInt64(r, "before", 0) // pagination cursor (unix seconds)
|
||||
|
||||
// Prefer chain-authoritative list (includes soft-deleted flag) so
|
||||
// clients can't be fooled by a stale relay that has an already-
|
||||
// deleted post. If chain isn't wired, fall back to relay index.
|
||||
if cfg.PostsByAuthor != nil {
|
||||
records, err := cfg.PostsByAuthor(pub, limit)
|
||||
records, err := cfg.PostsByAuthor(pub, beforeTs, limit)
|
||||
if err != nil {
|
||||
jsonErr(w, err, 500)
|
||||
return
|
||||
@@ -461,6 +462,8 @@ func feedAuthor(cfg FeedConfig) http.HandlerFunc {
|
||||
jsonOK(w, map[string]any{"author": pub, "count": len(out), "posts": out})
|
||||
return
|
||||
}
|
||||
// Fallback: relay index (no chain). Doesn't support `before` yet;
|
||||
// the chain-authoritative path above is what production serves.
|
||||
ids, err := cfg.Mailbox.PostsByAuthor(pub, limit)
|
||||
if err != nil {
|
||||
jsonErr(w, err, 500)
|
||||
@@ -556,7 +559,8 @@ func feedTimeline(cfg FeedConfig) http.HandlerFunc {
|
||||
jsonErr(w, fmt.Errorf("timeline requires chain queries"), 503)
|
||||
return
|
||||
}
|
||||
limit := queryInt(r, "limit", 50)
|
||||
limit := queryInt(r, "limit", 30)
|
||||
beforeTs := queryInt64(r, "before", 0) // pagination cursor
|
||||
perAuthor := limit
|
||||
if perAuthor > 30 {
|
||||
perAuthor = 30
|
||||
@@ -569,7 +573,7 @@ func feedTimeline(cfg FeedConfig) http.HandlerFunc {
|
||||
}
|
||||
var merged []*blockchain.PostRecord
|
||||
for _, target := range following {
|
||||
posts, err := cfg.PostsByAuthor(target, perAuthor)
|
||||
posts, err := cfg.PostsByAuthor(target, beforeTs, perAuthor)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user