diff --git a/client-app/app/(app)/_layout.tsx b/client-app/app/(app)/_layout.tsx index 5260729..037004e 100644 --- a/client-app/app/(app)/_layout.tsx +++ b/client-app/app/(app)/_layout.tsx @@ -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 diff --git a/client-app/app/(app)/chats/[id].tsx b/client-app/app/(app)/chats/[id].tsx index 28eb0c9..c8fec3b 100644 --- a/client-app/app/(app)/chats/[id].tsx +++ b/client-app/app/(app)/chats/[id].tsx @@ -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 => { diff --git a/client-app/app/(app)/feed/index.tsx b/client-app/app/(app)/feed/index.tsx index 55e4c0a..4102028 100644 --- a/client-app/app/(app)/feed/index.tsx +++ b/client-app/app/(app)/feed/index.tsx @@ -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); diff --git a/client-app/lib/devSeed.ts b/client-app/lib/devSeed.ts deleted file mode 100644 index b8421b1..0000000 --- a/client-app/lib/devSeed.ts +++ /dev/null @@ -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; - -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]); -} diff --git a/client-app/lib/devSeedFeed.ts b/client-app/lib/devSeedFeed.ts deleted file mode 100644 index b46a875..0000000 --- a/client-app/lib/devSeedFeed.ts +++ /dev/null @@ -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; -}