Same Pressable dynamic-style bug that keeps reappearing: on some RN
builds the style function is re-evaluated during render in a way that
drops properties (previously hit on the FAB, then on PostCard, now on
the anti-spam fee selector). User saw three bare text labels on black
stuck together instead of distinct white/grey pills.
Fix: move visual properties (backgroundColor, borderWidth, padding,
border) to a static inner <View>. Pressable keeps only the opacity-
based press feedback which is stable because no other properties need
to flip on press. Functionally identical UX, layout guaranteed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Contact requests silently 404'd
fetchContactRequests hit /api/relay/contacts, but the server mounts
the whole /relay/* group at root (no /api prefix). Result: every
poll returned 404, the catch swallowed it, and the notifications
tab stayed empty even after the user sent themselves a CONTACT_
REQUEST on-chain. Fixed the client path to /relay/contacts — same
pattern as sendEnvelope / fetchInbox in the v1.0.x relay cleanup.
2. Search screen was half-finished
SearchBar used a dual-state hack (idle-centered Text overlaid with
an invisible TextInput) that broke focus + alignment on Android and
sometimes ate taps. Rewrote as a plain single-row pill: icon +
TextInput + optional clear button. Fewer moving parts, predictable
focus, proper placeholder styling.
new-contact.tsx cleaned up:
- Title "New chat" → "Поиск" (matches the NavBar tab label and the
rest of the Russian UI).
- All labels localised: "Accept/Decline", "Intro", "Anti-spam fee",
fee-tier names, error messages, final CTA.
- Proper empty-state hint when query is empty (icon + headline +
explanation of @username / hex / DC prefix) instead of just a
floating helper text.
- Search button hidden until user types something — the empty-
state stands alone, no dead grey button under it.
- onClear handler on SearchBar resets the resolved profile too.
requests.tsx localised: title, empty-state, Accept/Decline button
copy, confirmation Alert text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two dev-only seed modules removed now that the app talks to a real
backend:
- lib/devSeed.ts — fake 15+ contacts with mock chat histories,
mounted via useDevSeed() in (app)/_layout.tsx on empty store.
Was useful during client-first development; now it fights real
contact sync and confuses operators bringing up fresh nodes
("why do I see NBA scores and a dchain_updates channel in my
chat list?").
- lib/devSeedFeed.ts — 12 synthetic feed posts surfaced when the
real API returned empty. Same reasoning: operator imports genesis
key on a fresh node, opens Feed, sees 12 mock posts that aren't on
their chain. "Test data" that looks real is worse than an honest
empty state.
Feed screen now shows its proper empty state ("Пока нет
рекомендаций", etc.) when the API returns zero items OR on network
error. Chat screen starts empty until real contacts + messages
arrive via WS / storage cache.
Also cleaned a stale comment in chats/[id].tsx that referenced
devSeed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Feed list padding
FlatList had no inner padding so the first post bumped against the
tab strip and the last post against the NavBar. Added paddingTop: 8
/ paddingBottom: 24 on contentContainerStyle in both /feed and
/feed/tag/[tag] — first card now has a clear top gap, last card
doesn't get hidden behind the FAB or NavBar.
Share-to-chat flow
Replaces the placeholder share button (which showed an Alert with
the post URL) with a real "forward to chats" flow modeled on VK's
shared-wall-post embed.
New modules
lib/forwardPost.ts — encodePostRef / tryParsePostRef +
forwardPostToContacts(). Serialises a
feed post into a tiny JSON payload that
rides the same encrypted envelope as any
chat message; decode side distinguishes
"post_ref" payloads from regular text by
trying JSON.parse on decrypted text.
Mirrors the sent message into the sender's
local history so they see "you shared
this" in the chat they forwarded to.
components/feed/ShareSheet.tsx
— bottom-sheet picker. Multi-select
contacts via tick-box, search by
username / alias / address prefix.
"Send (N)" dispatches N parallel
encrypted envelopes. Contacts with no
X25519 key are filtered out (can't
encrypt for them).
components/chat/PostRefCard.tsx
— compact embedded-post card for chat
bubbles. Ribbon "ПОСТ" label +
author + 3-line excerpt + "с фото"
indicator. Tap → /(app)/feed/{id} full
post detail. Palette switches between
blue-bubble-friendly and peer-bubble-
friendly depending on bubble side.
Message pipeline
lib/types.ts — Message.postRef optional field added.
text stays "" when the message is a
post-ref (nothing to render as plain text).
hooks/useMessages.ts + hooks/useGlobalInbox.ts
— post decryption of every inbound envelope
runs through tryParsePostRef; matching
messages get the postRef attached instead
of the raw JSON in .text.
components/chat/MessageBubble.tsx
— renders PostRefCard inside the bubble when
msg.postRef is set. Other bubble features
(reply quote, attachment preview, text)
still work around it.
PostCard
- share icon now opens <ShareSheet>; the full-URL placeholder is
gone. ShareSheet is embedded at the PostCard level so each card
owns its own sheet state (avoids modal-stacking issues).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- Post action row (chat / ❤ / eye / share) had only a small paddingRight
and no left padding. First icon sat flush under the avatar and share
iron against the card edge. Replaced with paddingHorizontal: 12 so
both sides get equal breathing room; each of the four cells still
flex:1 so the icons distribute evenly.
- FAB kept appearing at the LEFT edge instead of the right on user's
device despite position:absolute + right:12. Pressable's dynamic-
function style can drop absolute-positioning fields between renders
on some RN versions. Wrapping the Pressable in a plain absolute-
positioned View fixes this: positioning lives on the View (never
re-evaluated mid-render), the Pressable inside only declares size
and visuals. pointerEvents="box-none" on the wrapper keeps taps
outside the button passing through to the feed list below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PostSeparator was a bare 1px line flush against both neighbouring
cards, so the seam looked like card contact instead of a real
break. Wrapped it in a 12px vertical padding — now there's 12px
blank above the line, 1px line, 12px blank below. Trimmed the
card's own paddingVertical back down since the separator now owns
the inter-post breathing room.
- FAB was lifted to bottom: max(insets.bottom, 8) + 70 in the previous
commit which put it far too high (a leftover from the original
experiment with the absoluteFill wrapper). User wants 12px above
the NavBar and 12px from the right edge. The Feed screen's
container ends at the NavBar top (enforced by (app)/_layout.tsx's
outer <View flex:1> + NavBar sibling), so a simple `right: 12,
bottom: 12` on position:absolute lands the button exactly there on
every device. Removed the now-unnecessary absoluteFill wrapper too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Post divider was on each PostCard's outer Pressable as borderBottom
(#222), which was barely visible on OLED black and disappeared
entirely in pressed state (the pressed bg ate the line). Moved the
seam to a dedicated PostSeparator component (1px, #2a2a2a) wired as
FlatList's ItemSeparatorComponent on both /feed (timeline / for-you
/ trending) and /feed/tag/[tag]. Also bumped inter-card vertical
padding (14-16 top / 16-20 bottom) so cards have real breathing room
even before the divider.
- FAB position was flaky: with <Stack> at the (app) level the overlay
could end up positioned against the Stack's card view instead of the
tab container, which made the button drift around and stick against
unexpected edges. Wrapped it in an absoluteFill container with
pointerEvents="box-none" — the wrapper owns positioning against the
tab screen, the button inside just declares right: 14 / bottom: N.
Bumped bottom offset to `max(insets.bottom, 8) + 70` so the FAB
always clears the 5-icon NavBar with ~14px visual gap on every
device. Shadow switched from blue-cast to standard dark for better
depth perception on dark backgrounds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PostCard rows got cramped paddings and a near-invisible divider.
Increased paddingTop 12→16, paddingBottom 12→18, paddingHorizontal
14→16; divider colour #141414→#222222 so the seam between posts is
legible on OLED blacks.
- Action row (chat / ❤ / view / share) used a fixed gap:32 + spacer.
Reworked to four flex:1 cells with justifyContent: space-between,
so the first three icons distribute evenly across the row and share
pins to the right edge. Matches Twitter's layout where each action
occupies a quarter of the row regardless of label width.
- Feed tab strip (Подписки / Для вас / В тренде) used flex:1 +
gap:10 which bunched the three labels together visually. Switched
to justifyContent: space-between + paddingHorizontal:20 so each
tab hugs its label and the three labels spread to the edges with
full horizontal breathing room.
- Post detail screen (/feed/[id]) and hashtag feed (/feed/tag/[tag])
were missing the safe-area top inset — their headers butted right
against the status bar / notch. Added useSafeAreaInsets().top as
paddingTop on the outer View, matching the rest of the app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related UX fixes on the client.
1. Participants count on profile
DMs always have exactly two participants (you and the contact) so a
"Участников: 1" row was confusing — either it's obviously the other
person or it's wrong depending on how you count. Removed for direct
conversations; the row still appears for group chats (and shows an
em-dash until v2.1.0 gives groups a real member list).
2. Dev feed seed now activates on network / 404 errors
The seed was only surfaced when the real API returned an EMPTY
array. If the node was down (Network request failed) or the endpoint
replied 404, the catch block quietly set posts to [] and the list
stayed blank — defeating the point of the seed. Now both the empty-
response path AND the network-error path fall back to getDevSeedFeed(),
so scrolling / like-toggling works even without a running node.
Also made the __DEV__ lookup more defensive: use `globalThis.__DEV__`
at runtime instead of the typed global. Some bundler configurations
have the TS type but not the runtime binding, or vice-versa — the
runtime lookup always agrees with Metro.
3. Back from profile → previous screen instead of tab root
Root cause: AnimatedSlot rendered <Slot>, which is stack-less. When
/chats/xyz pushed /profile/abc (cross-group), the chats group
unmounted. Hitting Back then re-entered chats at its root (/chats
list) rather than /chats/xyz.
Replaced <Slot> with <Stack> in AnimatedSlot. Tab switching still
stays flat because NavBar uses router.replace (which maps to
navigation.replace on the Stack — no history accumulation).
Cross-group pushes (post author tap from feed, avatar tap from chat
header, compose modal) now live in the outer Stack's history, so
Back pops correctly to the caller.
The nested Stacks (chats/_layout.tsx, feed/_layout.tsx,
profile/_layout.tsx) still handle intra-group navigation as before.
The PanResponder-based swipe-right-to-back was removed since the
native Stack now provides iOS edge-swipe natively; Android uses the
system back button.
animation: 'none' keeps the visual swap instant — matches the prior
Slot look so nothing flashes slide-animations that weren't there
before. Sub-group layouts can opt into slide_from_right themselves
(profile/_layout.tsx and feed/_layout.tsx already do).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
expo-router error: "A navigator cannot contain multiple 'Screen'
components with the same name (found duplicate screen named 'feed')".
Cause: having both app/(app)/feed.tsx (a file-route) and
app/(app)/feed/ (a folder with _layout.tsx) at the same level makes
expo-router try to register two routes called "feed".
Fix: the folder's _layout.tsx wraps the tab + its sub-routes in a
single Stack, so the tab screen becomes feed/index.tsx — the initial
screen of that Stack. Back navigation from /feed/[id] and
/feed/tag/[tag] now correctly pops to the Feed tab root.
No other route references changed (paths like /(app)/feed,
/(app)/feed/<id>, /(app)/feed/tag/<tag> still resolve identically).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Profile screen
- Missing safe-area top inset on the header — fixed by wrapping the
outer View in paddingTop: insets.top (matches the rest of the tab
screens).
- "Чат" button icon + text sat on two lines because the button used
column layout by default. Rewritten as a full-width CTA pill with
flexDirection: row and alignItems: center → chat-bubble icon and
label sit on one line.
- Dedicated "Копировать адрес" button removed. The address row in
the info card is now the tap target: pressing it copies to clipboard
and flips the row to "Скопировано" with a green check for 1.8s.
- Posts tab removed entirely. User's right — a "chat profile" has no
posts concept, just participant count + date + encryption. The
profile screen is now a single-view info card (address, encryption
status, added date, participants). Posts are discoverable via the
Feed tab; a dedicated /feed/author/{pub} screen is on the roadmap
for browsing a specific user's timeline.
Back-stack navigation
- app/(app)/profile/_layout.tsx + app/(app)/feed/_layout.tsx added,
each with a native <Stack>. The outer AnimatedSlot is stack-less
(intentional — it animates tab switches), so without these nested
stacks `router.back()` from a profile screen had no history to pop
and fell through to root. Now tapping an author in the feed → back
returns to feed; opening a profile from chat header → back returns
to the chat.
Dev feed seed
- lib/devSeedFeed.ts: 12 synthetic posts (text-only, mix of hashtags,
one with has_attachment, varying likes/views, timestamps from "1m
ago" to "36h ago"). Exposed via getDevSeedFeed() — stripped from
production via __DEV__ gate.
- Feed screen falls back to the dev seed only when the real API
returned zero posts. Gives something to scroll / tap / like-toggle
before the backend has real content; real data takes over as soon
as it arrives.
Note: tapping a mock post into detail will 404 against the real node
(the post_ids don't exist on-chain). Intentional — the seed is for
scroll + interaction feel, not deep linking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Composer footer (attach / counter / fee bar) now rides with the
keyboard on both platforms: KeyboardAvoidingView behavior is
"padding" on iOS and "height" on Android. Previously behavior was
undefined on Android, and the global softwareKeyboardLayoutMode:"pan"
setting lifted the whole screen — leaving the footer hidden below
the keyboard.
- Feed tab strip (Подписки / Для вас / В тренде) had zero horizontal
breathing room — three equal-width Pressables flush to the edges.
Added 12px outer paddingHorizontal + 10px gap + slightly bigger
paddingVertical. The active indicator is now 32px wide (was 48) so
it sits under the label instead of underlining two words.
- Floating compose FAB pinned to the right edge with a 14px inset
(was 18). Vertical offset tightened from +70 to +62 above the
safe-area inset — closer to the NavBar top edge while still clear
of the icons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ships the client side of the v2.0.0 feed feature. Folds client-app/
into the monorepo (was previously .gitignored as "tracked separately"
but no separate repo ever existed — for v2.0.0 the client is
first-class).
Feed screens
app/(app)/feed.tsx — Feed tab
- Three-way tab strip: Подписки / Для вас / В тренде backed by
/feed/timeline, /feed/foryou, /feed/trending respectively
- Default landing tab is "Для вас" — surfaces discovery without
requiring the user to follow anyone first
- FlatList with pull-to-refresh + viewability-driven view counter
bump (posts visible ≥ 60% for ≥ 1s trigger POST /feed/post/…/view)
- Floating blue compose button → /compose
- Per-post liked_by_me fetched in batches of 6 after list load
app/(app)/compose.tsx — post composer modal
- Fullscreen, Twitter-like header (✕ left, Опубликовать right)
- Auto-focused multiline TextInput, 4000 char cap
- Hashtag preview chips that auto-update as you type
- expo-image-picker + expo-image-manipulator pipeline: resize to
1080px max-dim, JPEG Q=50 (client-side first-pass compression
before the mandatory server-side scrub)
- Live fee estimate + balance guard with a confirmation modal
("Опубликовать пост? Цена: 0.00X T · Размер: N KB")
- Exif: false passed to ImagePicker as an extra privacy layer
app/(app)/feed/[id].tsx — post detail
- Full PostCard rendering + detailed info panel (views, likes,
size, fee, hosting relay, hashtags as tappable chips)
- Triggers bumpView on mount
- 410 (on-chain soft-delete) routes back to the feed
app/(app)/feed/tag/[tag].tsx — hashtag feed
app/(app)/profile/[address].tsx — rebuilt
- Twitter-ish profile: avatar, name, address short-form, post count
- Posts | Инфо tab strip
- Follow / Unfollow button for non-self profiles (optimistic UI)
- Edit button on self profile → settings
- Secondary actions (chat, copy address) when viewing a known contact
Supporting library
lib/feed.ts — HTTP wrappers + tx builders for every /feed/* endpoint:
- publishPost (POST /feed/publish, signed)
- publishAndCommit (publish → on-chain CREATE_POST)
- fetchPost / fetchStats / bumpView
- fetchAuthorPosts / fetchTimeline / fetchForYou / fetchTrending /
fetchHashtag
- buildCreatePostTx / buildDeletePostTx
- buildFollowTx / buildUnfollowTx
- buildLikePostTx / buildUnlikePostTx
- likePost / unlikePost / followUser / unfollowUser / deletePost
(high-level helpers that bundle build + submitTx)
- formatFee, formatRelativeTime, formatCount — Twitter-like display
helpers
components/feed/PostCard.tsx — core card component
- Memoised for performance (N-row re-render on every like elsewhere
would cost a lot otherwise)
- Optimistic like toggle with heart-bounce spring animation
- Hashtag highlighting in body text (tappable → hashtag feed)
- Long-press context menu (Delete, owner-only)
- Views / likes / share-link / reply icons in footer row
Navigation cleanup
- NavBar: removed the SOON pill on the Feed tab (it's shipped now)
- (app)/_layout: hide NavBar on /compose and /feed/* sub-routes
- AnimatedSlot: treat /feed/<id>, /feed/tag/<t>, /compose as
sub-routes so back-swipe-right closes them
Channel removal (client side)
- lib/types.ts: ContactKind stripped to 'direct' | 'group'; legacy
'channel' flag removed. `kind` field kept for backward compat with
existing AsyncStorage records.
- lib/devSeed.ts: dropped the 5 channel seed contacts.
- components/ChatTile.tsx: removed channel kindIcon branch.
Dependencies
- expo-image-manipulator added for client-side image compression.
- expo-file-system/legacy used for readAsStringAsync (SDK 54 moved
that API to the legacy sub-path; the new streaming API isn't yet
stable).
Type check
- npx tsc --noEmit — clean, 0 errors.
Next (not in this commit)
- Direct attachment-bytes endpoint on the server so post-detail can
actually render the image (currently shows placeholder with URL)
- Cross-relay body fetch via /api/relays + hosting_relay pubkey
- Mentions (@username) with notifications
- Full-text search
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Excluded from release bundle:
- CONTEXT.md, CHANGELOG.md (agent/project working notes)
- client-app/ (React Native messenger — tracked separately)
- contracts/hello_go/ (unused standalone example)
Kept contracts/counter/ and contracts/name_registry/ as vm-test fixtures
(referenced by vm/vm_test.go; NOT production contracts).
Docs refactor:
- docs/README.md — new top-level index with cross-references
- docs/quickstart.md — rewrite around single-node as primary path
- docs/node/README.md — full rewrite, all CLI flags, schema table
- docs/api/README.md — add /api/well-known-version, /api/update-check
- docs/contracts/README.md — split native (Go) vs WASM (user-deployable)
- docs/update-system.md — new, full 5-layer update system design
- README.md — link into docs/, drop CHANGELOG/client-app references
Build-time version system (inherited from earlier commits this branch):
- node --version / client --version with ldflags-injected metadata
- /api/well-known-version with {build, protocol_version, features[]}
- Peer-version gossip on dchain/version/v1
- /api/update-check against Gitea release API
- deploy/single/update.sh with semver guard + 15-min systemd jitter