From f392670e2a4393bde791e6739781fbcddb7a5dc2 Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 30 Jun 2026 13:04:41 -0400 Subject: [PATCH] feat(mesh): show sender identity on received channel messages Received messages snapshot peer_name at receive time, so a Meshtastic text that arrived before its sender's NodeInfo was stuck showing the synthetic "Meshtastic !xxxx" id forever, and channel/group bubbles showed no sender at all. Add a per-bubble sender label for received messages in multi-sender views (mesh + Archipelago channels), resolved LIVE from the peer table so it always shows the current archy identity (e.g. "Arch Optiplex") the moment NodeInfo is learned. Falls back to "Unknown sender" rather than echoing a Channel/synthetic placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) --- neode-ui/src/views/Mesh.vue | 42 +++++++++++++++++++++++++ neode-ui/src/views/mesh/mesh-styles.css | 1 + 2 files changed, 43 insertions(+) diff --git a/neode-ui/src/views/Mesh.vue b/neode-ui/src/views/Mesh.vue index d1337a2c..f4fe9704 100644 --- a/neode-ui/src/views/Mesh.vue +++ b/neode-ui/src/views/Mesh.vue @@ -768,6 +768,47 @@ function mergedUnreadCount(mp: MergedPeer): number { return total } +// Live contact_id -> friendly display name, rebuilt from the current peer +// table. Received messages snapshot a `peer_name` at receive time, so a text +// that arrived before its sender's NodeInfo is stuck showing a synthetic +// "Meshtastic !xxxx" id forever. Resolving the name live here means the bubble +// always shows the current archy identity (e.g. "Arch Optiplex") the moment it +// is learned — in every view, without rewriting stored rows. +const nameByContactId = computed>(() => { + const out = new Map() + for (const mp of mergedPeers.value) { + for (const cid of mp.contact_ids) out.set(cid, mp.display_name) + } + return out +}) + +// A generic, non-identifying placeholder the backend stamps before the real +// sender is known. We never want these to win over a live archy name. +function isPlaceholderName(name: string | null | undefined): boolean { + if (!name) return true + return /^Meshtastic !?[0-9a-f]{1,8}$/i.test(name) + || /^Channel \d+$/.test(name) + || /^Node #/.test(name) + || name === 'dm-via-channel' + || name === 'Unknown' +} + +// Sender label shown above a RECEIVED bubble. Only meaningful in multi-sender +// views (mesh channels + the Archipelago channel) where the header alone can't +// attribute each message; 1:1 DM threads already name the peer in the header. +// Prefers the live archy identity over the snapshotted name. +function senderLabelFor(msg: MeshMessage): string | null { + if (msg.direction !== 'received') return null + if (!activeChatChannel.value && !archChannelActive.value) return null + const live = nameByContactId.value.get(msg.peer_contact_id) + if (live && !isPlaceholderName(live)) return live + if (!isPlaceholderName(msg.peer_name)) return msg.peer_name ?? null + // Sender genuinely unknown (e.g. a meshcore channel broadcast, which drops + // the sender, or a text seen before its NodeInfo) — stay honest rather than + // echoing a "Channel N" / synthetic id as if it were a person. + return 'Unknown sender' +} + // Inline contact rename in the chat header. The pencil button toggles an // input bound to renameDraft; commit fires mesh.contacts-save keyed by // DID (or pubkey hex as fallback) so the alias propagates everywhere @@ -1689,6 +1730,7 @@ function isImageMime(mime?: string): boolean { :class="msg.direction" >
+
{{ senderLabelFor(msg) }}
↳ {{ replyTargetPreview(msg) }}
diff --git a/neode-ui/src/views/mesh/mesh-styles.css b/neode-ui/src/views/mesh/mesh-styles.css index fea1568d..059c7fcb 100644 --- a/neode-ui/src/views/mesh/mesh-styles.css +++ b/neode-ui/src/views/mesh/mesh-styles.css @@ -144,6 +144,7 @@ .mesh-chat-bubble { max-width: 75%; padding: 10px 14px; border-radius: 16px; word-break: break-word; } .mesh-chat-bubble.sent { background: rgba(251, 146, 60, 0.15); border: 1px solid rgba(251, 146, 60, 0.2); border-bottom-right-radius: 4px; } .mesh-chat-bubble.received { background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08); border-bottom-left-radius: 4px; } +.mesh-chat-bubble-sender { font-size: 0.7rem; font-weight: 600; color: rgba(251, 146, 60, 0.85); margin-bottom: 3px; } .mesh-chat-bubble-text { color: rgba(255, 255, 255, 0.9); font-size: 0.9rem; line-height: 1.4; } .mesh-chat-bubble-meta { display: flex; align-items: center; gap: 6px; margin-top: 4px; justify-content: flex-end; } .mesh-chat-bubble-time { font-size: 0.65rem; color: rgba(255, 255, 255, 0.3); }