diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 50363bba..18cc880c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index d12f3e7c..6ab17647 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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 diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 784bcf83..16b08152 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -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; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 603f55b1..41514849 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -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; }