chore(client): remove dev-seed for chats + feed

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>
This commit is contained in:
vsecoder
2026-04-18 23:07:42 +03:00
parent 6ed4e7ca50
commit 3e9ddc1a43
5 changed files with 7 additions and 640 deletions

View File

@@ -23,7 +23,6 @@ import { useWellKnownContracts } from '@/hooks/useWellKnownContracts';
import { useNotifications } from '@/hooks/useNotifications';
import { useGlobalInbox } from '@/hooks/useGlobalInbox';
import { getWSClient } from '@/lib/ws';
import { useDevSeed } from '@/lib/devSeed';
import { NavBar } from '@/components/NavBar';
import { AnimatedSlot } from '@/components/AnimatedSlot';
@@ -45,7 +44,6 @@ export default function AppLayout() {
useBalance();
useContacts();
useWellKnownContracts();
useDevSeed();
useNotifications(); // permission + tap-handler
useGlobalInbox(); // global inbox listener → notifications on new peer msg

View File

@@ -121,9 +121,9 @@ export default function ChatScreen() {
// Восстановить сообщения из persistent-storage при первом заходе в чат.
//
// Важно: НЕ перезаписываем store пустым массивом — это стёрло бы
// содержимое, которое уже лежит в zustand (например, из devSeed или
// только что полученные по WS сообщения пока монтировались). Если
// в кэше что-то есть — мержим: берём max(cached, in-store) по id.
// содержимое, которое уже лежит в zustand (только что полученные по
// WS сообщения пока монтировались). Если в кэше что-то есть — мержим:
// берём max(cached, in-store) по id.
useEffect(() => {
if (!contactAddress) return;
loadMessages(contactAddress).then(cached => {

View File

@@ -27,7 +27,6 @@ import {
fetchTimeline, fetchForYou, fetchTrending, fetchStats, bumpView,
type FeedPostItem,
} from '@/lib/feed';
import { getDevSeedFeed } from '@/lib/devSeedFeed';
type TabKey = 'following' | 'foryou' | 'trending';
@@ -78,12 +77,6 @@ export default function FeedScreen() {
}
if (seq !== requestRef.current) return; // stale response
// Dev-only fallback: if the node has no real posts yet, surface
// synthetic ones so we can scroll + tap. Stripped from production.
if (items.length === 0) {
items = getDevSeedFeed();
}
setPosts(items);
// If the server returned fewer than PAGE_SIZE, we already have
// everything — disable further paginated fetches.
@@ -105,11 +98,11 @@ export default function FeedScreen() {
} catch (e: any) {
if (seq !== requestRef.current) return;
const msg = String(e?.message ?? e);
// Network / 404 is benign — node just unreachable or empty. In __DEV__
// fall back to synthetic seed posts so the scroll / tap UI stays
// testable; in production this path shows the empty state.
// Network / 404 is benign — node just unreachable or empty. Show
// the empty-state; the catch block above already cleared error
// on benign messages. Production treats this identically.
if (/Network request failed|→\s*404/.test(msg)) {
setPosts(getDevSeedFeed());
setPosts([]);
setExhausted(true);
} else {
setError(msg);

View File

@@ -1,444 +0,0 @@
/**
* Dev seed — заполняет store фейковыми контактами и сообщениями для UI-теста.
*
* Запускается один раз при монтировании layout'а если store пустой
* (useDevSeed). Реальные контакты через WS/HTTP приходят позже —
* `upsertContact` перезаписывает mock'и по address'у.
*
* Цели seed'а:
* 1. Показать все три типа чатов (direct / group / channel) с разным
* поведением sender-meta.
* 2. Наполнить список чатов до скролла (15+ контактов).
* 3. В каждом чате — ≥15 сообщений для скролла в chat view.
* 4. Продемонстрировать "staircase" (run'ы одного отправителя
* внутри 1h-окна) и переключения между отправителями.
*/
import { useEffect } from 'react';
import { useStore } from './store';
import type { Contact, Message } from './types';
// ─── Детерминированные «pubkey» (64 hex символа) ───────────────────
function fakeHex(seed: number): string {
let h = '';
let x = seed;
for (let i = 0; i < 32; i++) {
x = (x * 1103515245 + 12345) & 0xffffffff;
h += (x & 0xff).toString(16).padStart(2, '0');
}
return h;
}
const now = () => Math.floor(Date.now() / 1000);
const MINE = fakeHex(9999);
// ─── Контакты ──────────────────────────────────────────────────────
// 16 штук: 5 DM + 6 групп + 5 каналов. Поле `addedAt` задаёт порядок в
// списке когда нет messages — ordering-fallback.
const mockContacts: Contact[] = [
// ── DM ──────────────────────────────────────────────────────────
{ address: fakeHex(1001), x25519Pub: fakeHex(2001),
username: 'jordan', addedAt: Date.now() - 60 * 60 * 1_000, kind: 'direct' },
{ address: fakeHex(1002), x25519Pub: fakeHex(2002),
alias: 'Myles Wagner', addedAt: Date.now() - 2 * 60 * 60 * 1_000, kind: 'direct' },
{ address: fakeHex(1010), x25519Pub: fakeHex(2010),
username: 'sarah_k', addedAt: Date.now() - 3 * 60 * 60 * 1_000, kind: 'direct',
unread: 2 },
{ address: fakeHex(1011), x25519Pub: fakeHex(2011),
alias: 'Mom', addedAt: Date.now() - 5 * 60 * 60 * 1_000, kind: 'direct' },
{ address: fakeHex(1012), x25519Pub: fakeHex(2012),
username: 'alex_dev', addedAt: Date.now() - 6 * 60 * 60 * 1_000, kind: 'direct' },
// ── Groups ─────────────────────────────────────────────────────
{ address: fakeHex(1003), x25519Pub: fakeHex(2003),
alias: 'Tahoe weekend 🌲', addedAt: Date.now() - 4 * 60 * 60 * 1_000, kind: 'group' },
{ address: fakeHex(1004), x25519Pub: fakeHex(2004),
alias: 'Knicks tickets', addedAt: Date.now() - 5 * 60 * 60 * 1_000, kind: 'group',
unread: 3 },
{ address: fakeHex(1020), x25519Pub: fakeHex(2020),
alias: 'Family', addedAt: Date.now() - 8 * 60 * 60 * 1_000, kind: 'group' },
{ address: fakeHex(1021), x25519Pub: fakeHex(2021),
alias: 'Work eng', addedAt: Date.now() - 12 * 60 * 60 * 1_000, kind: 'group',
unread: 7 },
{ address: fakeHex(1022), x25519Pub: fakeHex(2022),
alias: 'Book club', addedAt: Date.now() - 24 * 60 * 60 * 1_000, kind: 'group' },
{ address: fakeHex(1023), x25519Pub: fakeHex(2023),
alias: 'Tuesday D&D 🎲', addedAt: Date.now() - 30 * 60 * 60 * 1_000, kind: 'group' },
// (Channel seeds removed in v2.0.0 — channels replaced by the social feed.)
];
// ─── Генератор сообщений ───────────────────────────────────────────
// Альт-отправители для group-чатов — нужны только как идентификатор `from`.
const P_TYRA = fakeHex(3001);
const P_MYLES = fakeHex(3002);
const P_NATE = fakeHex(3003);
const P_TYLER = fakeHex(3004);
const P_MOM = fakeHex(3005);
const P_DAD = fakeHex(3006);
const P_SIS = fakeHex(3007);
const P_LEAD = fakeHex(3008);
const P_PM = fakeHex(3009);
const P_QA = fakeHex(3010);
const P_DESIGN= fakeHex(3011);
const P_ANNA = fakeHex(3012);
const P_DM_PEER = fakeHex(3013);
type Msg = Omit<Message, 'id'>;
function list(prefix: string, list: Msg[]): Message[] {
return list.map((m, i) => ({ ...m, id: `${prefix}_${i}` }));
}
function mockMessagesFor(contact: Contact): Message[] {
const peer = contact.x25519Pub;
// ── DM: @jordan ────────────────────────────────────────────────
if (contact.username === 'jordan') {
// Важно: id'ы сообщений используются в replyTo.id, поэтому
// указываем их явно где нужно сшить thread.
const msgs: Message[] = list('jordan', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 22, text: 'Hey, have a sec later today?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 21, read: true, text: 'yep around 4pm' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 20, text: 'cool, coffee at the corner spot?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 19, read: true, text: 'works' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 4, text: 'just parked 🚗' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 4, text: 'see you in 5' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 3, read: true, text: "that was a great catchup" },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 3, text: "totally — thanks for the book rec" },
{ from: peer, mine: false, timestamp: now() - 60 * 40, text: 'Hey Jordan - Got tickets to the Knicks game tomorrow, let me know if you want to come!' },
{ from: peer, mine: false, timestamp: now() - 60 * 39, text: "we've got floor seats 🔥" },
{ from: peer, mine: false, timestamp: now() - 60 * 38, text: "starts at 7, pregame at the bar across the street" },
{ from: MINE, mine: true, timestamp: now() - 60 * 14, read: true, edited: true, text: 'Ah sadly I already have plans' },
{ from: MINE, mine: true, timestamp: now() - 60 * 13, read: true, text: 'maybe next time?' },
{ from: peer, mine: false, timestamp: now() - 60 * 5, text: "no worries — enjoy whatever you're up to" },
{ from: peer, mine: false, timestamp: now() - 60 * 2, text: "wish you could make it tho 🏀" },
]);
// Пришьём reply: MINE-сообщение "Ah sadly…" отвечает на "Hey Jordan - Got tickets…"
const target = msgs.find(m => m.text.startsWith('Hey Jordan - Got tickets'));
const mine = msgs.find(m => m.text === 'Ah sadly I already have plans');
if (target && mine) {
mine.replyTo = {
id: target.id,
author: '@jordan',
text: target.text,
};
}
return msgs;
}
// ── DM: Myles Wagner ───────────────────────────────────────────
if (contact.alias === 'Myles Wagner') {
return list('myles', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 30, text: 'saw the draft, left a bunch of comments' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 29, read: true, text: 'thx, going through them now' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 29, text: 'no rush — tomorrow is fine' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 5, text: 'lunch today?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 4, read: true, text: "can't, stuck in reviews" },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 4, read: true, text: 'tomorrow?' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 4, text: '✅ tomorrow' },
{
from: peer, mine: false, timestamp: now() - 60 * 60 * 2, text: '',
attachment: {
kind: 'voice',
uri: 'voice-demo://myles-1',
duration: 17,
},
},
{ from: peer, mine: false, timestamp: now() - 60 * 25, text: 'the dchain repo finally built for me' },
{ from: peer, mine: false, timestamp: now() - 60 * 25, text: 'docker weirdness was the issue' },
{ from: MINE, mine: true, timestamp: now() - 60 * 21, read: true, text: "nice, told you the WSL path would do it" },
{ from: peer, mine: false, timestamp: now() - 60 * 20, text: 'So good!' },
]);
}
// ── DM: @sarah_k (с unread=2) ──────────────────────────────────
if (contact.username === 'sarah_k') {
return list('sarah', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 30, text: "hey! been a while" },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 28, read: true, text: 'yeah, finally surfaced after the launch crunch' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 27, text: 'how did it go?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 27, read: true, text: "pretty well actually 🙏" },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 2, text: 'btw drinks on friday?' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 2, text: 'that new wine bar' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 2, text: 'around 7 if you can make it' },
]);
}
// ── DM: Mom ────────────────────────────────────────────────────
if (contact.alias === 'Mom') {
return list('mom', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 48, text: 'Did you see the photos from the trip?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 47, read: true, text: 'not yet, send them again?' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 47, text: 'ok' },
{
from: peer, mine: false, timestamp: now() - 60 * 60 * 46, text: '',
attachment: {
kind: 'image',
uri: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800',
width: 800, height: 533, mime: 'image/jpeg',
},
},
{
from: peer, mine: false, timestamp: now() - 60 * 60 * 46, text: '',
attachment: {
kind: 'image',
uri: 'https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800',
width: 800, height: 533, mime: 'image/jpeg',
},
},
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 30, read: true, text: 'wow, grandma looks great' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 30, text: 'she asked about you!' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 7, text: 'call later?' },
]);
}
// ── DM: @alex_dev ──────────────────────────────────────────────
if (contact.username === 'alex_dev') {
return list('alex', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 12, text: 'did you try the new WASM build?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 11, read: true, text: 'yeah, loader error on start' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 11, text: 'path encoding issue again?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 10, read: true, text: 'probably, checking now' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 8, read: true, text: 'yep, was the trailing slash' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 8, text: 'classic 😅' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 7, text: 'PR for that incoming tomorrow' },
]);
}
// ── Group: Tahoe weekend 🌲 ────────────────────────────────────
if (contact.alias === 'Tahoe weekend 🌲') {
const msgs: Message[] = list('tahoe', [
{ from: P_TYRA, mine: false, timestamp: now() - 60 * 60 * 50, text: "who's in for Tahoe this weekend?" },
{ from: P_MYLES, mine: false, timestamp: now() - 60 * 60 * 49, text: "me!" },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 48, read: true, text: "count me in" },
{ from: P_TYRA, mine: false, timestamp: now() - 60 * 60 * 48, text: "woohoo 🎉" },
{ from: P_ANNA, mine: false, timestamp: now() - 60 * 60 * 47, text: "planning friday night → sunday evening yeah?" },
{ from: P_TYRA, mine: false, timestamp: now() - 60 * 60 * 46, text: "yep, maybe leave friday after lunch" },
{ from: P_MYLES, mine: false, timestamp: now() - 60 * 60 * 30, text: "I made this itinerary with Grok, what do you think?" },
{ from: P_MYLES, mine: false, timestamp: now() - 60 * 60 * 30, text: "Day 1: Eagle Falls hike" },
{ from: P_MYLES, mine: false, timestamp: now() - 60 * 60 * 30, text: "Day 2: Emerald bay kayak" },
{ from: P_MYLES, mine: false, timestamp: now() - 60 * 60 * 30, text: "Day 3: lazy breakfast then drive back" },
{
from: P_MYLES, mine: false, timestamp: now() - 60 * 60 * 30, text: '',
attachment: {
kind: 'file',
uri: 'https://example.com/Lake_Tahoe_Itinerary.pdf',
name: 'Lake_Tahoe_Itinerary.pdf',
size: 97_280, // ~95 KB
mime: 'application/pdf',
},
},
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 24, read: true, edited: true, text: "Love it — Eagle falls looks insane" },
{ from: P_ANNA, mine: false, timestamp: now() - 60 * 60 * 24, text: "Eagle falls was stunning last year!" },
{ from: P_TYRA, mine: false, timestamp: now() - 60 * 31, text: "who's excited for Tahoe this weekend?" },
{ from: P_TYRA, mine: false, timestamp: now() - 60 * 30, text: "I've been checking the forecast — sun all weekend 🌞" },
{ from: P_MYLES, mine: false, timestamp: now() - 60 * 22, text: "I made this itinerary with Grok, what do you think?" },
{ from: P_MYLES, mine: false, timestamp: now() - 60 * 21, text: "Day 1 we can hit Eagle Falls" },
{ from: MINE, mine: true, timestamp: now() - 60 * 14, read: true, edited: true, text: "Love it — Eagle falls looks insane" },
{
from: P_TYRA, mine: false, timestamp: now() - 60 * 3, text: 'pic from my last trip 😍',
attachment: {
kind: 'image',
uri: 'https://images.unsplash.com/photo-1505245208761-ba872912fac0?w=800',
width: 800,
height: 1000,
mime: 'image/jpeg',
},
},
]);
// Thread: mine "Love it — Eagle falls looks insane" — ответ на
// Myles'овский itinerary-PDF. Берём ПЕРВЫЙ match "Day 1 we can hit
// Eagle Falls" и пришиваем его к первому mine-bubble'у.
const target = msgs.find(m => m.text === 'Day 1 we can hit Eagle Falls');
const reply = msgs.find(m => m.text === 'Love it — Eagle falls looks insane' && m.mine);
if (target && reply) {
reply.replyTo = {
id: target.id,
author: 'Myles Wagner',
text: target.text,
};
}
return msgs;
}
// ── Group: Knicks tickets ──────────────────────────────────────
if (contact.alias === 'Knicks tickets') {
return list('knicks', [
{ from: P_NATE, mine: false, timestamp: now() - 60 * 60 * 20, text: "quick group update — got 5 tickets for thursday" },
{ from: P_TYLER, mine: false, timestamp: now() - 60 * 60 * 19, text: 'wow nice' },
{ from: P_TYLER, mine: false, timestamp: now() - 60 * 60 * 19, text: 'where are we seated?' },
{ from: P_NATE, mine: false, timestamp: now() - 60 * 60 * 19, text: 'section 102, row 12' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 18, read: true, text: 'thats a great spot' },
{ from: P_ANNA, mine: false, timestamp: now() - 60 * 60 * 18, text: "can someone venmo nate 🙏" },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 17, read: true, text: 'sending now' },
{ from: P_NATE, mine: false, timestamp: now() - 60 * 32, text: "Ok who's in for tomorrow's game?" },
{ from: P_NATE, mine: false, timestamp: now() - 60 * 31, text: 'Got 2 extra tickets, first-come-first-served' },
{ from: P_TYLER, mine: false, timestamp: now() - 60 * 27, text: "I'm in!" },
{ from: P_TYLER, mine: false, timestamp: now() - 60 * 26, text: 'What time does it start?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 20, read: true, text: "Let's meet at the bar around 6?" },
{ from: P_NATE, mine: false, timestamp: now() - 60 * 15, text: 'Sounds good' },
]);
}
// ── Group: Family ──────────────────────────────────────────────
if (contact.alias === 'Family') {
return list('family', [
{ from: P_MOM, mine: false, timestamp: now() - 60 * 60 * 36, text: 'remember grandma birthday on sunday' },
{ from: P_DAD, mine: false, timestamp: now() - 60 * 60 * 36, text: 'noted 🎂' },
{ from: P_SIS, mine: false, timestamp: now() - 60 * 60 * 35, text: 'who is bringing the cake?' },
{ from: P_MOM, mine: false, timestamp: now() - 60 * 60 * 35, text: "I'll get it from the bakery" },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 34, read: true, text: 'I can pick up flowers' },
{ from: P_SIS, mine: false, timestamp: now() - 60 * 60 * 34, text: 'perfect' },
{ from: P_DAD, mine: false, timestamp: now() - 60 * 60 * 8, text: 'forecast is rain sunday — backup plan?' },
{ from: P_MOM, mine: false, timestamp: now() - 60 * 60 * 8, text: "we'll move indoors, the living room works" },
{ from: P_SIS, mine: false, timestamp: now() - 60 * 60 * 7, text: 'works!' },
]);
}
// ── Group: Work eng (unread=7) ─────────────────────────────────
if (contact.alias === 'Work eng') {
return list('work', [
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 60 * 16, text: 'standup at 10 moved to 11 today btw' },
{ from: P_PM, mine: false, timestamp: now() - 60 * 60 * 16, text: 'thanks!' },
{ from: P_QA, mine: false, timestamp: now() - 60 * 60 * 15, text: "the staging deploy broke again 🙃" },
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 60 * 15, text: "ugh, looking" },
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 60 * 14, text: 'fixed — migration was stuck' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 13, read: true, text: 'Worked for me now 👍' },
{ from: P_PM, mine: false, timestamp: now() - 60 * 60 * 5, text: 'reminder: demo tomorrow, slides by eod' },
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 60 * 4, text: 'Ill handle the technical half' },
{ from: P_DESIGN,mine: false,timestamp: now() - 60 * 60 * 4, text: 'just posted the v2 mocks in figma' },
{ from: P_PM, mine: false, timestamp: now() - 60 * 60 * 3, text: 'chatting with sales — 3 new trials this week' },
{ from: P_QA, mine: false, timestamp: now() - 60 * 60 * 2, text: 'flaky test on CI — investigating' },
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 30, text: 'okay seems like CI is green now' },
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 28, text: 'retry passed' },
{ from: P_PM, mine: false, timestamp: now() - 60 * 20, text: "we're good for release" },
]);
}
// ── Group: Book club ───────────────────────────────────────────
if (contact.alias === 'Book club') {
return list('book', [
{ from: P_ANNA, mine: false, timestamp: now() - 60 * 60 * 96, text: 'next month: "Project Hail Mary"?' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 95, read: true, text: '👍' },
{ from: P_SIS, mine: false, timestamp: now() - 60 * 60 * 94, text: 'yes please' },
{ from: P_TYRA, mine: false, timestamp: now() - 60 * 60 * 48, text: 'halfway through — so good' },
{ from: P_ANNA, mine: false, timestamp: now() - 60 * 60 * 48, text: 'love the linguistics angle' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 30, read: true, text: "rocky is my favourite character in years" },
{ from: P_SIS, mine: false, timestamp: now() - 60 * 60 * 28, text: 'agreed' },
{ from: P_ANNA, mine: false, timestamp: now() - 60 * 60 * 24, text: "let's meet sunday 4pm?" },
]);
}
// ── Group: Tuesday D&D 🎲 ──────────────────────────────────────
if (contact.alias === 'Tuesday D&D 🎲') {
return list('dnd', [
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 60 * 72, text: 'Session 14 recap up on the wiki' },
{ from: P_ANNA, mine: false, timestamp: now() - 60 * 60 * 72, text: '🙏' },
{ from: P_TYRA, mine: false, timestamp: now() - 60 * 60 * 50, text: 'can we start 30min late next tuesday? commute issue' },
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 60 * 50, text: 'sure' },
{ from: MINE, mine: true, timestamp: now() - 60 * 60 * 49, read: true, text: 'works for me' },
{ from: P_LEAD, mine: false, timestamp: now() - 60 * 60 * 32, text: 'we pick up where we left — in the dragons cave' },
{ from: P_ANNA, mine: false, timestamp: now() - 60 * 60 * 32, text: 'excited 🐉' },
]);
}
// ── Channel: dchain_updates ────────────────────────────────────
if (contact.username === 'dchain_updates') {
return list('dchain_updates', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 96, text: '🔨 v0.0.1-alpha tagged on Gitea' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 72, text: 'PBFT equivocation-detection тесты зелёные' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 60, text: 'New: /api/peers теперь включает peer-version info' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 48, text: '📘 Docs overhaul merged: docs/README.md' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 36, text: 'Schema migration scaffold landed (no-op для текущей версии)' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 20, text: '🚀 v0.0.1 released' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 20 + 10, text: 'Includes: auto-update from Gitea, peer-version gossip, schema migrations' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 20 + 20, text: 'Check /api/well-known-version for the full feature list' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 12, text: 'Thanks to all testers — feedback drives the roadmap 🙏' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 3, text: 'v0.0.2 roadmap published: https://git.vsecoder.vodka/vsecoder/dchain' },
{ from: peer, mine: false, timestamp: now() - 60 * 30, text: 'quick heads-up: nightly builds switching to new docker-slim base' },
]);
}
// ── Channel: Relay broadcasts ──────────────────────────────────
if (contact.alias === '⚡ Relay broadcasts') {
return list('relay_bc', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 48, text: 'Relay fleet snapshot: 12 active, 3 inactive' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 40, text: 'Relay #3 came online in US-east' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 30, text: 'Validator set updated: 3→4' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 20, text: 'PBFT view-change детектирован и отработан на блоке 184120' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 15, text: 'Mailbox eviction ran — 42 stale envelopes' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 5, text: 'Relay #8 slashed for equivocation — evidence at block 184202' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 2, text: 'Relay #12 came online in EU-west, registering now…' },
]);
}
// ── Channel: Tech news ────────────────────────────────────────
if (contact.alias === '📰 Tech news') {
return list('tech_news', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 120, text: 'Rust 1.78 released — new lints for raw pointers' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 100, text: 'Go 1.23 ships range-over-func officially' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 80, text: 'Expo SDK 54 drops — new-architecture default' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 60, text: 'CVE-2026-1337 patched in libsodium (update your keys)' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 40, text: 'Matrix protocol adds post-quantum handshakes' },
{
from: peer, mine: false, timestamp: now() - 60 * 60 * 30, text: 'Data-center aerial view — new hyperscaler in Iceland',
attachment: {
kind: 'image',
uri: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800',
width: 800, height: 533, mime: 'image/jpeg',
},
},
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 20, text: 'IETF draft: "DNS-over-blockchain"' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 6, text: 'GitHub tightens 2FA defaults for orgs' },
]);
}
// ── Channel: Design inspo (unread=12) ──────────────────────────
if (contact.alias === '🎨 Design inspo') {
return list('design_inspo', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 160, text: 'Weekly pick: Linear UI v3 breakdown' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 140, text: 'Figma file of the week: "Command bar patterns"' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 120, text: 'Motion study: Stripe checkout shake-error animation' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 100, text: "10 great empty-state illustrations (blogpost)" },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 80, text: 'Tool: Hatch — colour-palette extractor from photos' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 60, text: '🔮 Trend watch: glassmorphism is back (again)' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 40, text: 'Twitter thread: why rounded buttons are the default' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 20, text: 'Framer templates — black friday sale' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 3, text: 'New typeface: "Grotesk Pro" — free for personal use' },
]);
}
// ── Channel: NBA scores ────────────────────────────────────────
if (contact.alias === '🏀 NBA scores') {
return list('nba', [
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 160, text: 'Lakers 112 — Warriors 108 (OT)' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 130, text: 'Celtics 128 — Heat 115' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 100, text: 'Nuggets 119 — Thunder 102' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 70, text: "Knicks 101 — Bulls 98" },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 48, text: 'Mavericks 130 — Kings 127' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 24, text: 'Bucks 114 — Sixers 110' },
{ from: peer, mine: false, timestamp: now() - 60 * 60 * 4, text: 'Live: Lakers leading 78-72 at half' },
]);
}
return [];
}
// ─── Hook ──────────────────────────────────────────────────────────
export function useDevSeed() {
const contacts = useStore(s => s.contacts);
const setContacts = useStore(s => s.setContacts);
const setMessages = useStore(s => s.setMessages);
useEffect(() => {
if (contacts.length > 0) return;
setContacts(mockContacts);
for (const c of mockContacts) {
const msgs = mockMessagesFor(c);
if (msgs.length > 0) setMessages(c.address, msgs);
}
}, [contacts.length, setContacts, setMessages]);
}

View File

@@ -1,180 +0,0 @@
/**
* 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'],
},
];
/**
* Returns the dev-seed post list. Only returns actual items in dev
* builds; release bundles return an empty array so fake posts never
* appear in production.
*
* We use the runtime `globalThis.__DEV__` lookup rather than the typed
* `__DEV__` global because some builds can have the TS typing
* out-of-sync with the actual injected value.
*/
export function getDevSeedFeed(): FeedPostItem[] {
const g = globalThis as unknown as { __DEV__?: boolean };
if (g.__DEV__ !== true) return [];
return SEED_POSTS;
}