Three related layout polish issues.
1. Avatar + name + time splitting to two lines
Fixed by collapsing the "·" separator + time into a single Text
component (was three sibling Texts). RN occasionally lays out
three-Text-sibling rows oddly when the name is long because each
Text measures independently. With the separator glued to the time,
there's one indivisible chip for "· 2h" that flexShrink:0 keeps on
the row; the name takes the rest and truncates with "…" if needed.
Also wrapped the name + time in a flex:1 Pressable (author tap
target) so the row width is authoritative.
2. Avatar flush against the left edge
The outer Pressable's style-function paddingLeft was being applied
but the visual offset felt wrong because avatar has no explicit
width. Added width:44 to the avatar's own Pressable wrapper so the
flex-row layout reserves exactly the expected space and the 16-px
paddingLeft on the outer Pressable is visible around the avatar's
left edge.
3. Like / view count text visually below icon's centre
RN Text includes extra top padding (Android) and baseline-anchors
(iOS) that push the number glyph a pixel or two below the icon
centre in a flex-row with alignItems:'center'. Added explicit
lineHeight:16 (matching icon size) + includeFontPadding:false +
textAlignVertical:'center' on both the heart button's Text and the
shared ActionButton Text → numbers now sit on the same optical
baseline as their icons on both platforms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The body Text inherited the content column's width only implicitly
via flex:1 on the parent, which on some Android RN builds was
computed one or two pixels wider than the true column width — enough
to make a long line visually escape the right side of the card.
Belt-and-braces fix:
- content column gets overflow:'hidden' so anything that escapes
the computed width gets clipped instead of drawn outside.
- body Text explicitly sets width:'100%' + flexShrink:1 +
paddingRight:4 so the wrapping algorithm always knows the
authoritative maximum and leaves a 4-px visual buffer from the
column edge.
Together these guarantee long single-line posts wrap correctly on
both iOS and Android, and short-but-wide tokens (URLs, long
hashtags) ellipsize instead of poking out.
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>
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