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) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-30 13:04:41 -04:00
parent a57ae388ec
commit f392670e2a
2 changed files with 43 additions and 0 deletions

View File

@ -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<Map<number, string>>(() => {
const out = new Map<number, string>()
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"
>
<div class="mesh-chat-bubble" :class="[msg.direction, msg.message_type ? 'typed-' + msg.message_type : '', { 'menu-open': actionMenuForId === msg.id }]">
<div v-if="senderLabelFor(msg)" class="mesh-chat-bubble-sender">{{ senderLabelFor(msg) }}</div>
<div v-if="replyTargetPreview(msg)" class="mesh-chat-reply-quote">
{{ replyTargetPreview(msg) }}
</div>

View File

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