Updated channel sharing so channel is immediately added upon receipt of DM, no additional steps required

This commit is contained in:
pelgraine
2026-05-23 06:51:06 +10:00
parent 4cc15f7ab0
commit 834cd5fc87
4 changed files with 101 additions and 115 deletions
+46 -5
View File
@@ -661,7 +661,7 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
_voiceEnvHandler(from.name, text);
}
// Intercept channel share messages before BLE gets them
// Intercept channel share messages -- auto-add the channel
if (text && strncmp(text, MECK_CH_PREFIX, MECK_CH_PREFIX_LEN) == 0) {
const char* payload = text + MECK_CH_PREFIX_LEN;
const char* sep = strchr(payload, '|');
@@ -677,11 +677,53 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
if (hexLen >= 32) {
uint8_t secret[16];
mesh::Utils::fromHex(secret, 16, hexStr);
addPendingInvite(chName, secret, from.name);
Serial.printf("Channel invite from %s: '%s'\n", from.name, chName);
// Check if channel already exists (by name)
bool exists = false;
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails existing;
if (getChannel(i, existing) && existing.name[0] != '\0'
&& strcmp(existing.name, chName) == 0) {
exists = true;
break;
}
}
if (!exists) {
// Find empty slot and add
ChannelDetails newCh;
memset(&newCh, 0, sizeof(newCh));
strncpy(newCh.name, chName, sizeof(newCh.name));
newCh.name[31] = '\0';
memcpy(newCh.channel.secret, secret, 16);
bool added = false;
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails existing;
if (!getChannel(i, existing) || existing.name[0] == '\0') {
if (setChannel(i, newCh)) {
saveChannels();
added = true;
Serial.printf("Channel '%s' added from %s at idx %d\n",
chName, from.name, i);
}
break;
}
}
if (added && _ui) {
char buf[64];
snprintf(buf, sizeof(buf), "Channel '%s' added from %s", chName, from.name);
_ui->showAlert(buf, 3000);
} else if (!added) {
Serial.printf("Channel '%s' from %s: no empty slot\n", chName, from.name);
}
} else {
Serial.printf("Channel '%s' from %s: already exists\n", chName, from.name);
}
}
// Sanitise display text
// Sanitise display text for the DM conversation
char sanitised[64];
snprintf(sanitised, sizeof(sanitised), "Shared channel: %s", chName);
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, sanitised);
@@ -1355,7 +1397,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
memset(send_scope.key, 0, sizeof(send_scope.key));
memset(_sent_track, 0, sizeof(_sent_track));
_sent_track_idx = 0;
memset(_pendingInvites, 0, sizeof(_pendingInvites));
_admin_contact_idx = -1;
_discoveredCount = 0;
_discoveryActive = false;
+3 -54
View File
@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 11
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 May 2026"
#define FIRMWARE_BUILD_DATE "23 May 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v1.10"
#define FIRMWARE_VERSION "Meck v1.11"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -100,18 +100,10 @@ struct DiscoveredNode {
bool already_in_contacts; // true if contact was auto-added or already known
};
// Channel invite received via DM -- stored in RAM until accepted/dismissed
#define MAX_PENDING_INVITES 8
// Channel share DM prefix
#define MECK_CH_PREFIX "[MECK:CH]"
#define MECK_CH_PREFIX_LEN 9
struct PendingChannelInvite {
char name[32]; // channel name
uint8_t secret[16]; // channel secret (CIPHER_KEY_SIZE bytes)
char senderName[32]; // who shared it
bool active; // is this slot in use
};
class MyMesh : public BaseChatMesh, public DataStoreHost {
public:
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
@@ -269,48 +261,6 @@ public:
_store->saveMainIdentity(self_id);
}
// --- Pending channel invites (received via DM) ---
int getPendingInviteCount() const {
int count = 0;
for (int i = 0; i < MAX_PENDING_INVITES; i++) {
if (_pendingInvites[i].active) count++;
}
return count;
}
const PendingChannelInvite* getPendingInvite(int idx) const {
int seen = 0;
for (int i = 0; i < MAX_PENDING_INVITES; i++) {
if (_pendingInvites[i].active) {
if (seen == idx) return &_pendingInvites[i];
seen++;
}
}
return nullptr;
}
bool addPendingInvite(const char* name, const uint8_t* secret, const char* senderName) {
for (int i = 0; i < MAX_PENDING_INVITES; i++) {
if (!_pendingInvites[i].active) {
strncpy(_pendingInvites[i].name, name, 31);
_pendingInvites[i].name[31] = '\0';
memcpy(_pendingInvites[i].secret, secret, 16);
strncpy(_pendingInvites[i].senderName, senderName, 31);
_pendingInvites[i].senderName[31] = '\0';
_pendingInvites[i].active = true;
return true;
}
}
return false; // no free slots
}
void removePendingInvite(int idx) {
int seen = 0;
for (int i = 0; i < MAX_PENDING_INVITES; i++) {
if (_pendingInvites[i].active) {
if (seen == idx) { _pendingInvites[i].active = false; return; }
seen++;
}
}
}
private:
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
@@ -336,7 +286,6 @@ private:
mutable bool _forceNextImport = false;
bool _deferSaves = false;
unsigned long _lastUserInput = 0; // millis() of last keypress -- defer saves until idle
PendingChannelInvite _pendingInvites[MAX_PENDING_INVITES]; // RAM-only pending channel shares
uint32_t pending_login;
uint32_t pending_status;
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
@@ -165,7 +165,6 @@ enum SettingsRowType : uint8_t {
ROW_CH_HEADER, // "--- Channels ---" separator
ROW_CHANNEL, // A channel entry (dynamic, index stored separately)
ROW_ADD_CHANNEL, // "+ Add Channel (# = public)"
ROW_PENDING_INVITE, // Pending channel invite (param = invite index)
#ifdef HAS_SDCARD
ROW_EXPORT_IMPORT_SUBMENU, // Folder row: "Export/Import >>"
ROW_EXPORT_TO_SD, // "Export to SD >>" (enters flags sub-screen)
@@ -445,10 +444,6 @@ private:
}
}
addRow(ROW_ADD_CHANNEL);
// Pending channel invites (received via DM)
for (int pi = 0; pi < the_mesh.getPendingInviteCount(); pi++) {
addRow(ROW_PENDING_INVITE, pi);
}
#ifdef MECK_OTA_UPDATE
} else if (_subScreen == SUB_OTA_TOOLS) {
// --- OTA Tools sub-screen ---
@@ -2156,16 +2151,6 @@ public:
display.print(tmp);
break;
case ROW_PENDING_INVITE: {
const PendingChannelInvite* inv = the_mesh.getPendingInvite(_rows[i].param);
if (inv) {
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::YELLOW);
snprintf(tmp, sizeof(tmp), "Pending: %s", inv->name);
display.print(tmp);
}
break;
}
#ifdef MECK_OTA_UPDATE
case ROW_OTA_TOOLS_SUBMENU:
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
@@ -2662,9 +2647,25 @@ public:
ContactInfo ci_info;
if (the_mesh.getContactByIdx(_shareContacts[ci], ci_info)) {
display.setCursor(bx + 4, iy + 1);
if (ci_info.flags & 0x01) {
display.print("* ");
}
display.print(ci_info.name);
}
}
// Scroll indicator
if (_shareContactCount > maxVisible) {
int sbX = bx + bw - 4;
int sbH = listBot - listTop;
display.setColor(DisplayDriver::LIGHT);
display.drawRect(sbX, listTop, 3, sbH);
int thumbH = max(4, (maxVisible * sbH) / _shareContactCount);
int maxScroll = _shareContactCount - maxVisible;
if (maxScroll < 1) maxScroll = 1;
int thumbY = listTop + (_sharePickerScroll * (sbH - thumbH)) / maxScroll;
display.fillRect(sbX + 1, thumbY + 1, 1, thumbH - 2);
}
}
// Footer hint
@@ -2825,7 +2826,9 @@ public:
} else if (_editMode == EDIT_CONFIRM) {
// Footer already covered by overlay
} else {
if (_subScreen != SUB_NONE) {
if (_subScreen == SUB_CHANNELS) {
display.print("Q:Bk C:Share");
} else if (_subScreen != SUB_NONE) {
display.print("Q:Back");
} else {
display.print("Q:Bk");
@@ -3658,34 +3661,6 @@ public:
startEditText("");
break;
case ROW_PENDING_INVITE: {
// Accept pending channel invite
const PendingChannelInvite* inv = the_mesh.getPendingInvite(_rows[_cursor].param);
if (inv) {
ChannelDetails newCh;
memset(&newCh, 0, sizeof(newCh));
strncpy(newCh.name, inv->name, sizeof(newCh.name));
newCh.name[31] = '\0';
memcpy(newCh.channel.secret, inv->secret, 16);
// Find next empty slot
bool added = false;
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails existing;
if (!the_mesh.getChannel(i, existing) || existing.name[0] == '\0') {
if (the_mesh.setChannel(i, newCh)) {
the_mesh.saveChannels();
Serial.printf("Settings: Accepted channel '%s' at idx %d\n", inv->name, i);
added = true;
}
break;
}
}
the_mesh.removePendingInvite(_rows[_cursor].param);
rebuildRows();
}
break;
}
#ifdef MECK_OTA_UPDATE
case ROW_OTA_TOOLS_SUBMENU:
_savedTopCursor = _cursor;
@@ -3775,19 +3750,12 @@ public:
}
// X: delete channel (when on a channel row, idx > 0)
// dismiss pending invite (when on a pending invite row)
if (c == 'x' || c == 'X') {
if (_rows[_cursor].type == ROW_CHANNEL && _rows[_cursor].param > 0) {
_editMode = EDIT_CONFIRM;
_confirmAction = 1;
return true;
}
if (_rows[_cursor].type == ROW_PENDING_INVITE) {
the_mesh.removePendingInvite(_rows[_cursor].param);
rebuildRows();
Serial.println("Settings: dismissed pending channel invite");
return true;
}
}
// N: cycle notification preference (All -> Mentions -> None -> All)
@@ -3804,19 +3772,47 @@ public:
}
}
// S: share channel with a contact via DM
if (c == 's' || c == 'S') {
// C: share channel with a contact via DM (channels sub-screen only)
if ((c == 'c' || c == 'C') && _subScreen == SUB_CHANNELS) {
if (_rows[_cursor].type == ROW_CHANNEL) {
_shareChannelIdx = _rows[_cursor].param;
// Populate contact list with DM-capable contacts
// Populate contact list with DM-capable contacts, favourites first
_shareContactCount = 0;
int numContacts = the_mesh.getNumContacts();
// First pass: favourites
for (int ci = 0; ci < numContacts && _shareContactCount < SHARE_MAX_CONTACTS; ci++) {
ContactInfo contact;
if (the_mesh.getContactByIdx(ci, contact) && contact.type == ADV_TYPE_CHAT) {
if (the_mesh.getContactByIdx(ci, contact) && contact.type == ADV_TYPE_CHAT
&& (contact.flags & 0x01)) {
_shareContacts[_shareContactCount++] = ci;
}
}
int favCount = _shareContactCount;
// Second pass: non-favourites
for (int ci = 0; ci < numContacts && _shareContactCount < SHARE_MAX_CONTACTS; ci++) {
ContactInfo contact;
if (the_mesh.getContactByIdx(ci, contact) && contact.type == ADV_TYPE_CHAT
&& !(contact.flags & 0x01)) {
_shareContacts[_shareContactCount++] = ci;
}
}
// Sort each group alphabetically by name
auto sortRange = [&](int start, int end) {
for (int a = start; a < end - 1; a++) {
for (int b = a + 1; b < end; b++) {
ContactInfo ca, cb;
the_mesh.getContactByIdx(_shareContacts[a], ca);
the_mesh.getContactByIdx(_shareContacts[b], cb);
if (strcasecmp(ca.name, cb.name) > 0) {
int tmp = _shareContacts[a];
_shareContacts[a] = _shareContacts[b];
_shareContacts[b] = tmp;
}
}
}
};
sortRange(0, favCount);
sortRange(favCount, _shareContactCount);
_sharePickerIdx = 0;
_sharePickerScroll = 0;
_editMode = EDIT_SHARE_PICK;
+1 -1
View File
@@ -683,7 +683,7 @@ public:
#endif
y += 10;
display.setColor(DisplayDriver::YELLOW);
display.drawTextCentered(display.width() / 2, y, "[R] Trace [J] Games ");
display.drawTextCentered(display.width() / 2, y, "[R] Trace [J] Games ");
display.setColor(DisplayDriver::LIGHT);
y += 14;
}