diff --git a/docs/solo/messages.md b/docs/solo/messages.md index 27c6f0cc..d572696a 100644 --- a/docs/solo/messages.md +++ b/docs/solo/messages.md @@ -144,6 +144,7 @@ correctly whatever the keyboard is set to. | Mark all read | Clears its unread count | | Notif, Melody | Its own notification and sound, instead of the global ones | | Fav | Marks it as a favourite (shared with the app) | +| Save | Wio Tracker L1, with Keep messages on: whether its new messages go on the flash. Off for a busy channel you only read live, so it leaves the room to the others; what's already saved stays until the limit or Delete after removes it | | Scope | The region its messages are tagged with: **Default** follows the list's default, `*` sends none; the list is in Settings › Radio › Scope | | Pin to dial | Puts it on the Favourites page | | Edit, Delete | Renames it or changes its secret; removes it | diff --git a/docs/solo/settings.md b/docs/solo/settings.md index cec247ef..87745609 100644 --- a/docs/solo/settings.md +++ b/docs/solo/settings.md @@ -39,8 +39,9 @@ A few notes: - **Keep messages** (Wio Tracker L1) saves messages on the flash, across a power-off and past what fits in memory (see [How much is kept](./messages.md#how-much-is-kept)): **Off**, 500, 1000 or - 2500 in total. When the limit is reached the oldest go, in batches. Setting - it to Off deletes what was saved. + 2500 in total. When the limit is reached the oldest go, in batches. **Off** + stops saving at once and deletes what was saved at the next start, so turn it + back on before then to keep it. **Delete history** deletes right away. - **Delete after** (Wio Tracker L1) removes saved messages older than 7, 30 or 90 days, or **Never**. They go in batches too, so some stay a little longer, and nothing goes while the clock isn't set. diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index b3e9460f..eaec6168 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -336,6 +336,9 @@ struct NodePrefsFields { // Settings > Messages > Delete after: messages older than this go, from the // list at once (index into contactExpiryDays(); 0 = never). uint8_t msg_expire; + // Channel options > Save: bit i = channel i isn't kept on flash, for one too + // busy to be worth the room [del→onChannelRemoved] + uint64_t ch_no_save; // ui-new's new-message alert: the banner from the top, an envelope with a // count in the corner, or nothing (the sound and the wake still happen). static const uint8_t MSG_ALERT_NORMAL = 0, MSG_ALERT_COMPACT = 1, MSG_ALERT_OFF = 2; @@ -904,6 +907,7 @@ class NodePrefs : public NodePrefsFields, public ConfigSerializer { s.hex("ch_fav", ch_fav_bitmask); s.def("ch_fav_only", ch_fav_only); s.hex("favs", favourite_contacts); s.hex("fav_kinds", favourite_kinds); s.def("fav_sort", fav_sort); s.def("expiry", contact_expiry_idx); s.def("msg_keep", msg_keep); s.def("msg_expire", msg_expire); + s.hex("ch_no_save", ch_no_save); } void gpioSolo(Section& s) { s.def("m1", gpio1_mode); s.def("m2", gpio2_mode); s.def("m3", gpio3_mode); s.def("m4", gpio4_mode); diff --git a/examples/companion_radio/ui-core/ChannelControl.h b/examples/companion_radio/ui-core/ChannelControl.h index fa4801b7..daf8f674 100644 --- a/examples/companion_radio/ui-core/ChannelControl.h +++ b/examples/companion_radio/ui-core/ChannelControl.h @@ -259,6 +259,13 @@ static void setFavourite(NodePrefs* p, uint8_t idx, bool on) { if (on) p->ch_fav_bitmask |= 1ULL << idx; else p->ch_fav_bitmask &= ~(1ULL << idx); } +// Whether new messages of this channel go to the flash log (HistoryLog.h). +static bool saved(const NodePrefs* p, uint8_t idx) { return !p || !(p->ch_no_save & (1ULL << idx)); } +static void setSaved(NodePrefs* p, uint8_t idx, bool on) { + if (!p) return; + if (on) p->ch_no_save &= ~(1ULL << idx); else p->ch_no_save |= 1ULL << idx; +} + // Scope as a picker row: 0 = Default (follow the list's default), then the // list itself from 1 ("*" = unscoped, then the named entries). static uint8_t scope(const NodePrefs* p, uint8_t idx) { @@ -274,8 +281,8 @@ static uint8_t scopeFromRow(int row) { // here, so a channel re-added at a freed slot can't inherit the old one's // settings. Adding such a field: clear it below (and mark it in NodePrefs.h). // Covers bot_channel_idx, loc_share_channel_idx, ch_notif_melody_*, -// ch_notif_override / ch_notif_muted, ch_fav_bitmask, favourite dial slots, -// ch_scope_idx. True when something changed (caller saves). +// ch_notif_override / ch_notif_muted, ch_fav_bitmask, ch_no_save, favourite dial +// slots, ch_scope_idx. True when something changed (caller saves). static bool onRemoved(NodePrefs* p, uint8_t idx) { if (!p) return false; bool changed = false; @@ -286,6 +293,7 @@ static bool onRemoved(NodePrefs* p, uint8_t idx) { if (p->ch_notif_melody_set & m) { p->ch_notif_melody_set &= ~m; p->ch_notif_melody_2 &= ~m; changed = true; } if (p->ch_notif_override & m) { p->ch_notif_override &= ~m; p->ch_notif_muted &= ~m; changed = true; } if (p->ch_fav_bitmask & m) { p->ch_fav_bitmask &= ~m; changed = true; } + if (p->ch_no_save & m) { p->ch_no_save &= ~m; changed = true; } if (idx < NodePrefs::MAX_SCOPED_CHANNELS && p->ch_scope_idx[idx] != NodePrefs::CH_SCOPE_DEFAULT) { p->ch_scope_idx[idx] = NodePrefs::CH_SCOPE_DEFAULT; // back to Default, as a new channel changed = true; diff --git a/examples/companion_radio/ui-core/HistoryLog.h b/examples/companion_radio/ui-core/HistoryLog.h index 7eca47c2..192b1fe6 100644 --- a/examples/companion_radio/ui-core/HistoryLog.h +++ b/examples/companion_radio/ui-core/HistoryLog.h @@ -25,7 +25,9 @@ // A record: Rec, the path, the text, then its own total length as one byte, so // the log can be walked from the end. restore() fills the RAM rings from there. // A channel's messages carry channelKey(), so they follow the channel, not the -// slot; those of a channel since deleted aren't restored. +// slot; those of a channel since deleted aren't restored. A channel with Save +// off (chanctl::saved()) stays out of the log from then on; what it has there +// stays until it ages out. // // Older messages, past what the rings hold, are read a page at a time // (pageBegin*(), pageOlder(), pageNewer()): the messages screen measures its @@ -54,6 +56,8 @@ public: if (_on) { scan(); restore(); + } else if (_fs && _fs->exists(ROOT)) { + forget(); // Keep messages off since the last start, see loop() } _next_prune = millis() + SAVE_DELAY_MS; history->setArchive(this); @@ -61,8 +65,12 @@ public: // ── HistArchive ──────────────────────────────────────────────────────────── // arc_id orders the entries of this run; a restored one keeps 0, already saved. - void chAppend(ChHistEntry& e) override { e.arc_id = ++_seq; appended(); } - void chUpdate(const ChHistEntry&) override { touch(); } + void chAppend(ChHistEntry& e) override { + if (!chanctl::saved(_prefs, e.ch_idx)) { e.arc_id = NOT_SAVED; return; } + e.arc_id = ++_seq; + appended(); + } + void chUpdate(const ChHistEntry& e) override { if (e.arc_id != NOT_SAVED) touch(); } void dmAppend(DmHistEntry& e) override { e.arc_id = ++_seq; appended(); } void dmUpdate(const DmHistEntry&) override { touch(); } @@ -70,7 +78,9 @@ public: bool on = _fs && _prefs && _prefs->msg_keep; if (on != _on) { _on = on; - if (on) touch(); else forget(); + // Off only stops the saving; begin() deletes the log, so a value stepped + // past on the way to another costs nothing, and the wait comes at a start. + if (on) touch(); else _dirty = false; } if (_dirty && (int32_t)(millis() - _due) >= 0) flush(false); if (_on && (int32_t)(millis() - _next_prune) >= 0) { @@ -177,6 +187,9 @@ public: private: static constexpr const char* ROOT = "/h"; + // arc_id of a message that stays in the ring: a channel with Save off. Above + // any _ch_done, so it never counts as in the log (pageBeginCh). + static const uint32_t NOT_SAVED = 0xFFFFFFFFUL; struct __attribute__((packed)) Rec { uint32_t key; // channelKey(), or a DM's 4-byte public key prefix @@ -250,7 +263,7 @@ private: uint32_t now = rtc_clock.getCurrentTime(); for (int j = _history->chHistCount() - 1; j >= 0; j--) { const ChHistEntry& e = _history->chAtPos(_history->chHistPosNewest(j)); - if (e.arc_id <= _ch_done) continue; + if (e.arc_id <= _ch_done || e.arc_id == NOT_SAVED) continue; if (!all && e.relay_status == ACK_PENDING && now - e.timestamp < SETTLE_SECS) { more = true; break; } ChannelDetails ch; if (the_mesh.getChannel(e.ch_idx, ch) && ch.name[0] && diff --git a/examples/companion_radio/ui-core/UiCore.h b/examples/companion_radio/ui-core/UiCore.h index 964c60c1..a0f77e6f 100644 --- a/examples/companion_radio/ui-core/UiCore.h +++ b/examples/companion_radio/ui-core/UiCore.h @@ -15,6 +15,7 @@ #include "MessageHistory.h" #ifdef HIST_FLASH +#include "ChannelControl.h" // chanctl::saved() #include "HistoryLog.h" #endif #include "DmUnreadTable.h" diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index 1f77b4c2..79aa19c1 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -73,6 +73,8 @@ class MessagesScreen : public UIScreen { char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)" char _ctx_fav_item[12]; // "Fav: ON" / "Fav: OFF" — shared by the channel, // contact and room menus (never open at once) + char _ctx_save_item[12]; // "Save: ON" / "Save: OFF" + int _ctx_save_idx = -1; // its row in the channel menu, -1 with nothing to save int _ctx_fav_idx = -1; // row the Fav toggle sits on, frozen at menu open int _ctx_pin_idx = -1; // ...and the Pin row, in the room menu (it sits after Fav, // whose own row depends on whether Logout is shown) @@ -985,6 +987,11 @@ class MessagesScreen : public UIScreen { snprintf(_ctx_fav_item, sizeof(_ctx_fav_item), is_fav ? "Fav: ON" : "Fav: OFF"); _ctx_dirty = true; } + } else if (sel == _ctx_save_idx) { + bool on = !chanctl::saved(_task->getNodePrefs(), ch_idx); + chanctl::setSaved(_task->getNodePrefs(), ch_idx, on); + snprintf(_ctx_save_item, sizeof(_ctx_save_item), on ? "Save: ON" : "Save: OFF"); + _ctx_dirty = true; } } @@ -2365,6 +2372,7 @@ public: if (res == PopupMenu::SELECTED && _num_channels > 0) { uint8_t ch_idx = _ctx_ch_idx; // frozen at menu open — see declaration int sel = _ctx_menu.selectedIndex(); + if (_ctx_save_idx >= 0 && sel > _ctx_save_idx) sel--; // the rows below count without it if (sel == 0) { int cleared = (int)_history.chUnread(ch_idx); _history.setChUnread(ch_idx, 0); @@ -2465,6 +2473,15 @@ public: _ctx_menu.addValueItem(_ctx_melody_item); _ctx_fav_idx = 3; _ctx_menu.addValueItem(_ctx_fav_item); + _ctx_save_idx = -1; +#ifdef HIST_FLASH + if (_task->getNodePrefs() && _task->getNodePrefs()->msg_keep) { // only with the log on + snprintf(_ctx_save_item, sizeof(_ctx_save_item), + chanctl::saved(_task->getNodePrefs(), ch_idx) ? "Save: ON" : "Save: OFF"); + _ctx_save_idx = 4; + _ctx_menu.addValueItem(_ctx_save_item); + } +#endif _ctx_menu.addItem(_ctx_scope_item); _ctx_menu.addItem(_ctx_pin_item); _ctx_menu.addItem("Edit");