diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 6424dfc7..88bc0fec 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -73,5 +73,9 @@ public: virtual void onSharedLocation(const uint8_t* pub_key, const char* name, int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, bool verified) {} + // A contact was removed (companion app / CLI command). Lets UI state that + // references contacts by pubkey (favourite slots, the Locator/Live Share + // target) drop a reference that would otherwise dangle. Default no-op. + virtual void onContactRemoved(const uint8_t* pub_key) {} virtual void loop() = 0; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 44575f70..1288aa6f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1951,6 +1951,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (recipient && removeContact(*recipient)) { _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); forgetRoomPassword(pub_key); // drop any saved room login -- useless without the contact + if (_ui) _ui->onContactRemoved(pub_key); // drop any favourite slot / Locator / Live Share target pointed at it dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); writeOKFrame(); } else { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 51030eaf..c778b209 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2399,6 +2399,52 @@ void UITask::clearTarget() { resetLocator(); } +void UITask::clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6) { + if (!_node_prefs || !_node_prefs->locator_has_target || _node_prefs->locator_target_kind != 0) return; + if (_node_prefs->locator_lat_1e6 != lat_1e6 || _node_prefs->locator_lon_1e6 != lon_1e6) return; + clearTarget(); + the_mesh.savePrefs(); +} + +void UITask::onContactRemoved(const uint8_t* pub_key) { + if (!_node_prefs || !pub_key) return; + bool changed = false; + + int slot = findFavouriteSlot(pub_key); + if (slot >= 0) { clearFavouriteSlot(slot); changed = true; } + + if (_node_prefs->locator_has_target && _node_prefs->locator_target_kind == 1 + && memcmp(_node_prefs->locator_key, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) { + clearTarget(); + changed = true; + } + // Fail closed rather than guess a new recipient: a contact target that's + // gone just turns auto-share off, it doesn't fall back to some other target. + if (_node_prefs->loc_share_target_type == 1 + && memcmp(_node_prefs->loc_share_dm_prefix, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) { + _node_prefs->loc_share_enabled = 0; + changed = true; + } + // Per-contact mute/melody overrides — only 16 slots shared across every + // contact, so an orphaned entry isn't just stale, it can eventually starve + // new overrides for contacts that still exist. Keyed by a 4-byte prefix + // (narrower than the 6-byte one above), so compare only that many bytes. + for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++) { + if (_node_prefs->dm_notif[i].state && memcmp(_node_prefs->dm_notif[i].prefix, pub_key, 4) == 0) { + memset(&_node_prefs->dm_notif[i], 0, sizeof(_node_prefs->dm_notif[i])); + changed = true; + } + } + for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) { + if (_node_prefs->dm_melody[i].slot && memcmp(_node_prefs->dm_melody[i].prefix, pub_key, 4) == 0) { + memset(&_node_prefs->dm_melody[i], 0, sizeof(_node_prefs->dm_melody[i])); + changed = true; + } + } + + if (changed) the_mesh.savePrefs(); +} + // Homing beeper: while armed with a target and inside the radius, emit a short // tick whose interval shrinks linearly with distance — slow at the edge, rapid // near the centre. Polls distance a few times a second; silent outside the diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 39908e85..38a4c2f7 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -207,6 +207,18 @@ public: // Unset the active target (locator_has_target = 0). Distinct from setTarget() // because there's no "kind" for nothing — clearing is its own operation. void clearTarget(); + // One-shot: if the active target is exactly this waypoint, clear it and + // persist immediately (setTargetNow()'s save-on-the-spot policy) — called + // from waypoint deletion so the Locator can't keep pointing at a spot + // that no longer exists. + void clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6); + // A contact was removed (companion app / CLI): drop any UI reference to its + // pubkey that would otherwise dangle — a pinned favourite slot, the Locator + // target if it was this contact, the Live Share target if it was this + // contact (auto-share turns off rather than guessing a new recipient), and + // any per-contact mute/melody override (those tables have only 16 shared + // slots, so an orphan isn't just stale — it can starve other contacts). + void onContactRemoved(const uint8_t* pub_key) override; // Resolve a person target (6-byte pubkey prefix) to a current position: // prefers an active [LOC] live share, falls back to their last-advertised // GPS fix. Returns false when neither is known. Optional live/ts report diff --git a/examples/companion_radio/ui-new/WaypointsView.h b/examples/companion_radio/ui-new/WaypointsView.h index ab66f71f..76e4b95d 100644 --- a/examples/companion_radio/ui-new/WaypointsView.h +++ b/examples/companion_radio/ui-new/WaypointsView.h @@ -269,6 +269,8 @@ public: } } else if (sel == 1) { // Delete if (wi >= 0 && wi < _task->waypoints().count()) { + const Waypoint& w = _task->waypoints().at(wi); + _task->clearTargetIfWaypoint(w.lat_1e6, w.lon_1e6); // don't leave the Locator pointed at a deleted spot _task->waypoints().remove(wi); _task->saveWaypoints(); if (_sel >= wpListCount()) _sel = wpListCount() - 1;