From 07ca6ca28643a7b655e2965ae278e61de8aac7e1 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 13 Apr 2026 08:01:21 -0400 Subject: [PATCH] feat(mesh-ui): render tx/lightning relay typed messages and skip self-send Adds renderers for tx_relay, tx_relay_response, tx_confirmation, lightning_relay, and lightning_relay_response message types so these appear as rich cards in the chat stream. sendArchMessage now looks up our own onion via getTorAddress and skips federation peers that match, preventing the duplicate "echoed back to self" message we were seeing on single-node test federations. Empty-federation error message is also clearer. Co-Authored-By: Claude Opus 4.6 (1M context) --- neode-ui/src/stores/mesh.ts | 5 ++++ neode-ui/src/views/Mesh.vue | 55 +++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index 5ae38b5c..634904c0 100644 --- a/neode-ui/src/stores/mesh.ts +++ b/neode-ui/src/stores/mesh.ts @@ -42,6 +42,11 @@ export type MeshMessageTypeLabel = | 'psbt_hash' | 'coordinate' | 'block_header' + | 'tx_relay' + | 'tx_relay_response' + | 'tx_confirmation' + | 'lightning_relay' + | 'lightning_relay_response' export interface MeshMessage { id: number diff --git a/neode-ui/src/views/Mesh.vue b/neode-ui/src/views/Mesh.vue index bf50dd3a..9d59fe18 100644 --- a/neode-ui/src/views/Mesh.vue +++ b/neode-ui/src/views/Mesh.vue @@ -97,11 +97,20 @@ async function sendArchMessage() { sendingArch.value = true try { const nodes = await rpcClient.federationListNodes() + // Get our own onion address to skip sending to self + let selfOnion: string | null = null + try { + const tor = await rpcClient.getTorAddress() + selfOnion = tor.tor_address + } catch { /* non-fatal */ } const msg = messageText.value.trim() let sent = 0 for (const node of nodes.nodes) { + const nodeOnion = node.onion || node.did + // Skip sending to ourselves (would create duplicate received message) + if (selfOnion && (nodeOnion === selfOnion || nodeOnion === selfOnion.replace('.onion', '') || selfOnion === nodeOnion + '.onion')) continue try { - await rpcClient.sendMessageToPeer(node.onion || node.did, msg) + await rpcClient.sendMessageToPeer(nodeOnion, msg) sent++ } catch { /* some peers may be offline */ } } @@ -109,7 +118,7 @@ async function sendArchMessage() { await rpcClient.call({ method: 'node-store-sent', params: { message: msg } }) } catch { /* non-fatal */ } messageText.value = '' - if (sent === 0) sendError.value = 'No peers reachable — message may arrive when they come online' + if (sent === 0 && nodes.nodes.length <= 1) sendError.value = 'No other peers in federation — add nodes first' await loadArchMessages() } catch (e) { sendError.value = e instanceof Error ? e.message : 'Send failed' @@ -217,7 +226,7 @@ const chatMessages = computed(() => { } else if (m.from_name) { peerName = m.from_name } else if (fedNodeNames.value[m.from_pubkey]) { - peerName = fedNodeNames.value[m.from_pubkey] + peerName = fedNodeNames.value[m.from_pubkey]! } else { peerName = m.from_pubkey.slice(0, 12) + '...' } @@ -642,6 +651,46 @@ function truncatePubkey(hex: string | null): string { {{ msg.typed_payload.message || msg.plaintext }} + +
+ + TX relay #{{ msg.typed_payload.request_id }} ({{ (msg.typed_payload.tx_hex || '').length }} hex chars) +
+ +
+ {{ msg.typed_payload.txid ? '✅' : '❌' }} + + + + +
+ +
+ + {{ msg.typed_payload.confirmations }} conf @ block {{ msg.typed_payload.block_height }} — {{ String(msg.typed_payload.txid || '').substring(0, 12) }}… +
+ +
+
+ + Lightning Relay Request +
+
{{ (msg.typed_payload.amount_sats || 0).toLocaleString() }} sats
+
{{ (msg.typed_payload.bolt11 || '').substring(0, 40) }}…
+
+ +
+
+ {{ msg.typed_payload.preimage ? '✅' : '❌' }} + + + + +
+
hash: {{ String(msg.typed_payload.payment_hash).substring(0, 20) }}…
+
preimage: {{ String(msg.typed_payload.preimage).substring(0, 20) }}…
+
{{ msg.typed_payload.error }}
+
{{ msg.plaintext }}