fix(companion): clear stale Locator/Live Share/favourite refs on delete

Deleting a waypoint left the Locator pointed at coordinates that no longer
existed (it's a coordinate snapshot, so nothing noticed). Removing a contact
was worse: nothing cleared its favourite slot, its Locator/Live Share target,
or its per-contact mute/melody entry, so all four kept referencing a pubkey
that no longer resolved to anything.

- WaypointsView's Delete now calls UITask::clearTargetIfWaypoint() first.
- New AbstractUITask::onContactRemoved() hook, called from MyMesh.cpp's
  CMD_REMOVE_CONTACT handler, clears the favourite slot, the Locator target,
  and dm_notif/dm_melody entries for that pubkey. Live Share's DM target
  turns auto-share off instead of guessing a new recipient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-26 17:48:05 +02:00
parent 50cd56cd0c
commit 5c4607cca9
5 changed files with 65 additions and 0 deletions

View File

@@ -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;
};

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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;