feat(mesh): Telegram primitives pass + attachment transport router

Bundles the Phase 2b/3/4/5 work that accumulated across prior sessions
and the new attachment chunking router from this session. Everything
ships in one shot so the full mesh surface stays coherent on-wire.

Telegram primitives (variants 13–18, 20–22):
- Reply / Reaction / ReadReceipt / Forward / Edit / Delete
- Presence heartbeat + last-seen tracking
- ChannelInvite + ContactCard payload types
- MessageKey (sender_pubkey, sender_seq) as cross-transport identity
- Action menu, reply banner, edit banner, tombstones, (edited) marker
- Debounced auto-read-receipts on scroll + message arrival

Activated prototypes (Phase 4):
- PsbtHash send RPC
- Contacts CRUD (in-memory alias/notes/pinned/blocked)
- Outbox 📤 badge, rotate-prekeys button
- Chunked send fallback (MCIIXXTT framing) as auto-failover inside
  send_typed_wire when a typed wire exceeds the LoRa per-frame budget

Unified inbox (Phase 1):
- conversations.list + conversations.messages RPCs (UI collapse deferred)

Attachment transport router (new this session):
- ContentInline variant 23 + ContentInlinePayload carrying file bytes
  directly in the envelope for small files with no Tor path
- mesh.send-content-inline RPC — mirrors to local BlobStore, rides
  send_typed_wire which auto-chunks over MCIIXXTT framing (~2.3 KB cap)
- mesh.transport-advice RPC as single source of truth for tier
  decisions: auto-mesh / choose / tor-only / impossible
- Receive arm writes inline bytes to local BlobStore so the existing
  content_ref card renderer handles both transports uniformly
- MeshState.blob_store field + order-independent propagation from
  RpcHandler::set_blob_store / set_mesh_service
- Frontend handleAttachFile calls advice first, branches into silent
  auto-send, transport-chooser modal, Tor-only path, or red error
- Transport modal with 📡 mesh / 🧅 Tor options + ETA + disabled
  state when peer has no Tor reachability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-14 20:40:19 -04:00
parent 5616bb74e6
commit 6760d11a57
16 changed files with 789 additions and 153 deletions

View File

@@ -407,6 +407,49 @@ export const useMeshStore = defineStore('mesh', () => {
}
}
async function transportAdvice(contactId: number, size: number) {
return rpcClient.call<{
tier: 'auto-mesh' | 'choose' | 'tor-only' | 'impossible'
est_seconds: number
has_tor: boolean
reason: string
size: number
mesh_auto_max: number
mesh_hard_max: number
}>({
method: 'mesh.transport-advice',
params: { contact_id: contactId, size },
})
}
async function sendContentInline(
contactId: number,
mime: string,
bytes: Uint8Array,
filename?: string,
caption?: string,
) {
try {
sending.value = true
error.value = null
// Base64-encode bytes for JSON transport.
let binary = ''
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]!)
const bytes_b64 = btoa(binary)
const res = await rpcClient.call<{ sent: boolean; message_id: number; cid: string; size: number }>({
method: 'mesh.send-content-inline',
params: { contact_id: contactId, mime, filename, caption, bytes_b64 },
})
if (res.sent) await fetchMessages()
return res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to send inline content'
throw err
} finally {
sending.value = false
}
}
async function sendReply(contactId: number, targetPubkey: string, targetSeq: number, text: string) {
sending.value = true
try {
@@ -633,6 +676,8 @@ export const useMeshStore = defineStore('mesh', () => {
sendCoordinate,
sendAlert,
sendContent,
sendContentInline,
transportAdvice,
fetchContent,
sendReply,
sendReaction,