feat(mesh-ui): attach button + ContentRef card in chat

Compose row gains a 📎 attach button that uploads the file via /api/blob
and calls mesh.send-content for the selected peer. Received content_ref
bubbles render as a caption+filename card with either an inline image
preview or a Download button that calls mesh.fetch-content and swaps in
the returned local_url.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-13 11:10:59 -04:00
co-authored by Claude Opus 4.6
parent dce5084451
commit 019144903c
2 changed files with 154 additions and 1 deletions
+35
View File
@@ -47,6 +47,7 @@ export type MeshMessageTypeLabel =
| 'tx_confirmation'
| 'lightning_relay'
| 'lightning_relay_response'
| 'content_ref'
export interface MeshMessage {
id: number
@@ -375,6 +376,38 @@ export const useMeshStore = defineStore('mesh', () => {
}
}
async function sendContent(contactId: number, cid: string, caption?: string) {
try {
sending.value = true
error.value = null
const res = await rpcClient.call<{ sent: boolean; message_id: number; cid: string; size: number }>({
method: 'mesh.send-content',
params: { contact_id: contactId, cid, caption },
})
if (res.sent) await fetchMessages()
return res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to send content'
throw err
} finally {
sending.value = false
}
}
async function fetchContent(params: {
cid: string
sender_onion: string
cap_token: string
cap_exp: number
mime?: string
filename?: string
}) {
return rpcClient.call<{ fetched: boolean; cached: boolean; cid: string; size?: number; mime?: string }>({
method: 'mesh.fetch-content',
params,
})
}
async function getSessionStatus(contactId: number): Promise<SessionStatus> {
return rpcClient.call<SessionStatus>({
method: 'mesh.session-status',
@@ -495,6 +528,8 @@ export const useMeshStore = defineStore('mesh', () => {
sendInvoice,
sendCoordinate,
sendAlert,
sendContent,
fetchContent,
getSessionStatus,
rotatePrekeys,
getNodePositions,