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:
co-authored by
Claude Opus 4.6
parent
8ef7af985d
commit
bdacc06a2b
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user