fix(client): profile polish + proper back stack + dev feed seed

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>
This commit is contained in:
vsecoder
2026-04-18 20:08:48 +03:00
parent 0ff2760a11
commit 5728cfc85a
5 changed files with 451 additions and 338 deletions

View File

@@ -0,0 +1,181 @@
/**
* Dev-only mock posts for the feed.
*
* Why: in __DEV__ before any real posts exist on the node, the timeline/
* for-you/trending tabs come back empty. Empty state is fine visually but
* doesn't let you test scrolling, like animations, view-counter bumps,
* navigation to post detail, etc. This module injects a small set of
* synthetic posts so the UI has something to chew on.
*
* Gating:
* - Only active when __DEV__ === true (stripped from production builds).
* - Only surfaces when the REAL API returns an empty array. If the node
* is returning actual posts, we trust those and skip the mocks.
*
* These posts have made-up post_ids — tapping on them to open detail
* WILL 404 against the real backend. That's intentional — the mock is
* purely for scroll / tap-feedback testing.
*/
import type { FeedPostItem } from './feed';
// Fake hex-like pubkeys so Avatar's colour hash still looks varied.
function fakeAddr(seed: number): string {
const h = (seed * 2654435761).toString(16).padStart(8, '0');
return (h + h + h + h).slice(0, 64);
}
function fakePostID(n: number): string {
return `dev${String(n).padStart(29, '0')}`;
}
const NOW = Math.floor(Date.now() / 1000);
// Small curated pool of posts covering the render surface we care about:
// plain text, hashtag variety, different lengths, likes / views spread,
// reply/quote references, one with an attachment marker.
const SEED_POSTS: FeedPostItem[] = [
{
post_id: fakePostID(1),
author: fakeAddr(1),
content: 'Добро пожаловать в ленту DChain. Это #DEV-посты — они видны только пока реальная лента пустая.',
created_at: NOW - 60,
size: 200,
hosting_relay: fakeAddr(100),
views: 127, likes: 42,
has_attachment: false,
hashtags: ['dev'],
},
{
post_id: fakePostID(2),
author: fakeAddr(2),
content: 'Пробую новую ленту #twitter-style. Лайки, просмотры, подписки — всё on-chain, тела постов — off-chain в mailbox релея.',
created_at: NOW - 540,
size: 310,
hosting_relay: fakeAddr(100),
views: 89, likes: 23,
has_attachment: false,
hashtags: ['twitter'],
},
{
post_id: fakePostID(3),
author: fakeAddr(3),
content: 'Сжатие изображений — максимальное на клиенте (WebP Q=50 @1080p), плюс серверный EXIF-скраб через stdlib re-encode. GPS-координаты из EXIF больше никогда не утекают. #privacy',
created_at: NOW - 1200,
size: 420,
hosting_relay: fakeAddr(100),
views: 312, likes: 78,
has_attachment: true,
hashtags: ['privacy'],
},
{
post_id: fakePostID(4),
author: fakeAddr(4),
content: 'Короткий пост.',
created_at: NOW - 3600,
size: 128,
hosting_relay: fakeAddr(100),
views: 12, likes: 3,
has_attachment: false,
},
{
post_id: fakePostID(5),
author: fakeAddr(1),
content: 'Отвечаю сам себе — фича threads пока через reply_to только, без UI thread-виджета.',
created_at: NOW - 7200,
size: 220,
hosting_relay: fakeAddr(100),
views: 45, likes: 11,
has_attachment: false,
reply_to: fakePostID(1),
},
{
post_id: fakePostID(6),
author: fakeAddr(5),
content: '#golang + #badgerdb + #libp2p = DChain бэкенд. Пять package в test suite, все зелёные.',
created_at: NOW - 10800,
size: 180,
hosting_relay: fakeAddr(100),
views: 201, likes: 66,
has_attachment: false,
hashtags: ['golang', 'badgerdb', 'libp2p'],
},
{
post_id: fakePostID(7),
author: fakeAddr(6),
content: 'Feed-mailbox хранит тела постов до 30 дней (настраиваемо через DCHAIN_FEED_TTL_DAYS). Потом BadgerDB выселяет автоматически — chain-метаданные остаются навсегда.',
created_at: NOW - 14400,
size: 380,
hosting_relay: fakeAddr(100),
views: 156, likes: 48,
has_attachment: false,
},
{
post_id: fakePostID(8),
author: fakeAddr(7),
content: 'Pricing: BasePostFee = 1000 µT (0.001 T) + 1 µT за каждый байт. Уходит владельцу релея, принявшего пост.',
created_at: NOW - 21600,
size: 250,
hosting_relay: fakeAddr(100),
views: 78, likes: 22,
has_attachment: false,
},
{
post_id: fakePostID(9),
author: fakeAddr(8),
content: 'Twitter-like, но без миллиардов долларов на инфраструктуру — каждый оператор ноды платит за свой кусок хостинга и зарабатывает на публикациях. #decentralised #messaging',
created_at: NOW - 43200,
size: 340,
hosting_relay: fakeAddr(100),
views: 412, likes: 103,
has_attachment: false,
hashtags: ['decentralised', 'messaging'],
},
{
post_id: fakePostID(10),
author: fakeAddr(9),
content: 'Короче. Лайк = on-chain tx с fee 1000 µT. Дорого для спама, дёшево для реального лайка. Пока без батчинга, но в плане. #design',
created_at: NOW - 64800,
size: 200,
hosting_relay: fakeAddr(100),
views: 92, likes: 29,
has_attachment: false,
hashtags: ['design'],
},
{
post_id: fakePostID(11),
author: fakeAddr(2),
content: 'Follow граф на chain: двусторонний индекс (forward + inbound), так что Followers() и Following() — оба O(M).',
created_at: NOW - 86400 - 1000,
size: 230,
hosting_relay: fakeAddr(100),
views: 61, likes: 14,
has_attachment: false,
},
{
post_id: fakePostID(12),
author: fakeAddr(10),
content: 'Рекомендации (For You): берём последние 48ч постов, фильтруем подписки + уже лайкнутые + свои, ранжируем по likes × 3 + views. Версия 1 — будет умнее. #recsys',
created_at: NOW - 129600,
size: 290,
hosting_relay: fakeAddr(100),
views: 189, likes: 58,
has_attachment: false,
hashtags: ['recsys'],
},
];
/** True when the current build is a Metro dev bundle. __DEV__ is a
* global injected by Metro at bundle time and typed via react-native's
* ambient declarations, so no ts-ignore is needed. */
function isDev(): boolean {
return typeof __DEV__ !== 'undefined' && __DEV__ === true;
}
/**
* Returns the dev-seed post list (only in __DEV__). Called by the Feed
* screen as a fallback when the real API returned an empty list.
*/
export function getDevSeedFeed(): FeedPostItem[] {
if (!isDev()) return [];
return SEED_POSTS;
}