feat(nav): share a waypoint as a message

Waypoints list → Hold Enter → Send hands the point to the Messages screen
as "[WAY]<lat>,<lon> <label>" (same text format geo::parseLatLon already
reads). The user picks a contact or channel, the text lands prefilled in
the keyboard to confirm/edit, then sends — closing the loop with the
Navigate / Save waypoint actions on the receiving end.

- QuickMsgScreen: share-compose mode (startShare/beginShareCompose). Picking
  a recipient jumps straight to the prefilled keyboard; cancel returns home;
  afterSend clears the mode.
- UITask::shareToMessage hands off from TrailScreen to the Messages screen.
- TrailScreen: "Send" added to the waypoint Rename/Delete popup; builds the
  [WAY] payload at the {loc} precision (5 dp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-04 11:38:12 +02:00
parent b5f11f430f
commit 75f9ccb34a
5 changed files with 52 additions and 4 deletions

View File

@@ -122,7 +122,9 @@ A waypoint is a saved spot — your car, camp, a water source — that you can n
There is no magnetometer, so the screen shows two *absolute* bearings and you compare them: target at 145°, travelling at 90° → bear right. The **Hdg** line is derived from GPS movement (see Compass) and reads `--` until you move.
**Managing****Hold Enter** on a waypoint row offers **Rename** / **Delete** (the *Trail start* row is navigate-only). **Hold Enter → Clear waypoints** on the Trail screen wipes them all at once.
**Managing****Hold Enter** on a waypoint row offers **Rename** / **Delete** / **Send** (the *Trail start* row is navigate-only). **Hold Enter → Clear waypoints** on the Trail screen wipes them all at once.
**Sharing****Send** hands the waypoint to the Messages screen: pick a contact or channel, and the message is pre-filled as `[WAY]<lat>,<lon> <label>` (e.g. `[WAY]37.42123,-122.08456 CAR`) for you to confirm or edit before sending. On the receiving device, opening that message and **Hold Enter → Navigate / Save waypoint** turns it back into a navigable point (see *Messages Fullscreen message view*). The format is plain text, so it stays readable on other firmware and the phone app.
### Downloading GPX

View File

@@ -72,6 +72,12 @@ class QuickMsgScreen : public UIScreen {
int32_t _nav_lat = 0, _nav_lon = 0;
char _nav_label[24] = "";
// Share-compose mode: launched from elsewhere (e.g. a waypoint) with a
// prepared message; the user picks a recipient and the text lands prefilled
// in the keyboard to confirm/edit before sending.
bool _share_mode = false;
char _share_text[160] = "";
struct ChHistEntry { uint8_t ch_idx; char text[140]; uint32_t timestamp; };
ChHistEntry _hist[CH_HIST_MAX];
int _hist_head, _hist_count;
@@ -135,6 +141,14 @@ class QuickMsgScreen : public UIScreen {
_phase = MSG_PICK;
}
// Recipient chosen while sharing — open the keyboard with the prepared text.
void beginShareCompose(bool channel) {
_sending_to_channel = channel;
_reply_mode = false;
_kb.begin(_share_text);
_phase = KEYBOARD;
}
// Build the fullscreen-message options popup: Reply (if allowed) plus
// Navigate / Save waypoint when `body` carries a location. Opens _ctx_menu
// only when there's at least one action. Parses the location once here and
@@ -274,6 +288,7 @@ class QuickMsgScreen : public UIScreen {
void afterSend(bool ok, const char* msg) {
_reply_mode = false;
_share_mode = false;
if (ok && _sending_to_channel) {
_hist_sel = 0;
_hist_scroll = 0;
@@ -557,6 +572,7 @@ public:
_ctx_menu.active = false;
_ctx_dirty = false;
_nav_active = false;
_share_mode = false;
_pin_picker_active = false;
_dm_direct_entry = false;
_unread_at_entry = 0;
@@ -593,6 +609,17 @@ public:
// Caller must have already reset() the screen. Marks the entry so KEY_CANCEL
// from DM_HIST returns to the home screen instead of falling back through
// CONTACT_PICK → MODE_SELECT.
// Enter the screen pre-loaded to share `text` (e.g. a "[WAY]lat,lon label"
// waypoint). The user picks Direct/Channel then a recipient; selecting one
// jumps straight to the keyboard prefilled with the text (see beginShareCompose).
void startShare(const char* text) {
reset();
_share_mode = true;
strncpy(_share_text, text, sizeof(_share_text) - 1);
_share_text[sizeof(_share_text) - 1] = '\0';
_phase = MODE_SELECT;
}
void enterDM(const ContactInfo& ci) {
_sel_contact = ci;
_task->clearDMUnread(ci.id.pub_key);
@@ -1225,6 +1252,7 @@ public:
_dm_hist_scroll = 0;
_dm_fs.active = false;
_phase = DM_HIST;
if (_share_mode) beginShareCompose(false);
}
return true;
}
@@ -1321,6 +1349,7 @@ public:
_viewing_max_seen = _hist_sel >= 0 ? _hist_sel : 0;
_phase = CHANNEL_HIST;
updateChannelUnread();
if (_share_mode) beginShareCompose(true);
return true;
}
if (c == KEY_CONTEXT_MENU && _num_channels > 0) {
@@ -1506,7 +1535,8 @@ public:
} else if (_phase == KEYBOARD) {
auto res = _kb.handleInput(c);
if (res == KeyboardWidget::CANCELLED) {
_phase = MSG_PICK;
if (_share_mode) { _share_mode = false; _task->gotoHomeScreen(); }
else { _phase = MSG_PICK; }
} else if (res == KeyboardWidget::DONE) {
int prefix_len = _reply_mode ? (int)strlen(_reply_prefix) : 0;
if (_kb.len > prefix_len) {

View File

@@ -119,13 +119,22 @@ public:
_wp_kb.begin(_task->waypoints().at(wi).label, WAYPOINT_LABEL_LEN - 1);
_kb_active = true;
}
} else { // Delete
} else if (sel == 1) { // Delete
if (wi >= 0 && wi < _task->waypoints().count()) {
_task->waypoints().remove(wi);
_task->saveWaypoints();
if (_wp_sel >= wpListCount()) _wp_sel = wpListCount() - 1;
if (_wp_sel < 0) _wp_sel = 0;
}
} else { // Send (share in a message)
if (wi >= 0 && wi < _task->waypoints().count()) {
const Waypoint& w = _task->waypoints().at(wi);
double lat = w.lat_1e6 / 1000000.0, lon = w.lon_1e6 / 1000000.0;
char text[80];
if (w.label[0]) snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f %s", lat, lon, w.label);
else snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
_task->shareToMessage(text); // hands off to the Messages screen
}
}
}
return true;
@@ -147,9 +156,10 @@ public:
if (c == KEY_ENTER && n > 0) { _wp_mode = WP_NAV; return true; }
// Rename/Delete apply to saved waypoints only — not the Trail-start row.
if (c == KEY_CONTEXT_MENU && !selIsStart() && wpIndex() < _task->waypoints().count()) {
_wp_ctx.begin("Waypoint", 2);
_wp_ctx.begin("Waypoint", 3);
_wp_ctx.addItem("Rename");
_wp_ctx.addItem("Delete");
_wp_ctx.addItem("Send");
return true;
}
return true;

View File

@@ -1325,6 +1325,11 @@ void UITask::openContactDM(const ContactInfo& ci) {
setCurrScreen(quick_msg);
}
void UITask::shareToMessage(const char* text) {
((QuickMsgScreen*)quick_msg)->startShare(text);
setCurrScreen(quick_msg);
}
int UITask::getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN], int max) const {
return ((QuickMsgScreen*)quick_msg)->getRecentDMContacts(out, max);
}

View File

@@ -137,6 +137,7 @@ public:
void gotoSettingsScreen();
void gotoQuickMsgScreen();
void openContactDM(const ContactInfo& ci);
void shareToMessage(const char* text); // open Messages pre-loaded to share `text`
int getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN], int max) const;
void gotoToolsScreen();
void gotoRingtoneEditor(int slot = 0);