fix(mesh): Meshtastic 3ccc pkc_capable pill + Sideband image interop + critical CBOR wire-bloat fix
Merges in the meshtastic agent's now-finished work alongside this session's
continuation: stock-peer (3ccc) PKI-capability is now stamped through
get_contacts -> refresh_contacts -> MeshPeer.pkc_capable, so a directed DM to/from
a PKC-capable stock Meshtastic peer correctly shows the E2E pill on the Sent row,
not just received messages. Confirmed live: .198 sees "Meshtastic 3ccc" with
pkc_capable=true.
Also fixes two real interop/correctness bugs found while live-testing the
Reticulum <-> Sideband link:
- Receive: the daemon only ever read LXMF's plain-text content, silently
dropping native FIELD_IMAGE/FIELD_FILE_ATTACHMENTS fields — a stock
Sideband/NomadNet photo vanished into a blank-space message. Now decoded
into the same ContentInline typed envelope our own attachments use.
- Send: images to a non-archy (stock) peer now use native LXMF FIELD_IMAGE
instead of our own opaque CBOR wire format, which Sideband can't decode.
- Root cause of a garbled MC-chunk-fragment bug: TypedEnvelope.v/.sig (the
OUTER wrapper every message type uses) serialized raw bytes as a CBOR
array-of-integers instead of a native byte string, bloating every
message on the wire ~2-3.5x — enough to push even a tiny ReadReceipt
over the 140-byte single-frame chunking threshold. Root-caused by
reading ciborium's deserializer source directly (deserialize_bytes only
works within its internal scratch buffer; deserialize_byte_buf streams
unbounded).
Frontend: consolidated the attach/record buttons into a single animated "+"
menu (was overflowing the compose row).
857/857 tests pass. Verified live across all 5 deploy-roster nodes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f54c853128
commit
0eb5c258f5
+61
-24
@@ -1276,6 +1276,17 @@ function clearPendingAttachment() {
|
||||
|
||||
// ── ContentRef attach + fetch (Phase 3b) ──────────────────────────────────
|
||||
const attaching = ref(false)
|
||||
// Compose row's "+" attach menu — a single toggle button that pops open a
|
||||
// small vertical stack (attach file / voice message) instead of cramming
|
||||
// each option in as its own always-visible button (was overflowing).
|
||||
const showAttachMenu = ref(false)
|
||||
function closeAttachMenuOnOutsideClick(ev: MouseEvent) {
|
||||
if (!showAttachMenu.value) return
|
||||
const target = ev.target as HTMLElement
|
||||
if (!target.closest('.mesh-attach-menu-anchor')) showAttachMenu.value = false
|
||||
}
|
||||
onMounted(() => document.addEventListener('click', closeAttachMenuOnOutsideClick))
|
||||
onUnmounted(() => document.removeEventListener('click', closeAttachMenuOnOutsideClick))
|
||||
const attachError = ref<string | null>(null)
|
||||
const fetchingCids = ref<Set<string>>(new Set())
|
||||
const fetchedUrls = ref<Map<string, string>>(new Map())
|
||||
@@ -1509,6 +1520,14 @@ let voiceChunks: Blob[] = []
|
||||
|
||||
async function startVoiceRecording() {
|
||||
if (isRecordingVoice.value || attaching.value || !activeChatPeer.value) return
|
||||
// navigator.mediaDevices is only exposed by the browser in a secure context
|
||||
// (HTTPS, or localhost) — undefined entirely on plain-HTTP LAN nodes like
|
||||
// .116, which throws a confusing "Cannot read properties of undefined"
|
||||
// instead of the permission-style error handled below.
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
attachError.value = 'Voice messages need HTTPS (or localhost) — the browser blocks microphone access on plain HTTP'
|
||||
return
|
||||
}
|
||||
try {
|
||||
voiceRecorderStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
} catch (e) {
|
||||
@@ -2099,30 +2118,48 @@ function isImageMime(mime?: string): boolean {
|
||||
<button class="mesh-chat-pending-clear" @click="clearPendingAttachment" title="Discard attachment">✕</button>
|
||||
</div>
|
||||
<div class="mesh-chat-compose-row">
|
||||
<label
|
||||
v-if="activeChatPeer"
|
||||
class="glass-button mesh-chat-attach-btn"
|
||||
:class="{ 'is-busy': attaching }"
|
||||
:title="attaching ? 'uploading…' : 'Attach file'"
|
||||
>
|
||||
<input type="file" @change="handleAttachFile" style="display:none;" :disabled="attaching" />
|
||||
<span v-if="attaching" class="mesh-spinner" aria-hidden="true"></span>
|
||||
<span v-else>📎</span>
|
||||
</label>
|
||||
<button
|
||||
v-if="activeChatPeer"
|
||||
type="button"
|
||||
class="glass-button mesh-chat-record-btn"
|
||||
:class="{ 'is-recording': isRecordingVoice }"
|
||||
:disabled="attaching"
|
||||
:title="isRecordingVoice ? 'Release to send' : 'Hold to record a voice message'"
|
||||
@pointerdown.prevent="startVoiceRecording"
|
||||
@pointerup.prevent="stopVoiceRecording"
|
||||
@pointerleave="stopVoiceRecordingIfActive"
|
||||
>
|
||||
<span v-if="isRecordingVoice" class="mesh-spinner" aria-hidden="true"></span>
|
||||
<span v-else>🎤</span>
|
||||
</button>
|
||||
<div v-if="activeChatPeer" class="mesh-attach-menu-anchor">
|
||||
<Transition name="mesh-attach-stack">
|
||||
<div v-if="showAttachMenu" class="mesh-attach-stack">
|
||||
<label
|
||||
class="glass-button mesh-chat-attach-btn"
|
||||
:class="{ 'is-busy': attaching }"
|
||||
:title="attaching ? 'uploading…' : 'Attach file'"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
@change="(e) => { showAttachMenu = false; handleAttachFile(e) }"
|
||||
style="display:none;"
|
||||
:disabled="attaching"
|
||||
/>
|
||||
<span v-if="attaching" class="mesh-spinner" aria-hidden="true"></span>
|
||||
<span v-else>📎</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="glass-button mesh-chat-record-btn"
|
||||
:class="{ 'is-recording': isRecordingVoice }"
|
||||
:disabled="attaching"
|
||||
:title="isRecordingVoice ? 'Release to send' : 'Hold to record a voice message'"
|
||||
@pointerdown.prevent="startVoiceRecording"
|
||||
@pointerup.prevent="() => { stopVoiceRecording(); showAttachMenu = false }"
|
||||
@pointerleave="() => { stopVoiceRecordingIfActive(); showAttachMenu = false }"
|
||||
>
|
||||
<span v-if="isRecordingVoice" class="mesh-spinner" aria-hidden="true"></span>
|
||||
<span v-else>🎤</span>
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
<button
|
||||
type="button"
|
||||
class="glass-button mesh-chat-plus-btn"
|
||||
:class="{ 'is-open': showAttachMenu }"
|
||||
title="Attach"
|
||||
@click="showAttachMenu = !showAttachMenu"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="messageText"
|
||||
class="mesh-chat-input"
|
||||
|
||||
@@ -472,6 +472,22 @@ select.mesh-bitcoin-input option { background: #1a1a2e; color: rgba(255,255,255,
|
||||
.mesh-chat-attach-btn.is-busy { opacity: 0.8; cursor: wait; }
|
||||
.mesh-chat-record-btn.is-recording { background: rgba(239,68,68,0.25); animation: mesh-record-pulse 1.1s ease-in-out infinite; }
|
||||
@keyframes mesh-record-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.4); } 50% { box-shadow: 0 0 0 6px rgba(239,68,68,0); } }
|
||||
|
||||
/* "+" attach menu — replaces individually-visible attach/record buttons
|
||||
(was overflowing the compose row) with one toggle + an animated stack. */
|
||||
.mesh-attach-menu-anchor { position: relative; flex-shrink: 0; }
|
||||
.mesh-chat-plus-btn { font-size: 1.3rem; line-height: 1; font-weight: 300; transition: transform 0.2s ease; }
|
||||
.mesh-chat-plus-btn.is-open { transform: rotate(45deg); background: rgba(255,255,255,0.14); }
|
||||
.mesh-attach-stack {
|
||||
position: absolute; bottom: calc(100% + 8px); left: 0;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
}
|
||||
.mesh-attach-stack-enter-active, .mesh-attach-stack-leave-active {
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
.mesh-attach-stack-enter-from, .mesh-attach-stack-leave-to {
|
||||
opacity: 0; transform: translateY(8px) scale(0.9);
|
||||
}
|
||||
.mesh-chat-reaction-btn.is-busy { background: rgba(251,146,60,0.25); }
|
||||
.mesh-chat-reaction-btn:disabled { opacity: 0.6; cursor: wait; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user