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); }