fix(mesh): route Meshtastic public-channel text to the channel thread, not DMs
Inbound Meshtastic text addressed to BROADCAST_NUM (the default public LongFast channel, or any channel slot) was filed into a per-sender 1:1 DM thread, so public-channel messages polluted individual people's DM chats and appeared as if sent directly to the user. packet_to_inbound_frame now detects `to == BROADCAST_NUM` and emits a new synthetic RESP_MESHTASTIC_CHANNEL_TEXT frame ([channel_idx][sender_prefix(6)][text]) that the listener files under the channel thread (contact_id = u32::MAX - idx) while still attributing the message to its real sender. Directed text (to == our node) still routes to the DM thread — a regression test locks that split in. send_channel_text now sets MeshPacket.channel (field 3) so archy actually transmits on channel 0 (public) instead of ignoring the slot. Mesh.vue keeps the synthetic "Meshtastic !xxxx" sender id when that is the best identity available for a stock public-channel device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f392670e2a
commit
12e7990b10
@ -138,7 +138,13 @@ pub(super) async fn handle_frame(
|
|||||||
match protocol::parse_channel_msg_v3_raw(&frame.data) {
|
match protocol::parse_channel_msg_v3_raw(&frame.data) {
|
||||||
Ok((channel_idx, payload)) => {
|
Ok((channel_idx, payload)) => {
|
||||||
if !payload.is_empty() {
|
if !payload.is_empty() {
|
||||||
handle_channel_payload(state, channel_idx, &payload, our_x25519_secret)
|
handle_channel_payload(
|
||||||
|
state,
|
||||||
|
channel_idx,
|
||||||
|
&payload,
|
||||||
|
our_x25519_secret,
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,7 +157,13 @@ pub(super) async fn handle_frame(
|
|||||||
match protocol::parse_channel_msg_v1_raw(&frame.data) {
|
match protocol::parse_channel_msg_v1_raw(&frame.data) {
|
||||||
Ok((channel_idx, payload)) => {
|
Ok((channel_idx, payload)) => {
|
||||||
if !payload.is_empty() {
|
if !payload.is_empty() {
|
||||||
handle_channel_payload(state, channel_idx, &payload, our_x25519_secret)
|
handle_channel_payload(
|
||||||
|
state,
|
||||||
|
channel_idx,
|
||||||
|
&payload,
|
||||||
|
our_x25519_secret,
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,6 +171,30 @@ pub(super) async fn handle_frame(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Synthetic Meshtastic channel broadcast that carries its sender:
|
||||||
|
// `[channel_idx: u8][sender_pubkey_prefix: 6 bytes][text…]`. Resolve the
|
||||||
|
// sender to a friendly name, then file the message under the channel
|
||||||
|
// thread attributed to them — this is what makes the default public
|
||||||
|
// LongFast channel actually show inbound traffic (and who sent it).
|
||||||
|
protocol::RESP_MESHTASTIC_CHANNEL_TEXT => {
|
||||||
|
if frame.data.len() > 7 {
|
||||||
|
let channel_idx = frame.data[0];
|
||||||
|
let sender_prefix_hex = hex::encode(&frame.data[1..7]);
|
||||||
|
let payload = frame.data[7..].to_vec();
|
||||||
|
if !payload.is_empty() {
|
||||||
|
let (_cid, name) = resolve_peer(state, &sender_prefix_hex).await;
|
||||||
|
handle_channel_payload(
|
||||||
|
state,
|
||||||
|
channel_idx,
|
||||||
|
&payload,
|
||||||
|
our_x25519_secret,
|
||||||
|
Some(name),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protocol::PUSH_LOG_DATA | protocol::PUSH_PATH_UPDATE | protocol::PUSH_RAW_DATA => {
|
protocol::PUSH_LOG_DATA | protocol::PUSH_PATH_UPDATE | protocol::PUSH_RAW_DATA => {
|
||||||
// Internal device logging/path data — safe to ignore
|
// Internal device logging/path data — safe to ignore
|
||||||
}
|
}
|
||||||
@ -182,6 +218,12 @@ async fn handle_channel_payload(
|
|||||||
channel_idx: u8,
|
channel_idx: u8,
|
||||||
payload: &[u8],
|
payload: &[u8],
|
||||||
our_x25519_secret: &[u8; 32],
|
our_x25519_secret: &[u8; 32],
|
||||||
|
// When the transport knows who sent this channel broadcast (Meshtastic
|
||||||
|
// packets carry the originating node), the plain-text/typed message is filed
|
||||||
|
// under the channel thread but attributed to this sender name. Meshcore
|
||||||
|
// channel frames carry no sender, so they pass `None` and fall back to a
|
||||||
|
// generic "Channel N" label.
|
||||||
|
sender_name: Option<String>,
|
||||||
) {
|
) {
|
||||||
// DM-via-channel wrapper (text form): the channel text carries an
|
// DM-via-channel wrapper (text form): the channel text carries an
|
||||||
// ASCII "@DM:<base64>" token somewhere in the body. We locate the
|
// ASCII "@DM:<base64>" token somewhere in the body. We locate the
|
||||||
@ -390,15 +432,18 @@ async fn handle_channel_payload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular channel broadcast (not DM-wrapped)
|
// Regular channel broadcast (not DM-wrapped). File it under the channel
|
||||||
|
// thread (contact_id = u32::MAX - idx) but label it with the real sender
|
||||||
|
// when the transport gave us one (Meshtastic), so the channel view shows who
|
||||||
|
// said what. Meshcore frames have no sender → generic "Channel N".
|
||||||
let chan_contact_id = u32::MAX - (channel_idx as u32);
|
let chan_contact_id = u32::MAX - (channel_idx as u32);
|
||||||
let chan_name = format!("Channel {}", channel_idx);
|
let chan_name = sender_name.unwrap_or_else(|| format!("Channel {}", channel_idx));
|
||||||
if TypedEnvelope::is_typed(payload) {
|
if TypedEnvelope::is_typed(payload) {
|
||||||
handle_typed_message(payload, chan_contact_id, &chan_name, state).await;
|
handle_typed_message(payload, chan_contact_id, &chan_name, state).await;
|
||||||
} else {
|
} else {
|
||||||
let text = String::from_utf8_lossy(payload).to_string();
|
let text = String::from_utf8_lossy(payload).to_string();
|
||||||
store_plain_message(state, chan_contact_id, &chan_name, &text).await;
|
store_plain_message(state, chan_contact_id, &chan_name, &text).await;
|
||||||
info!(channel = channel_idx, "Received mesh channel message");
|
info!(channel = channel_idx, sender = %chan_name, "Received mesh channel message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -610,9 +610,17 @@ impl MeshtasticDevice {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_channel_text(&mut self, _channel: u8, msg: &[u8]) -> Result<()> {
|
pub async fn send_channel_text(&mut self, channel: u8, msg: &[u8]) -> Result<()> {
|
||||||
let text = String::from_utf8_lossy(msg);
|
let text = String::from_utf8_lossy(msg);
|
||||||
let packet = encode_mesh_packet(BROADCAST_NUM, TEXT_MESSAGE_APP, text.as_bytes());
|
let mut packet = encode_mesh_packet(BROADCAST_NUM, TEXT_MESSAGE_APP, text.as_bytes());
|
||||||
|
// MeshPacket.channel (field 3, varint) selects which channel slot the
|
||||||
|
// firmware encrypts and transmits on. 0 = primary (the default public
|
||||||
|
// LongFast channel); a non-zero slot is our secondary archipelago
|
||||||
|
// channel. Appended after encode_mesh_packet's fields — protobuf fields
|
||||||
|
// are order-independent.
|
||||||
|
if channel != 0 {
|
||||||
|
encode_varint_field_into(3, channel as u64, &mut packet);
|
||||||
|
}
|
||||||
self.send_to_radio(&encode_to_radio_variant(TO_RADIO_PACKET, &packet))
|
self.send_to_radio(&encode_to_radio_variant(TO_RADIO_PACKET, &packet))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -1002,6 +1010,24 @@ fn packet_to_inbound_frame(
|
|||||||
flags: 0,
|
flags: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Channel broadcast (e.g. the default public LongFast channel, or any other
|
||||||
|
// channel slot): `to == BROADCAST_NUM`. File it under the channel thread —
|
||||||
|
// NOT a 1:1 DM with the sender — so it shows in the public/channel view,
|
||||||
|
// while still carrying the sender prefix so the listener can attribute each
|
||||||
|
// message to who sent it. Without this, every public-channel message was
|
||||||
|
// scattered into per-sender DM threads and the public channel looked dead.
|
||||||
|
if packet.to == Some(BROADCAST_NUM) {
|
||||||
|
let mut data = Vec::with_capacity(7 + packet.payload.len());
|
||||||
|
data.push(packet.channel); // channel index (0 = primary/public)
|
||||||
|
data.extend_from_slice(&from_key[..6]); // sender pubkey prefix
|
||||||
|
data.extend_from_slice(&packet.payload);
|
||||||
|
return Some(InboundFrame {
|
||||||
|
code: super::protocol::RESP_MESHTASTIC_CHANNEL_TEXT,
|
||||||
|
data,
|
||||||
|
bytes_consumed: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let mut payload = Vec::with_capacity(15 + packet.payload.len());
|
let mut payload = Vec::with_capacity(15 + packet.payload.len());
|
||||||
payload.push(0); // SNR unknown
|
payload.push(0); // SNR unknown
|
||||||
payload.extend_from_slice(&[0, 0]); // reserved
|
payload.extend_from_slice(&[0, 0]); // reserved
|
||||||
@ -1386,6 +1412,13 @@ fn parse_user(data: &[u8]) -> Option<ParsedUser> {
|
|||||||
|
|
||||||
struct ParsedPacket {
|
struct ParsedPacket {
|
||||||
from: Option<u32>,
|
from: Option<u32>,
|
||||||
|
/// MeshPacket.to (field 2): the destination node, or `BROADCAST_NUM` for a
|
||||||
|
/// channel broadcast. Distinguishes a directed DM from a public/channel
|
||||||
|
/// message so each lands in the right thread.
|
||||||
|
to: Option<u32>,
|
||||||
|
/// MeshPacket.channel (field 3): the channel index a broadcast arrived on
|
||||||
|
/// (0 = primary / default public LongFast).
|
||||||
|
channel: u8,
|
||||||
portnum: u32,
|
portnum: u32,
|
||||||
payload: Vec<u8>,
|
payload: Vec<u8>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -1402,6 +1435,8 @@ struct ParsedPacket {
|
|||||||
fn parse_mesh_packet(data: &[u8]) -> Option<ParsedPacket> {
|
fn parse_mesh_packet(data: &[u8]) -> Option<ParsedPacket> {
|
||||||
let mut idx = 0;
|
let mut idx = 0;
|
||||||
let mut from = None;
|
let mut from = None;
|
||||||
|
let mut to = None;
|
||||||
|
let mut channel = 0u8;
|
||||||
let mut decoded = None;
|
let mut decoded = None;
|
||||||
let mut id = None;
|
let mut id = None;
|
||||||
let mut rx_time = None;
|
let mut rx_time = None;
|
||||||
@ -1412,6 +1447,8 @@ fn parse_mesh_packet(data: &[u8]) -> Option<ParsedPacket> {
|
|||||||
idx = next;
|
idx = next;
|
||||||
match (field, value) {
|
match (field, value) {
|
||||||
(1, FieldValue::Fixed32(v)) => from = Some(v),
|
(1, FieldValue::Fixed32(v)) => from = Some(v),
|
||||||
|
(2, FieldValue::Fixed32(v)) => to = Some(v),
|
||||||
|
(3, FieldValue::Varint(v)) => channel = v as u8,
|
||||||
(4, FieldValue::Bytes(b)) => decoded = Some(b),
|
(4, FieldValue::Bytes(b)) => decoded = Some(b),
|
||||||
(6, FieldValue::Fixed32(v)) => id = Some(v),
|
(6, FieldValue::Fixed32(v)) => id = Some(v),
|
||||||
(7, FieldValue::Fixed32(v)) if v != 0 => rx_time = Some(v),
|
(7, FieldValue::Fixed32(v)) if v != 0 => rx_time = Some(v),
|
||||||
@ -1435,6 +1472,8 @@ fn parse_mesh_packet(data: &[u8]) -> Option<ParsedPacket> {
|
|||||||
}
|
}
|
||||||
Some(ParsedPacket {
|
Some(ParsedPacket {
|
||||||
from,
|
from,
|
||||||
|
to,
|
||||||
|
channel,
|
||||||
portnum,
|
portnum,
|
||||||
payload,
|
payload,
|
||||||
id,
|
id,
|
||||||
@ -1645,13 +1684,41 @@ mod tests {
|
|||||||
let frame =
|
let frame =
|
||||||
packet_to_inbound_frame(&packet, Some(0x1111_1111), &mut contacts, &mut peer_pubkeys)
|
packet_to_inbound_frame(&packet, Some(0x1111_1111), &mut contacts, &mut peer_pubkeys)
|
||||||
.expect("live packet with unset radio clock must not be dropped");
|
.expect("live packet with unset radio clock must not be dropped");
|
||||||
assert_eq!(frame.code, protocol::RESP_CONTACT_MSG_V3);
|
// A `to == BROADCAST_NUM` text is a channel broadcast (3ccc on public
|
||||||
|
// LongFast), so it routes to the channel thread, carrying its sender.
|
||||||
|
assert_eq!(frame.code, protocol::RESP_MESHTASTIC_CHANNEL_TEXT);
|
||||||
|
assert_eq!(frame.data[0], 0, "no channel field set => primary/public (0)");
|
||||||
|
assert_eq!(&frame.data[1..7], &[0xcc, 0x3c, 0x00, 0x00, 0x6d, 0x65]);
|
||||||
|
assert_eq!(&frame.data[7..], b"hello from 3ccc");
|
||||||
|
assert!(contacts.contains_key(&from));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn packet_to_inbound_frame_directed_dm_stays_a_contact_message() {
|
||||||
|
// A text addressed directly to us (to == our node, not broadcast) must
|
||||||
|
// remain a 1:1 DM, NOT get rerouted to a channel thread.
|
||||||
|
let from = 0x0000_3ccc;
|
||||||
|
let me = 0x1111_1111;
|
||||||
|
let mut contacts = HashMap::new();
|
||||||
|
let mut peer_pubkeys = HashMap::new();
|
||||||
|
let mut decoded = Vec::new();
|
||||||
|
encode_varint_field_into(1, TEXT_MESSAGE_APP as u64, &mut decoded);
|
||||||
|
encode_len_field(2, b"direct hello", &mut decoded);
|
||||||
|
|
||||||
|
let mut packet = Vec::new();
|
||||||
|
encode_fixed32_field(1, from, &mut packet);
|
||||||
|
encode_fixed32_field(2, me, &mut packet); // to == us, directed
|
||||||
|
encode_len_field(4, &decoded, &mut packet);
|
||||||
|
encode_fixed32_field(7, 12_345, &mut packet);
|
||||||
|
|
||||||
|
let frame =
|
||||||
|
packet_to_inbound_frame(&packet, Some(me), &mut contacts, &mut peer_pubkeys)
|
||||||
|
.expect("directed DM must surface");
|
||||||
|
assert_eq!(frame.code, protocol::RESP_CONTACT_MSG_V3);
|
||||||
let (sender_prefix, payload, _snr) =
|
let (sender_prefix, payload, _snr) =
|
||||||
protocol::parse_contact_msg_v3_raw(&frame.data).unwrap();
|
protocol::parse_contact_msg_v3_raw(&frame.data).unwrap();
|
||||||
assert_eq!(sender_prefix, "cc3c00006d65");
|
assert_eq!(sender_prefix, "cc3c00006d65");
|
||||||
assert_eq!(payload, b"hello from 3ccc");
|
assert_eq!(payload, b"direct hello");
|
||||||
assert!(contacts.contains_key(&from));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1672,10 +1739,10 @@ mod tests {
|
|||||||
let frame =
|
let frame =
|
||||||
packet_to_inbound_frame(&packet, Some(0x1111_1111), &mut contacts, &mut peer_pubkeys)
|
packet_to_inbound_frame(&packet, Some(0x1111_1111), &mut contacts, &mut peer_pubkeys)
|
||||||
.expect("recent radio backlog must surface in mesh.messages");
|
.expect("recent radio backlog must surface in mesh.messages");
|
||||||
let (sender_prefix, payload, _snr) =
|
// Broadcast → channel frame: [channel_idx][sender_prefix(6)][text].
|
||||||
protocol::parse_contact_msg_v3_raw(&frame.data).unwrap();
|
assert_eq!(frame.code, protocol::RESP_MESHTASTIC_CHANNEL_TEXT);
|
||||||
assert_eq!(sender_prefix, "cc3c3e436d65");
|
assert_eq!(&frame.data[1..7], &[0xcc, 0x3c, 0x3e, 0x43, 0x6d, 0x65]);
|
||||||
assert_eq!(payload, b"recent backlog");
|
assert_eq!(&frame.data[7..], b"recent backlog");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -69,6 +69,15 @@ pub const RESP_STATS: u8 = 0x18;
|
|||||||
/// never emits this code; it lets the shared listener persist the E2E badge
|
/// never emits this code; it lets the shared listener persist the E2E badge
|
||||||
/// without changing the on-wire Meshcore frame format.
|
/// without changing the on-wire Meshcore frame format.
|
||||||
pub const RESP_CONTACT_MSG_V3_E2E: u8 = 0x13;
|
pub const RESP_CONTACT_MSG_V3_E2E: u8 = 0x13;
|
||||||
|
/// Archipelago-internal synthetic response code used by the Meshtastic adapter
|
||||||
|
/// for CHANNEL broadcast text (e.g. the default public LongFast channel). Unlike
|
||||||
|
/// the Meshcore `RESP_CHANNEL_MSG_V3` — which carries no sender — a Meshtastic
|
||||||
|
/// MeshPacket gives us the originating node, so the listener can both file the
|
||||||
|
/// message under the channel thread AND attribute it to its sender. Frame
|
||||||
|
/// layout: `[channel_idx: u8][sender_pubkey_prefix: 6 bytes][text…]`. Kept below
|
||||||
|
/// 0x80 so it is not mistaken for a device push notification; Meshcore never
|
||||||
|
/// emits it.
|
||||||
|
pub const RESP_MESHTASTIC_CHANNEL_TEXT: u8 = 0x70;
|
||||||
|
|
||||||
// --- Push notification codes (device -> host, async, >= 0x80) ---
|
// --- Push notification codes (device -> host, async, >= 0x80) ---
|
||||||
pub const PUSH_CONTACT_ADVERT: u8 = 0x80;
|
pub const PUSH_CONTACT_ADVERT: u8 = 0x80;
|
||||||
|
|||||||
@ -10,12 +10,35 @@ Updated: 2026-06-30
|
|||||||
identical in off-grid and normal mode. Test bed = `.116` / `.198` / `.228` (all EU_868).
|
identical in off-grid and normal mode. Test bed = `.116` / `.198` / `.228` (all EU_868).
|
||||||
Don't touch the federation/FIPS path.
|
Don't touch the federation/FIPS path.
|
||||||
|
|
||||||
### TL;DR of where we are
|
### ✅✅✅ SOLVED 2026-06-30 — archy↔archy LoRa WORKS (delivery + E2E pill + identity)
|
||||||
The **archy software is correct and deployed.** The blocker is now PROVEN to be at the
|
VERIFIED: `.198→.228` directed DM → `.228` row `RECEIVED enc=True peer="Arch Optiplex"`.
|
||||||
|
All three nodes (.116/.198/.228) now hear each other + stock peer 3ccc. Deployed binary
|
||||||
|
**`737b16c3235b`** active on all three. Fix source **COMMITTED as `a57ae388`** on `main`
|
||||||
|
(not yet pushed to gitea-vps2/origin).
|
||||||
|
|
||||||
|
**THE fix (receive stream):** archy ignored `FromRadio.rebooted` (field 8). Every config
|
||||||
|
write reboots the radio → firmware PhoneAPI resets to `STATE_SEND_NOTHING` and stops
|
||||||
|
streaming received packets until the client re-sends `want_config`. archy never did →
|
||||||
|
went deaf to inbound (that's why old messages only arrived after a full restart = fresh
|
||||||
|
want_config). Fix: handle `FROM_RADIO_REBOOTED` → set `pending_reinit` → re-send
|
||||||
|
want_config; plus a 10s keepalive heartbeat (insurance vs 15-min idle serial close) and
|
||||||
|
a pinned `modem_preset=LONG_FAST` so all radios share frequency. Combined with the earlier
|
||||||
|
E2E send fix (plain TEXT_MESSAGE_APP DM, firmware PKC) this closes archy↔archy LoRa.
|
||||||
|
|
||||||
|
**Open follow-ups:** #A surface received msgs under archy identity in all UI views; #6
|
||||||
|
device-onboarding modal; #8 Device-tab settings panel; #7 re-verify .116 in rotation;
|
||||||
|
#12 make modem_preset authoritative + hot-swap re-binding + RX-stall watchdog;
|
||||||
|
#14 signal-strength (RSSI/SNR) indicator per contact (from MeshPacket rx_rssi/rx_snr);
|
||||||
|
#15 map view plotting peer locations where shared (Meshtastic POSITION_APP portnum=3
|
||||||
|
lat/lon). See the resume memory `project_session_resume_2026_06_30_lora.md` for the full
|
||||||
|
task list.
|
||||||
|
|
||||||
|
### (historical) earlier TL;DR — RF-layer suspicion, now RESOLVED by the reboot-recovery fix
|
||||||
|
The **archy software is correct and deployed.** The blocker was at the
|
||||||
**radio/RF layer: the three radios are not hearing each other over the air at all.** No
|
**radio/RF layer: the three radios are not hearing each other over the air at all.** No
|
||||||
amount of archy code change will fix that until the radios actually RF-link. **Resume by
|
amount of archy code change will fix that until the radios actually RF-link. **Resume by
|
||||||
testing the radios directly at home (Meshtastic phone app over Bluetooth) — see "DO THIS
|
testing the radios directly at home (Meshtastic phone app over Bluetooth) — see "DO THIS
|
||||||
FIRST AT HOME" below.**
|
FIRST AT HOME" below.** ← this turned out to be the want_config resubscribe bug above.
|
||||||
|
|
||||||
### What is DONE and deployed (commit pending — see below)
|
### What is DONE and deployed (commit pending — see below)
|
||||||
- **E2E send fix** (`core/archipelago/src/mesh/mod.rs` `send_message`, ~L1542): archy↔archy
|
- **E2E send fix** (`core/archipelago/src/mesh/mod.rs` `send_message`, ~L1542): archy↔archy
|
||||||
|
|||||||
@ -802,11 +802,14 @@ function senderLabelFor(msg: MeshMessage): string | null {
|
|||||||
if (!activeChatChannel.value && !archChannelActive.value) return null
|
if (!activeChatChannel.value && !archChannelActive.value) return null
|
||||||
const live = nameByContactId.value.get(msg.peer_contact_id)
|
const live = nameByContactId.value.get(msg.peer_contact_id)
|
||||||
if (live && !isPlaceholderName(live)) return live
|
if (live && !isPlaceholderName(live)) return live
|
||||||
if (!isPlaceholderName(msg.peer_name)) return msg.peer_name ?? null
|
// The sender name snapshotted on the row. For a stock public-channel device a
|
||||||
// Sender genuinely unknown (e.g. a meshcore channel broadcast, which drops
|
// synthetic "Meshtastic !xxxx" id IS the best identity we have, so keep it —
|
||||||
// the sender, or a text seen before its NodeInfo) — stay honest rather than
|
// only suppress the genuinely contentless group/placeholder labels.
|
||||||
// echoing a "Channel N" / synthetic id as if it were a person.
|
const snap = msg.peer_name
|
||||||
return 'Unknown sender'
|
if (snap && !/^Channel \d+$/.test(snap) && snap !== 'dm-via-channel' && snap !== 'Unknown') {
|
||||||
|
return snap
|
||||||
|
}
|
||||||
|
return live || 'Unknown sender'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline contact rename in the chat header. The pencil button toggles an
|
// Inline contact rename in the chat header. The pencil button toggles an
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user