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>
445 lines
29 KiB
TypeScript
445 lines
29 KiB
TypeScript
/**
|
||
* 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]);
|
||
}
|