feat(mesh-ui): Telegram-style action menu + Forward/Edit/Delete/ReadReceipt/rotate/outbox

* Replaces click-anywhere-on-bubble with a tiny ⋯ trigger in the meta row
  that fades in on hover (always visible on touch devices). Outside-click
  closes the menu, bubble gets a `menu-open` class so the trigger stays lit.
* Action menu gains Forward (any message) + Edit + Delete (own messages
  only, delete is red). Reaction spinner + reply preview upgraded to handle
  typed targets (attachment/invoice/location/alert) via summarizeForPreview.
* Pending-edit banner with ✎ icon mirrors the reply banner; Send flushes as
  mesh.edit-message when pendingEdit is set.
* Forwarded bubbles render "↪ Forwarded from {orig_name}" header; tombstone
  + (edited) markers; pending-reply close button upsized (28px, red hover).
* Scroll + message-arrival watcher fires a debounced 400ms read receipt
  with per-peer seq dedup so we never double-ack.
* Chat header: ⟲ rotate-prekeys button next to the shield badge; 📤 outbox
  count when mesh.outbox reports queued messages. Blob-store test widget
  removed and chat list now sorts by most-recent message timestamp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-13 18:50:08 -04:00
co-authored by Claude Opus 4.6
parent 8ef7af985d
commit bdacc06a2b
3 changed files with 345 additions and 104 deletions
+68
View File
@@ -428,6 +428,69 @@ export const useMeshStore = defineStore('mesh', () => {
}
}
async function getOutbox() {
try {
return await rpcClient.call<{ count: number; messages?: unknown[] }>({ method: 'mesh.outbox' })
} catch {
return { count: 0 }
}
}
async function sendReadReceipt(contactId: number, targetPubkey: string, targetSeq: number) {
try {
const res = await rpcClient.call<{ sent: boolean }>({
method: 'mesh.send-read-receipt',
params: { contact_id: contactId, target_pubkey: targetPubkey, target_seq: targetSeq },
})
return res
} catch {
// Read receipts are best-effort — never surface errors to the user.
return null
}
}
async function forwardMessage(contactId: number, sourceMessageId: number) {
sending.value = true
try {
const res = await rpcClient.call<{ sent: boolean; message_id: number }>({
method: 'mesh.forward-message',
params: { contact_id: contactId, source_message_id: sourceMessageId },
})
if (res.sent) await fetchMessages()
return res
} finally {
sending.value = false
}
}
async function editMessage(contactId: number, targetSeq: number, newText: string) {
try {
const res = await rpcClient.call<{ sent: boolean; message_id: number }>({
method: 'mesh.edit-message',
params: { contact_id: contactId, target_seq: targetSeq, new_text: newText },
})
if (res.sent) await fetchMessages()
return res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to edit message'
throw err
}
}
async function deleteMessage(contactId: number, targetSeq: number) {
try {
const res = await rpcClient.call<{ sent: boolean; message_id: number }>({
method: 'mesh.delete-message',
params: { contact_id: contactId, target_seq: targetSeq },
})
if (res.sent) await fetchMessages()
return res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to delete message'
throw err
}
}
async function fetchContent(params: {
cid: string
sender_onion: string
@@ -566,6 +629,11 @@ export const useMeshStore = defineStore('mesh', () => {
fetchContent,
sendReply,
sendReaction,
getOutbox,
sendReadReceipt,
forwardMessage,
editMessage,
deleteMessage,
getSessionStatus,
rotatePrekeys,
getNodePositions,