feat(companion): live location sharing via [LOC] messages

Share and track positions over the mesh through chat messages, separate
from (and parallel to) the 0-hop auto-advert.

Receive / tracking:
- geo::parseLocShare + [LOC] tag (sibling to [WAY]); LiveTrackStore (16
  slots, DM=pubkey/verified, channel=name/best-effort, 20-min expiry).
- MyMesh parses [LOC] on DM + channel receive → UITask live-track table.
- NearbyScreen overlays shares onto contacts (fresher pin) and lists
  non-contact senders; markers (*=DM, ~=chan) + "Loc:" age/verified line.

Send:
- Map menu "Share my pos" (one-shot) → Messages recipient picker.
- New Tools > Live Share tool: Receive (Track loc) + Send (Auto, Target,
  Move/Min gap/Heartbeat, Share now). Target chosen via the Messages
  picker (channel/DM). Movement-gated auto-send engine in UITask loop.

Map / navigation:
- Trail map shows tracked contacts as ◆ markers (ICON_MAP_CONTACT).
- Home "Map" page: mini-map preview + status line; ENTER opens the tool,
  Cancel returns Home.

Prefs: track_shared_loc + loc_share_* (NodePrefs schema 0xC0DE0012).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-22 10:13:09 +02:00
co-authored by Claude Opus 4.8
parent 29deaaad44
commit d78e74d4cd
14 changed files with 816 additions and 21 deletions
@@ -84,6 +84,9 @@ class QuickMsgScreen : public UIScreen {
// in the keyboard to confirm/edit before sending.
bool _share_mode = false;
char _share_text[160] = "";
// Live-share target picker: reuse the recipient chooser to set the persistent
// auto-share target (channel / DM) instead of composing a message.
bool _pick_target = false;
// History text holds a full received message. Channel messages carry the
// sender embedded as "Name: body" in the payload, so a message can be up to
@@ -807,6 +810,7 @@ public:
_ctx_dirty = false;
_nav_active = false;
_share_mode = false;
_pick_target = false;
_pin_picker_active = false;
_dm_direct_entry = false;
_unread_at_entry = 0;
@@ -854,6 +858,38 @@ public:
_phase = MODE_SELECT;
}
// Open the recipient chooser to set the live-share target (channel/DM). On
// selection the target is stored in NodePrefs and the Map screen is restored.
void startPickTarget() {
reset();
_pick_target = true;
_phase = MODE_SELECT;
}
void commitPickTargetChannel(int ch_idx) {
NodePrefs* p = _task->getNodePrefs();
if (p) {
p->loc_share_target_type = 0;
p->loc_share_channel_idx = (uint8_t)ch_idx;
the_mesh.savePrefs();
}
_pick_target = false;
_task->showAlert("Share target set", 1200);
_task->gotoLiveShareScreen();
}
void commitPickTargetDM(const ContactInfo& ci) {
NodePrefs* p = _task->getNodePrefs();
if (p) {
p->loc_share_target_type = 1;
memcpy(p->loc_share_dm_prefix, ci.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN);
the_mesh.savePrefs();
}
_pick_target = false;
_task->showAlert("Share target set", 1200);
_task->gotoLiveShareScreen();
}
void enterDM(const ContactInfo& ci) {
_sel_contact = ci;
_task->clearDMUnread(ci.id.pub_key);
@@ -1465,6 +1501,7 @@ public:
if (c == KEY_DOWN && _num_contacts > 0) { _contact_sel = (_contact_sel < _num_contacts - 1) ? _contact_sel + 1 : 0; return true; }
if (c == KEY_ENTER && _num_contacts > 0) {
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
if (_pick_target) { commitPickTargetDM(_sel_contact); return true; }
_task->clearDMUnread(_sel_contact.id.pub_key);
_dm_hist_sel = -1;
_dm_hist_scroll = 0;
@@ -1555,6 +1592,7 @@ public:
if (c == KEY_DOWN && _num_channels > 0) { _channel_sel = (_channel_sel < _num_channels - 1) ? _channel_sel + 1 : 0; return true; }
if (c == KEY_ENTER && _num_channels > 0) {
_sel_channel_idx = _channel_indices[_channel_sel];
if (_pick_target) { commitPickTargetChannel(_sel_channel_idx); return true; }
int hc = histCountForChannel(_sel_channel_idx);
_unread_at_entry = (int)_ch_unread[_sel_channel_idx];
_hist_scroll = 0;