Feed list padding
FlatList had no inner padding so the first post bumped against the
tab strip and the last post against the NavBar. Added paddingTop: 8
/ paddingBottom: 24 on contentContainerStyle in both /feed and
/feed/tag/[tag] — first card now has a clear top gap, last card
doesn't get hidden behind the FAB or NavBar.
Share-to-chat flow
Replaces the placeholder share button (which showed an Alert with
the post URL) with a real "forward to chats" flow modeled on VK's
shared-wall-post embed.
New modules
lib/forwardPost.ts — encodePostRef / tryParsePostRef +
forwardPostToContacts(). Serialises a
feed post into a tiny JSON payload that
rides the same encrypted envelope as any
chat message; decode side distinguishes
"post_ref" payloads from regular text by
trying JSON.parse on decrypted text.
Mirrors the sent message into the sender's
local history so they see "you shared
this" in the chat they forwarded to.
components/feed/ShareSheet.tsx
— bottom-sheet picker. Multi-select
contacts via tick-box, search by
username / alias / address prefix.
"Send (N)" dispatches N parallel
encrypted envelopes. Contacts with no
X25519 key are filtered out (can't
encrypt for them).
components/chat/PostRefCard.tsx
— compact embedded-post card for chat
bubbles. Ribbon "ПОСТ" label +
author + 3-line excerpt + "с фото"
indicator. Tap → /(app)/feed/{id} full
post detail. Palette switches between
blue-bubble-friendly and peer-bubble-
friendly depending on bubble side.
Message pipeline
lib/types.ts — Message.postRef optional field added.
text stays "" when the message is a
post-ref (nothing to render as plain text).
hooks/useMessages.ts + hooks/useGlobalInbox.ts
— post decryption of every inbound envelope
runs through tryParsePostRef; matching
messages get the postRef attached instead
of the raw JSON in .text.
components/chat/MessageBubble.tsx
— renders PostRefCard inside the bubble when
msg.postRef is set. Other bubble features
(reply quote, attachment preview, text)
still work around it.
PostCard
- share icon now opens <ShareSheet>; the full-URL placeholder is
gone. ShareSheet is embedded at the PostCard level so each card
owns its own sheet state (avoids modal-stacking issues).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
7.8 KiB
TypeScript
166 lines
7.8 KiB
TypeScript
// ─── Key material ────────────────────────────────────────────────────────────
|
||
|
||
export interface KeyFile {
|
||
pub_key: string; // hex Ed25519 public key (32 bytes)
|
||
priv_key: string; // hex Ed25519 private key (64 bytes)
|
||
x25519_pub: string; // hex X25519 public key (32 bytes)
|
||
x25519_priv: string; // hex X25519 private key (32 bytes)
|
||
}
|
||
|
||
// ─── Contact ─────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Тип беседы в v2.0.0 — только direct (1:1 E2E чат). Каналы убраны в
|
||
* пользу публичной ленты (см. lib/feed.ts). Поле `kind` осталось ради
|
||
* обратной совместимости со старыми записями в AsyncStorage; новые
|
||
* контакты не пишут его.
|
||
*/
|
||
export type ContactKind = 'direct' | 'group';
|
||
|
||
export interface Contact {
|
||
address: string; // Ed25519 pubkey hex — blockchain address
|
||
x25519Pub: string; // X25519 pubkey hex — encryption key
|
||
username?: string; // @name from registry contract
|
||
alias?: string; // local nickname
|
||
addedAt: number; // unix ms
|
||
/** Legacy field (kept for backward compat with existing AsyncStorage). */
|
||
kind?: ContactKind;
|
||
/** Количество непрочитанных — опционально, проставляется WS read-receipt'ами. */
|
||
unread?: number;
|
||
}
|
||
|
||
// ─── Messages ─────────────────────────────────────────────────────────────────
|
||
|
||
export interface Envelope {
|
||
/** sha256(nonce||ciphertext)[:16] hex — stable server-assigned id. */
|
||
id: string;
|
||
sender_pub: string; // X25519 hex
|
||
recipient_pub: string; // X25519 hex
|
||
nonce: string; // hex 24 bytes
|
||
ciphertext: string; // hex NaCl box
|
||
timestamp: number; // unix seconds (server's sent_at, normalised client-side)
|
||
}
|
||
|
||
/**
|
||
* Вложение к сообщению. MVP — хранится как URI на локальной файловой
|
||
* системе клиента (expo-image-picker / expo-document-picker / expo-av
|
||
* возвращают именно такие URI). Wire-формат для передачи attachment'ов
|
||
* через relay-envelope ещё не финализирован — пока этот тип для UI'а и
|
||
* локального отображения.
|
||
*
|
||
* Формат по kind:
|
||
* image — width/height опциональны (image-picker их отдаёт)
|
||
* video — same + duration в секундах
|
||
* voice — duration в секундах, нет дизайна превью кроме waveform-stub
|
||
* file — name + size в байтах, тип через mime
|
||
*/
|
||
export type AttachmentKind = 'image' | 'video' | 'voice' | 'file';
|
||
|
||
export interface Attachment {
|
||
kind: AttachmentKind;
|
||
uri: string; // локальный file:// URI или https:// (incoming decoded)
|
||
mime?: string; // 'image/jpeg', 'application/pdf', …
|
||
name?: string; // имя файла (для file)
|
||
size?: number; // байты (для file)
|
||
width?: number; // image/video
|
||
height?: number; // image/video
|
||
duration?: number; // seconds (video/voice)
|
||
/** Для kind='video' — рендерить как круглое видео-сообщение (Telegram-style). */
|
||
circle?: boolean;
|
||
}
|
||
|
||
export interface Message {
|
||
id: string;
|
||
from: string; // X25519 pubkey of sender
|
||
text: string;
|
||
timestamp: number;
|
||
mine: boolean;
|
||
/** true если сообщение было отредактировано. Показываем "Edited" в углу. */
|
||
edited?: boolean;
|
||
/**
|
||
* Для mine=true — true если получатель его прочитал.
|
||
* UI: пустая галочка = отправлено, filled = прочитано.
|
||
* Для mine=false не используется.
|
||
*/
|
||
read?: boolean;
|
||
/** Одно вложение. Multi-attach пока не поддерживается — будет массивом. */
|
||
attachment?: Attachment;
|
||
/**
|
||
* Если сообщение — ответ на другое, здесь лежит ссылка + short preview
|
||
* того оригинала. id используется для scroll-to + highlight; text/author
|
||
* — для рендера "quoted"-блока внутри текущего bubble'а без запроса
|
||
* исходного сообщения (копия замороженная в момент ответа).
|
||
*/
|
||
replyTo?: {
|
||
id: string;
|
||
text: string;
|
||
author: string; // @username / alias / "you"
|
||
};
|
||
/**
|
||
* Ссылка на пост из ленты. Если присутствует — сообщение рендерится как
|
||
* карточка-превью поста (аватар автора, хэндл, текст-excerpt, картинка
|
||
* если есть). Тап на карточку → открывается полный пост. Сценарий — юзер
|
||
* нажал Share в ленте и отправил пост в этот чат/ЛС.
|
||
*
|
||
* Содержимое (автор, excerpt) дублируется тут, чтобы карточку можно было
|
||
* рендерить оффлайн / когда у хостящей релей-ноды пропал пост — чат
|
||
* остаётся читаемым независимо от жизни ленты.
|
||
*/
|
||
postRef?: {
|
||
postID: string;
|
||
author: string; // Ed25519 hex — для чипа имени в карточке
|
||
excerpt: string; // первые 120 символов тела поста
|
||
hasImage?: boolean;
|
||
};
|
||
}
|
||
|
||
// ─── Chat ────────────────────────────────────────────────────────────────────
|
||
|
||
export interface Chat {
|
||
contactAddress: string; // Ed25519 pubkey hex
|
||
contactX25519: string; // X25519 pubkey hex
|
||
username?: string;
|
||
alias?: string;
|
||
lastMessage?: string;
|
||
lastTime?: number;
|
||
unread: number;
|
||
}
|
||
|
||
// ─── Contact request ─────────────────────────────────────────────────────────
|
||
|
||
export interface ContactRequest {
|
||
from: string; // Ed25519 pubkey hex
|
||
x25519Pub: string; // X25519 pubkey hex; empty until fetched from identity
|
||
username?: string;
|
||
intro: string; // plaintext intro (stored on-chain)
|
||
timestamp: number;
|
||
txHash: string;
|
||
}
|
||
|
||
// ─── Transaction ─────────────────────────────────────────────────────────────
|
||
|
||
export interface TxRecord {
|
||
hash: string;
|
||
type: string;
|
||
from: string;
|
||
to?: string;
|
||
amount?: number;
|
||
fee: number;
|
||
timestamp: number;
|
||
status: 'confirmed' | 'pending';
|
||
}
|
||
|
||
// ─── Node info ───────────────────────────────────────────────────────────────
|
||
|
||
export interface NetStats {
|
||
total_blocks: number;
|
||
total_txs: number;
|
||
peer_count: number;
|
||
chain_id: string;
|
||
}
|
||
|
||
export interface NodeSettings {
|
||
nodeUrl: string;
|
||
contractId: string; // username_registry contract
|
||
}
|