feat(mesh-ui): reply banner + inline reaction chips (Phase 2a)

Tap a bubble to open an action menu with Reply + 6 quick reactions.
Reply stashes the target MessageKey and flips the Send button to
"Reply" mode, routing through mesh.send-reply. Reactions call
mesh.send-reaction immediately and render as chips under the target
bubble, collapsed per emoji with a count and self-highlight. Reaction
messages are filtered out of the main chat stream so they don't create
standalone bubbles. Reply bubbles show a "↳ quoted snippet" header
when the target is still in the local window.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-13 13:19:30 -04:00
parent a360f90647
commit 649433b7fd
2 changed files with 196 additions and 6 deletions

View File

@@ -48,6 +48,8 @@ export type MeshMessageTypeLabel =
| 'lightning_relay'
| 'lightning_relay_response'
| 'content_ref'
| 'reply'
| 'reaction'
export interface MeshMessage {
id: number
@@ -61,6 +63,10 @@ export interface MeshMessage {
message_type?: MeshMessageTypeLabel
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typed_payload?: Record<string, any> | null
/// Cross-transport identity for this message — (sender_pubkey, sender_seq)
/// forms a stable MessageKey used by replies/reactions.
sender_pubkey?: string | null
sender_seq?: number | null
}
export interface InvoiceData {
@@ -394,6 +400,34 @@ export const useMeshStore = defineStore('mesh', () => {
}
}
async function sendReply(contactId: number, targetPubkey: string, targetSeq: number, text: string) {
sending.value = true
try {
const res = await rpcClient.call<{ sent: boolean; message_id: number; sender_seq: number }>({
method: 'mesh.send-reply',
params: { contact_id: contactId, target_pubkey: targetPubkey, target_seq: targetSeq, text },
})
if (res.sent) await fetchMessages()
return res
} finally {
sending.value = false
}
}
async function sendReaction(contactId: number, targetPubkey: string, targetSeq: number, emoji: string) {
try {
const res = await rpcClient.call<{ sent: boolean; message_id: number; sender_seq: number }>({
method: 'mesh.send-reaction',
params: { contact_id: contactId, target_pubkey: targetPubkey, target_seq: targetSeq, emoji },
})
if (res.sent) await fetchMessages()
return res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to send reaction'
throw err
}
}
async function fetchContent(params: {
cid: string
sender_onion: string
@@ -530,6 +564,8 @@ export const useMeshStore = defineStore('mesh', () => {
sendAlert,
sendContent,
fetchContent,
sendReply,
sendReaction,
getSessionStatus,
rotatePrekeys,
getNodePositions,