mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(l1): older messages from the flash log, Delete after and Delete history
Past the oldest message in a conversation, Up reads the next 12 from the log. Pages, not a longer list: the messages screen measures its whole list every frame. Also from the full-screen view, and for a conversation the ring has lost. Settings > Messages > Delete after hides older messages at once and frees the flash a segment at a time, never with the clock unset or behind the log; Delete history clears the log and the rings. A batch is also written after 12 new messages, as a room's backlog could leave the ring unsaved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,5 +117,5 @@ values above that are set from the phone app or the CLI.
|
||||
| `ADVERT_NAME='"name"'` | Sets the default node name baked into a fresh device, instead of the hex of the first 4 bytes of its public key. Note the doubled quoting — it's a C string literal passed through a build flag. |
|
||||
| `MAP_VECTOR` | Wio Tracker L2: turns on the unfinished vector map (regions from `tools/maps/osm_vector.py`). Without it the vector map stays off whatever was saved. |
|
||||
| `HIST_ARCHIVE` | Wio Tracker L2: message history also kept on the SD card (`ui-lvgl/HistoryStore.h`). Set in its solo build. |
|
||||
| `HIST_FLASH` | Wio Tracker L1: message history also kept on the QSPI flash, across a power-off (`ui-core/HistoryLog.h`, Settings > Messages > Keep messages, off by default). Implies `HIST_ARCHIVE`'s hooks. Set in its solo builds. |
|
||||
| `HIST_FLASH` | Wio Tracker L1: message history also kept on the QSPI flash, across a power-off and past what RAM holds, read back a page at a time (`ui-core/HistoryLog.h`, Settings > Messages > Keep messages, off by default). Implies `HIST_ARCHIVE`'s hooks. Set in its solo builds. |
|
||||
| `MESHCORE_VERSION='"x.y"'` | Overrides the upstream MeshCore base-version string shown in diagnostics, for boards whose port hasn't been rebased onto the latest yet. Cosmetic only — doesn't change protocol behaviour. |
|
||||
|
||||
@@ -45,7 +45,7 @@ build flag it needs.
|
||||
| Channels, direct, rooms | yes | yes | yes | yes | yes, one screen with sections |
|
||||
| Opens on the first unread, "New" line | yes | yes | yes | yes | yes |
|
||||
| Bubbles | 2-line boxes | whole text | whole text | whole text | whole text |
|
||||
| History kept | 48 ch / 32 DM | same; the L1s can keep it on flash across a power-off (`HIST_FLASH`) | same | same | 256 / 128 in memory, all on SD (`HIST_ARCHIVE`) |
|
||||
| History kept | 48 ch / 32 DM | same; the L1s can keep up to 2500 more on flash (`HIST_FLASH`) | same | same | 256 / 128 in memory, all on SD (`HIST_ARCHIVE`) |
|
||||
| Path of a message | Path list | same | same | same | path diagram on the card |
|
||||
| Quick messages, placeholders | yes | yes | yes | yes | yes |
|
||||
| Bytes left while typing | yes | yes | yes | yes | yes |
|
||||
|
||||
@@ -349,6 +349,7 @@ static void sanitize(NodePrefs& p) {
|
||||
flag(p.fav_sort, 1);
|
||||
flag(p.msg_wake, 1);
|
||||
if (p.msg_keep >= NodePrefs::MSG_KEEP_COUNT) p.msg_keep = 0;
|
||||
if (p.msg_expire >= NodePrefs::CONTACT_EXPIRY_COUNT) p.msg_expire = 0;
|
||||
if (p.contact_expiry_idx >= NodePrefs::CONTACT_EXPIRY_COUNT) p.contact_expiry_idx = 0;
|
||||
if (p.dst_rule >= NodePrefs::DST_COUNT) p.dst_rule = NodePrefs::DST_OFF;
|
||||
if (p.display_brightness_pct > 100) p.display_brightness_pct = 0;
|
||||
|
||||
@@ -333,6 +333,9 @@ struct NodePrefsFields {
|
||||
// Settings > Messages > Keep messages, boards with HIST_FLASH: how many
|
||||
// messages in all stay on flash (index into msgKeepTotal(); 0 = off, none).
|
||||
uint8_t msg_keep;
|
||||
// Settings > Messages > Delete after: messages older than this go, from the
|
||||
// list at once (index into contactExpiryDays(); 0 = never).
|
||||
uint8_t msg_expire;
|
||||
// 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;
|
||||
@@ -900,7 +903,7 @@ class NodePrefs : public NodePrefsFields, public ConfigSerializer {
|
||||
s.def("dm_all", dm_show_all); s.def("room_fav", room_fav_only);
|
||||
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("fav_sort", fav_sort); s.def("expiry", contact_expiry_idx); s.def("msg_keep", msg_keep); s.def("msg_expire", msg_expire);
|
||||
}
|
||||
void gpioSolo(Section& s) {
|
||||
s.def("m1", gpio1_mode); s.def("m2", gpio2_mode); s.def("m3", gpio3_mode); s.def("m4", gpio4_mode);
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
// gives no warning) leaves the log as it was before the batch.
|
||||
//
|
||||
// A batch is written SAVE_DELAY_MS after the first change not yet saved -- not
|
||||
// after the last, or a busy channel would never get there -- and at shutdown().
|
||||
// after the last, or a busy channel would never get there -- at shutdown(), and
|
||||
// after SAVE_BURST new messages: a room's backlog can arrive faster than the
|
||||
// delay, and what leaves the ring unsaved is gone.
|
||||
// A record is written once, so a message waits for its final delivery state;
|
||||
// until then the entries behind it wait too, to keep the log in order.
|
||||
//
|
||||
@@ -24,12 +26,24 @@
|
||||
// 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.
|
||||
//
|
||||
// Older messages, past what the rings hold, are read a page at a time
|
||||
// (pageBegin*(), pageOlder(), pageNewer()): the messages screen measures its
|
||||
// whole list every frame, so it gets a short one, MessageHistory::older, never
|
||||
// the log. A page is found by walking the log, as a conversation's records lie
|
||||
// among all the others; a place in it (Spot) stays good as the log grows.
|
||||
//
|
||||
// Settings > Messages > Delete after hides what's older at once; the flash is
|
||||
// freed a whole segment at a time, once the newest record in it is that old.
|
||||
|
||||
class HistoryLog : public HistArchive {
|
||||
public:
|
||||
static const uint8_t SEGMENTS = 8;
|
||||
static const uint32_t SAVE_DELAY_MS = 60000;
|
||||
static const uint32_t SETTLE_SECS = 60; // an own channel post no repeater has echoed yet
|
||||
static const uint8_t SAVE_BURST = 12;
|
||||
static const uint32_t PRUNE_EVERY_MS = 3600000;
|
||||
static const int PAGE = 12;
|
||||
|
||||
void begin(NodePrefs* prefs, MessageHistory* history) {
|
||||
_prefs = prefs;
|
||||
@@ -41,14 +55,15 @@ public:
|
||||
scan();
|
||||
restore();
|
||||
}
|
||||
_next_prune = millis() + SAVE_DELAY_MS;
|
||||
history->setArchive(this);
|
||||
}
|
||||
|
||||
// ── 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; touch(); }
|
||||
void chAppend(ChHistEntry& e) override { e.arc_id = ++_seq; appended(); }
|
||||
void chUpdate(const ChHistEntry&) override { touch(); }
|
||||
void dmAppend(DmHistEntry& e) override { e.arc_id = ++_seq; touch(); }
|
||||
void dmAppend(DmHistEntry& e) override { e.arc_id = ++_seq; appended(); }
|
||||
void dmUpdate(const DmHistEntry&) override { touch(); }
|
||||
|
||||
void loop() {
|
||||
@@ -58,10 +73,108 @@ public:
|
||||
if (on) touch(); else forget();
|
||||
}
|
||||
if (_dirty && (int32_t)(millis() - _due) >= 0) flush(false);
|
||||
if (_on && (int32_t)(millis() - _next_prune) >= 0) {
|
||||
_next_prune = millis() + PRUNE_EVERY_MS;
|
||||
prune(_ch);
|
||||
prune(_dm);
|
||||
}
|
||||
}
|
||||
|
||||
void onShutdown() { if (_dirty && _prefs->msg_keep) flush(true); }
|
||||
|
||||
// Settings > Messages > Delete history: the log and the rings.
|
||||
void erase() {
|
||||
pageEnd();
|
||||
if (_fs) forget();
|
||||
_history->clearAll();
|
||||
}
|
||||
|
||||
// ── Older messages ─────────────────────────────────────────────────────────
|
||||
// Start reading a conversation from just past what the ring holds of it.
|
||||
// False when the log has nothing more; true: a page is in MessageHistory::older.
|
||||
bool pageBeginCh(int slot) {
|
||||
ChannelDetails ch;
|
||||
if (!the_mesh.getChannel(slot, ch) || !ch.name[0]) return false;
|
||||
int in_ring = 0;
|
||||
for (int j = 0; j < _history->chHistCount(); j++) {
|
||||
const ChHistEntry& e = _history->chAtPos(_history->chHistPosNewest(j));
|
||||
if (e.ch_idx == slot && e.arc_id <= _ch_done) in_ring++;
|
||||
}
|
||||
return pageBegin(&_ch, channelKey(ch), (uint8_t)slot, in_ring);
|
||||
}
|
||||
bool pageBeginDm(const uint8_t* prefix) {
|
||||
int in_ring = 0;
|
||||
for (int j = 0; j < _history->dmHistCount(); j++) {
|
||||
const DmHistEntry& e = _history->dmAtPos(_history->dmHistPosNewest(j));
|
||||
if (memcmp(e.prefix, prefix, 4) == 0 && e.arc_id <= _dm_done) in_ring++;
|
||||
}
|
||||
uint32_t key;
|
||||
memcpy(&key, prefix, 4);
|
||||
return pageBegin(&_dm, key, 0, in_ring);
|
||||
}
|
||||
int pageCount() const { return _pg_n; }
|
||||
void pageEnd() {
|
||||
free(_history->older);
|
||||
_history->older = nullptr;
|
||||
_pg = nullptr;
|
||||
_pg_n = 0;
|
||||
}
|
||||
|
||||
// The next page back; false, and the page as it was, at the end of the log.
|
||||
bool pageOlder() {
|
||||
if (!_pg) return false;
|
||||
int got = 0;
|
||||
Spot start = _lo;
|
||||
Spot end = back(start, [&](File& f, uint32_t, uint32_t pos, const Rec& r) {
|
||||
uint8_t buf[REC_MAX];
|
||||
if (readBody(f, pos, r, buf)) fill(got++, r, buf);
|
||||
return got < PAGE;
|
||||
});
|
||||
if (!got) return false;
|
||||
_pg_n = got;
|
||||
_hi = start;
|
||||
_lo = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The next page forward, up to where the ring takes over; false at that point.
|
||||
bool pageNewer() {
|
||||
if (!_pg) return false;
|
||||
int got = 0;
|
||||
Spot first = { 0, 0 }, past = _hi;
|
||||
char p[24];
|
||||
for (uint32_t seg = _hi.seg; seg && seg <= _pg->last && seg <= _ceil.seg && got < PAGE; seg++) {
|
||||
filePath(*_pg, seg, p, sizeof(p));
|
||||
File f = _fs->open(p, FILE_O_READ);
|
||||
if (!f) continue;
|
||||
uint32_t size = f.size(), pos = seg == _hi.seg ? _hi.pos : 0;
|
||||
uint32_t stop = seg == _ceil.seg && _ceil.pos < size ? _ceil.pos : size;
|
||||
while (pos < stop && got < PAGE) {
|
||||
uint8_t buf[REC_MAX];
|
||||
Rec r;
|
||||
if (!f.seek(pos) || f.read(&r, sizeof(r)) != (int)sizeof(r) || !readBody(f, pos, r, buf)) break;
|
||||
uint32_t next = pos + sizeof(r) + pathBytes(r.path_len) + r.text_len + 1;
|
||||
if (mine(r, cutoff())) {
|
||||
if (!got) first = { seg, pos };
|
||||
fill(got++, r, buf);
|
||||
past = { seg, next };
|
||||
}
|
||||
pos = next;
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
if (!got) return false;
|
||||
for (int i = 0, j = got - 1; i < j; i++, j--) { // index 0 is the newest
|
||||
OlderSlot t = _history->older[i];
|
||||
_history->older[i] = _history->older[j];
|
||||
_history->older[j] = t;
|
||||
}
|
||||
_pg_n = got;
|
||||
_lo = first;
|
||||
_hi = past;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr const char* ROOT = "/h";
|
||||
|
||||
@@ -82,12 +195,17 @@ private:
|
||||
uint32_t first, last;
|
||||
uint16_t in_last;
|
||||
};
|
||||
struct Spot { uint32_t seg, pos; }; // a place in a log; seg 0: before its start
|
||||
|
||||
void touch() {
|
||||
if (!_on || _dirty) return;
|
||||
_dirty = true;
|
||||
_due = millis() + SAVE_DELAY_MS;
|
||||
}
|
||||
void appended() {
|
||||
touch();
|
||||
if (_on && ++_unsaved >= SAVE_BURST) _due = millis();
|
||||
}
|
||||
|
||||
static void dirPath(const Log& l, uint32_t seg, char* out, size_t n) {
|
||||
snprintf(out, n, "%s/%c%lu", ROOT, l.kind, (unsigned long)seg);
|
||||
@@ -107,10 +225,19 @@ private:
|
||||
return r.text_len < MSG_TEXT_BUF && len == (int)sizeof(Rec) + pathBytes(r.path_len) + r.text_len + 1;
|
||||
}
|
||||
|
||||
// Timestamps older than this are past Settings > Messages > Delete after (0:
|
||||
// keep all). Never with a clock not set, or behind the log's newest message:
|
||||
// a wrong clock must not cost messages.
|
||||
uint32_t cutoff() const {
|
||||
uint32_t days = NodePrefs::contactExpiryDays(_prefs->msg_expire), now = rtc_clock.getCurrentTime();
|
||||
return days && now >= 1000000000UL && now >= _newest ? now - days * 86400UL : 0;
|
||||
}
|
||||
|
||||
// ── Writing ────────────────────────────────────────────────────────────────
|
||||
void flush(bool all) {
|
||||
uint32_t t0 = millis();
|
||||
_dirty = false;
|
||||
_unsaved = 0;
|
||||
bool more = flushCh(all);
|
||||
if (flushDm(all)) more = true;
|
||||
MESH_DEBUG_PRINTLN("HistoryLog: flush %lu ms", (unsigned long)(millis() - t0));
|
||||
@@ -170,6 +297,7 @@ private:
|
||||
buf[n] = (uint8_t)(n + 1); n++;
|
||||
if ((int)f.write(buf, n) != n) return false;
|
||||
l.in_last++;
|
||||
if (ts > _newest) _newest = ts;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -187,9 +315,22 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Drops the segments that have gone past Delete after, never the newest.
|
||||
void prune(Log& l) {
|
||||
uint32_t cut = cutoff();
|
||||
char p[24];
|
||||
while (cut && l.first && l.first < l.last) {
|
||||
Rec r;
|
||||
if (lastRec(l, l.first, r) && r.timestamp >= cut) break;
|
||||
dirPath(l, l.first++, p, sizeof(p));
|
||||
_fs->rmdir_r(p);
|
||||
}
|
||||
}
|
||||
|
||||
void forget() {
|
||||
_fs->rmdir_r(ROOT);
|
||||
_dirty = false;
|
||||
_newest = 0;
|
||||
_ch = { 'c', 0, 0, 0 };
|
||||
_dm = { 'd', 0, 0, 0 };
|
||||
_ch_done = _dm_done = 0; // on again: what the rings hold goes back in
|
||||
@@ -227,9 +368,25 @@ private:
|
||||
uint8_t len;
|
||||
for (uint32_t pos = f.size(); pos > 0 && stepBack(f, pos, len); pos -= len) n++;
|
||||
f.close();
|
||||
Rec r;
|
||||
if (n && lastRec(l, l.last, r) && r.timestamp > _newest) _newest = r.timestamp;
|
||||
return n;
|
||||
}
|
||||
|
||||
// The last record of a segment.
|
||||
bool lastRec(const Log& l, uint32_t seg, Rec& r) {
|
||||
char p[24];
|
||||
filePath(l, seg, p, sizeof(p));
|
||||
File f = _fs->open(p, FILE_O_READ);
|
||||
if (!f) return false;
|
||||
uint32_t pos = f.size();
|
||||
uint8_t len;
|
||||
bool ok = pos > 0 && stepBack(f, pos, len) && f.seek(pos - len) &&
|
||||
f.read(&r, sizeof(r)) == (int)sizeof(r) && sane(r, len);
|
||||
f.close();
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The record ending at pos: its length, false when that makes no sense.
|
||||
static bool stepBack(File& f, uint32_t pos, uint8_t& len) {
|
||||
return f.seek(pos - 1) && f.read(&len, 1) == 1 && len > sizeof(Rec) && len <= pos;
|
||||
@@ -255,7 +412,7 @@ private:
|
||||
void restoreLog(const Log& l, int want, const uint32_t* keys) {
|
||||
if (!l.last) return;
|
||||
char p[24];
|
||||
uint32_t seg0 = l.last, off0 = 0;
|
||||
uint32_t seg0 = l.last, off0 = 0, cut = cutoff();
|
||||
int got = 0;
|
||||
for (uint32_t seg = l.last; seg >= l.first && got < want; seg--) {
|
||||
filePath(l, seg, p, sizeof(p));
|
||||
@@ -267,7 +424,7 @@ private:
|
||||
pos -= len;
|
||||
Rec r;
|
||||
if (!f.seek(pos) || f.read(&r, sizeof(r)) != (int)sizeof(r) || !sane(r, len)) { pos += len; break; }
|
||||
if (!keys || chSlot(keys, r.key) >= 0) got++;
|
||||
if ((!keys || chSlot(keys, r.key) >= 0) && r.timestamp >= cut) got++;
|
||||
}
|
||||
seg0 = seg;
|
||||
off0 = pos;
|
||||
@@ -285,7 +442,7 @@ private:
|
||||
int rest = pathBytes(r.path_len) + r.text_len + 1;
|
||||
if (r.text_len >= MSG_TEXT_BUF || f.read(buf, rest) != rest) break;
|
||||
if (buf[rest - 1] != (uint8_t)(sizeof(r) + rest)) break;
|
||||
restoreRec(r, buf, keys);
|
||||
if (r.timestamp >= cut) restoreRec(r, buf, keys);
|
||||
pos += sizeof(r) + rest;
|
||||
}
|
||||
f.close();
|
||||
@@ -293,40 +450,121 @@ private:
|
||||
}
|
||||
|
||||
void restoreRec(const Rec& r, const uint8_t* buf, const uint32_t* keys) {
|
||||
int pn = pathBytes(r.path_len);
|
||||
if (keys) {
|
||||
int slot = chSlot(keys, r.key);
|
||||
if (slot < 0) return;
|
||||
ChHistEntry e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
e.ch_idx = (uint8_t)slot;
|
||||
e.timestamp = r.timestamp;
|
||||
e.relay_status = r.flags & STATUS_MASK;
|
||||
e.path_len = r.path_len;
|
||||
memcpy(e.path, buf, pn);
|
||||
memcpy(e.text, buf + pn, r.text_len);
|
||||
fillCh(e, r, buf, (uint8_t)slot);
|
||||
_history->restoreCh(e);
|
||||
} else {
|
||||
DmHistEntry e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
memcpy(e.prefix, &r.key, 4);
|
||||
e.outgoing = (r.flags & OUTGOING) ? 1 : 0;
|
||||
e.timestamp = r.timestamp;
|
||||
e.msg_ts = r.msg_ts;
|
||||
e.ack_status = r.flags & STATUS_MASK;
|
||||
e.path_len = r.path_len;
|
||||
memcpy(e.path, buf, pn);
|
||||
memcpy(e.text, buf + pn, r.text_len);
|
||||
fillDm(e, r, buf);
|
||||
_history->restoreDm(e);
|
||||
}
|
||||
}
|
||||
|
||||
static void fillCh(ChHistEntry& e, const Rec& r, const uint8_t* buf, uint8_t slot) {
|
||||
int pn = pathBytes(r.path_len);
|
||||
memset(&e, 0, sizeof(e));
|
||||
e.ch_idx = slot;
|
||||
e.timestamp = r.timestamp;
|
||||
e.relay_status = r.flags & STATUS_MASK;
|
||||
e.path_len = r.path_len;
|
||||
memcpy(e.path, buf, pn);
|
||||
memcpy(e.text, buf + pn, r.text_len);
|
||||
}
|
||||
static void fillDm(DmHistEntry& e, const Rec& r, const uint8_t* buf) {
|
||||
int pn = pathBytes(r.path_len);
|
||||
memset(&e, 0, sizeof(e));
|
||||
memcpy(e.prefix, &r.key, 4);
|
||||
e.outgoing = (r.flags & OUTGOING) ? 1 : 0;
|
||||
e.timestamp = r.timestamp;
|
||||
e.msg_ts = r.msg_ts;
|
||||
e.ack_status = r.flags & STATUS_MASK;
|
||||
e.path_len = r.path_len;
|
||||
memcpy(e.path, buf, pn);
|
||||
memcpy(e.text, buf + pn, r.text_len);
|
||||
}
|
||||
|
||||
// ── Older messages ─────────────────────────────────────────────────────────
|
||||
bool pageBegin(Log* l, uint32_t key, uint8_t slot, int in_ring) {
|
||||
pageEnd();
|
||||
if (!_on || !l->last) return false;
|
||||
_history->older = (OlderSlot*)malloc(PAGE * sizeof(OlderSlot));
|
||||
if (!_history->older) return false;
|
||||
_pg = l;
|
||||
_pg_key = key;
|
||||
_pg_slot = slot;
|
||||
Spot from = { l->last, 0xFFFFFFFFUL };
|
||||
int skip = in_ring;
|
||||
if (skip > 0) from = back(from, [&](File&, uint32_t, uint32_t, const Rec&) { return --skip > 0; });
|
||||
_ceil = _lo = _hi = from;
|
||||
if (pageOlder()) return true;
|
||||
pageEnd();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mine(const Rec& r, uint32_t cut) const { return r.key == _pg_key && r.timestamp >= cut; }
|
||||
|
||||
// Walks the conversation's records back from `from` until fn says stop:
|
||||
// where it stopped, or the log's start. fn(file, seg, pos, record).
|
||||
template <class F> Spot back(Spot from, F fn) {
|
||||
const Log& l = *_pg;
|
||||
uint32_t cut = cutoff();
|
||||
char p[24];
|
||||
for (uint32_t seg = from.seg; seg && seg >= l.first; seg--) {
|
||||
filePath(l, seg, p, sizeof(p));
|
||||
File f = _fs->open(p, FILE_O_READ);
|
||||
if (!f) continue;
|
||||
uint32_t pos = f.size();
|
||||
if (seg == from.seg && from.pos < pos) pos = from.pos;
|
||||
uint8_t len;
|
||||
while (pos > 0 && stepBack(f, pos, len)) {
|
||||
pos -= len;
|
||||
Rec r;
|
||||
if (!f.seek(pos) || f.read(&r, sizeof(r)) != (int)sizeof(r) || !sane(r, len)) break;
|
||||
if (mine(r, cut) && !fn(f, seg, pos, r)) {
|
||||
f.close();
|
||||
return { seg, pos };
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
return { 0, 0 };
|
||||
}
|
||||
|
||||
static bool readBody(File& f, uint32_t pos, const Rec& r, uint8_t* buf) {
|
||||
int rest = pathBytes(r.path_len) + r.text_len + 1;
|
||||
return r.text_len < MSG_TEXT_BUF && f.seek(pos + sizeof(r)) && f.read(buf, rest) == rest &&
|
||||
buf[rest - 1] == (uint8_t)(sizeof(r) + rest);
|
||||
}
|
||||
|
||||
void fill(int i, const Rec& r, const uint8_t* buf) {
|
||||
OlderSlot& o = _history->older[i];
|
||||
if (_pg == &_ch) {
|
||||
fillCh(o.ch, r, buf, _pg_slot);
|
||||
if (o.ch.relay_status == ACK_PENDING) o.ch.relay_status = ACK_NONE;
|
||||
} else {
|
||||
fillDm(o.dm, r, buf);
|
||||
if (o.dm.ack_status == ACK_PENDING) o.dm.ack_status = ACK_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
NodePrefs* _prefs = nullptr;
|
||||
MessageHistory* _history = nullptr;
|
||||
FILESYSTEM* _fs = nullptr;
|
||||
bool _on = false, _dirty = false;
|
||||
uint32_t _due = 0;
|
||||
uint32_t _due = 0, _next_prune = 0;
|
||||
uint8_t _unsaved = 0;
|
||||
uint32_t _newest = 0; // the newest timestamp in the log
|
||||
uint32_t _seq = 0, _ch_done = 0, _dm_done = 0; // arc_id: the last given, the last saved
|
||||
Log _ch = { 'c', 0, 0, 0 };
|
||||
Log _dm = { 'd', 0, 0, 0 };
|
||||
// The page being read: its log and conversation, where it starts and where
|
||||
// the one after it does, and where the ring takes over from the log.
|
||||
Log* _pg = nullptr;
|
||||
uint32_t _pg_key = 0;
|
||||
uint8_t _pg_slot = 0;
|
||||
int _pg_n = 0;
|
||||
Spot _lo = { 0, 0 }, _hi = { 0, 0 }, _ceil = { 0, 0 };
|
||||
};
|
||||
|
||||
@@ -113,6 +113,11 @@ public:
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef HIST_FLASH
|
||||
// A page of messages read back from the flash log (HistoryLog.h).
|
||||
union OlderSlot { ChHistEntry ch; DmHistEntry dm; };
|
||||
#endif
|
||||
|
||||
// Boards with PSRAM raise these (the L2: -D HIST_CH_MAX=256 -D HIST_DM_MAX=128,
|
||||
// ~80 KB in PSRAM with UiCore); the defaults are sized for the nRF52's heap.
|
||||
#ifndef HIST_CH_MAX
|
||||
@@ -274,8 +279,16 @@ public:
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef HIST_FLASH
|
||||
// A position past the ring is a slot of the page of older messages, for the
|
||||
// messages screen, which reaches both the same way. Allocated while it's open.
|
||||
OlderSlot* older = nullptr;
|
||||
ChHistEntry& chAtPos(int pos) { return pos >= CH_HIST_MAX ? older[pos - CH_HIST_MAX].ch : _hist[pos]; }
|
||||
const ChHistEntry& chAtPos(int pos) const { return pos >= CH_HIST_MAX ? older[pos - CH_HIST_MAX].ch : _hist[pos]; }
|
||||
#else
|
||||
ChHistEntry& chAtPos(int pos) { return _hist[pos]; }
|
||||
const ChHistEntry& chAtPos(int pos) const { return _hist[pos]; }
|
||||
#endif
|
||||
|
||||
// ── Per-channel unread counters ─────────────────────────────────────────────
|
||||
// Always clamped to what the ring still holds for that channel. The raw
|
||||
@@ -541,8 +554,13 @@ public:
|
||||
// Recent DM contacts, newest first, deduped. Resolves the 4-byte _dm_hist
|
||||
// prefix to a 6-byte pub_key prefix by walking the contact list once per
|
||||
// unique sender. Returns the number filled.
|
||||
#ifdef HIST_FLASH
|
||||
DmHistEntry& dmAtPos(int pos) { return pos >= DM_HIST_MAX ? older[pos - DM_HIST_MAX].dm : _dm_hist[pos]; }
|
||||
const DmHistEntry& dmAtPos(int pos) const { return pos >= DM_HIST_MAX ? older[pos - DM_HIST_MAX].dm : _dm_hist[pos]; }
|
||||
#else
|
||||
DmHistEntry& dmAtPos(int pos) { return _dm_hist[pos]; }
|
||||
const DmHistEntry& dmAtPos(int pos) const { return _dm_hist[pos]; }
|
||||
#endif
|
||||
|
||||
// Whole DM ring, newest first (j = 0 newest) -- for "recent conversations".
|
||||
int dmHistCount() const { return _dm_hist_count; }
|
||||
|
||||
@@ -21,7 +21,7 @@ static const Key KEYS[] = {
|
||||
K("autoadd", manual_add_contacts), KB("autoadd.chat", autoadd_config, 0x02), KB("autoadd.repeater", autoadd_config, 0x04),
|
||||
KB("autoadd.room", autoadd_config, 0x08), KB("autoadd.sensor", autoadd_config, 0x10),
|
||||
KB("autoadd.overwrite", autoadd_config, 0x01), K("autoadd.max.hops", autoadd_max_hops),
|
||||
K("contacts.expiry", contact_expiry_idx), K("contacts.fav.first", fav_sort), K("contacts.channel.order", ch_sort), K("map.flow", flow_show), K("dm.resend", dm_resend_count), K("messages.keep", msg_keep),
|
||||
K("contacts.expiry", contact_expiry_idx), K("contacts.fav.first", fav_sort), K("contacts.channel.order", ch_sort), K("map.flow", flow_show), K("dm.resend", dm_resend_count), K("messages.keep", msg_keep), K("messages.expire", msg_expire),
|
||||
K("display.brightness", display_brightness), K("display.off", auto_off_secs), K("display.wake", msg_wake),
|
||||
K("display.alert", msg_alert), K("lock", auto_lock), K("lock.clock", lock_compact),
|
||||
K("display.text.size", ui_text_scale), K("display.icon.size", ui_icon_scale),
|
||||
|
||||
@@ -140,6 +140,9 @@ static void optResend(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
static void optMsgKeep(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
if (v == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%u", (unsigned)NodePrefs::msgKeepTotal(v));
|
||||
}
|
||||
static void optMsgExpire(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
if (v == 0) snprintf(b, n, "Never"); else snprintf(b, n, "%u days", (unsigned)NodePrefs::contactExpiryDays(v));
|
||||
}
|
||||
#endif
|
||||
static void optBattDisplay(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
static const char* L[] = { "Icon", "Percent", "Voltage" };
|
||||
@@ -296,6 +299,8 @@ static const Setting ALL[] = {
|
||||
#ifdef HIST_FLASH
|
||||
IDX("Keep messages", "On flash, after a power-off too", SEC_MESSAGES, msg_keep, NodePrefs::MSG_KEEP_COUNT,
|
||||
optMsgKeep, nullptr),
|
||||
IDX("Delete after", "Messages older than this", SEC_MESSAGES, msg_expire, NodePrefs::CONTACT_EXPIRY_COUNT,
|
||||
optMsgExpire, nullptr),
|
||||
#endif
|
||||
|
||||
IDX("Contact expiry", "Prune after inactivity", SEC_CONTACTS, contact_expiry_idx,
|
||||
|
||||
@@ -110,6 +110,9 @@ public:
|
||||
void clearAllDMUnread() { dm_unread.clearAll(); }
|
||||
// Every conversation read: direct, channels, rooms.
|
||||
void markAllRead() { _room_unread = 0; dm_unread.clearAll(); history.clearAllChannelUnread(); }
|
||||
#ifdef HIST_FLASH
|
||||
void deleteHistory() { history_log.erase(); markAllRead(); } // Settings > Messages > Delete history
|
||||
#endif
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────
|
||||
// Send a DM composed on the device and file it (with end-to-end ACK tracking
|
||||
|
||||
@@ -142,6 +142,7 @@ class MessagesScreen : public UIScreen {
|
||||
|
||||
// DM_HIST view state (the ring itself is in _history).
|
||||
int _dm_hist_sel, _dm_hist_scroll;
|
||||
bool _older = false; // showing a page of the flash log, not the ring
|
||||
FullscreenMsgView _dm_fs;
|
||||
|
||||
// "New" over the first message unread when the conversation opened (index
|
||||
@@ -742,6 +743,7 @@ class MessagesScreen : public UIScreen {
|
||||
}
|
||||
_reply_mode = false;
|
||||
_share_mode = false;
|
||||
dropOlder(); // a reply from an older page: back to the newest
|
||||
if (_sending_to_channel) {
|
||||
_hist_sel = 0;
|
||||
_hist_scroll = 0;
|
||||
@@ -1057,10 +1059,10 @@ public:
|
||||
// relabeling a different one in under them. At sel <= 0 (already at newest,
|
||||
// or -1 == compose button focused) there's nothing to preserve.
|
||||
bool isViewingChannel(uint8_t ch_idx) const {
|
||||
return _phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx;
|
||||
return _phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx && !_older;
|
||||
}
|
||||
bool isViewingDM(const uint8_t* pub_key) const {
|
||||
return _phase == DM_HIST && memcmp(_sel_contact.id.pub_key, pub_key, 4) == 0;
|
||||
return _phase == DM_HIST && memcmp(_sel_contact.id.pub_key, pub_key, 4) == 0 && !_older;
|
||||
}
|
||||
void onViewedHistoryGrew(bool channel) {
|
||||
if (channel) {
|
||||
@@ -1102,6 +1104,73 @@ public:
|
||||
// Open the message history for the currently selected contact/room and reset
|
||||
// the scroll/selection view state. Shared by the Enter-on-contact path and the
|
||||
// post-login auto-enter.
|
||||
// The conversation being shown: the ring's entries for it, or a page of
|
||||
// older ones from the flash log. A list of a few either way, as a render
|
||||
// measures all of it. `_older` is that page.
|
||||
int chViewCount() const {
|
||||
return _older ? olderCount() : _history.histCountForChannel(_sel_channel_idx);
|
||||
}
|
||||
int chViewPos(int j) const {
|
||||
if (_older) return j < olderCount() ? MessageHistory::CH_HIST_MAX + j : -1;
|
||||
return _history.histEntryForChannel(_sel_channel_idx, j);
|
||||
}
|
||||
int dmViewCount() const {
|
||||
return _older ? olderCount() : _history.dmHistCountForContact(_sel_contact.id.pub_key);
|
||||
}
|
||||
int dmViewPos(int j) const {
|
||||
if (_older) return j < olderCount() ? MessageHistory::DM_HIST_MAX + j : -1;
|
||||
return _history.dmHistEntryForContact(_sel_contact.id.pub_key, j);
|
||||
}
|
||||
#ifdef HIST_FLASH
|
||||
int olderCount() const { return _task->core().history_log.pageCount(); }
|
||||
void dropOlder() {
|
||||
if (_older) _task->core().history_log.pageEnd();
|
||||
_older = false;
|
||||
}
|
||||
// Past the oldest entry: the next page back. True when that's handled
|
||||
// (at the very end nothing moves); false with nothing older, as a plain list.
|
||||
bool turnOlder(bool channel) {
|
||||
HistoryLog& log = _task->core().history_log;
|
||||
if (_older) {
|
||||
if (!log.pageOlder()) return true;
|
||||
} else {
|
||||
if (!(channel ? log.pageBeginCh(_sel_channel_idx) : log.pageBeginDm(_sel_contact.id.pub_key))) return false;
|
||||
_older = true;
|
||||
_new_idx = -1;
|
||||
_anchor_new = false;
|
||||
}
|
||||
(channel ? _hist_sel : _dm_hist_sel) = 0;
|
||||
(channel ? _hist_scroll : _dm_hist_scroll) = 0;
|
||||
return true;
|
||||
}
|
||||
// Past the newest of a page: the next one forward, then the ring again.
|
||||
bool turnNewer(bool channel) {
|
||||
if (!_older) return false;
|
||||
int count;
|
||||
if (_task->core().history_log.pageNewer()) {
|
||||
count = olderCount();
|
||||
} else {
|
||||
bool shown = (channel ? _fs : _dm_fs).active;
|
||||
leaveOlder(channel);
|
||||
(channel ? _fs : _dm_fs).active = shown;
|
||||
_new_idx = -1;
|
||||
count = channel ? chViewCount() : dmViewCount();
|
||||
}
|
||||
(channel ? _hist_sel : _dm_hist_sel) = count - 1;
|
||||
(channel ? _hist_scroll : _dm_hist_scroll) = count > _hist_visible ? count - _hist_visible : 0;
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
int olderCount() const { return 0; }
|
||||
void dropOlder() {}
|
||||
bool turnOlder(bool) { return false; }
|
||||
bool turnNewer(bool) { return false; }
|
||||
#endif
|
||||
void leaveOlder(bool channel) {
|
||||
dropOlder();
|
||||
if (channel) beginChannelView(_sel_channel_idx); else beginDmView();
|
||||
}
|
||||
|
||||
void openDmHistory() {
|
||||
beginDmView();
|
||||
_phase = DM_HIST;
|
||||
@@ -1110,6 +1179,7 @@ public:
|
||||
// Opening a conversation: on its first unread message ("New" above it), else
|
||||
// on the compose row under the newest.
|
||||
void beginDmView() {
|
||||
dropOlder();
|
||||
const uint8_t* key = _sel_contact.id.pub_key;
|
||||
_new_idx = _history.firstUnreadDm(key, _task->getDMUnread(key));
|
||||
_anchor_new = _new_idx >= 0;
|
||||
@@ -1119,6 +1189,7 @@ public:
|
||||
_dm_fs.active = false;
|
||||
}
|
||||
void beginChannelView(uint8_t channel_idx) {
|
||||
dropOlder();
|
||||
_sel_channel_idx = channel_idx;
|
||||
_unread_at_entry = (int)_history.chUnread(channel_idx);
|
||||
_unread_base = 0;
|
||||
@@ -1187,7 +1258,7 @@ public:
|
||||
// resulting count down into the store. The ring lives in _history, but this
|
||||
// "what has the user seen on screen" logic is pure phase-machine state.
|
||||
void updateChannelUnread() {
|
||||
if (_sel_channel_idx < 0 || _sel_channel_idx >= MAX_GROUP_CHANNELS) return;
|
||||
if (_sel_channel_idx < 0 || _sel_channel_idx >= MAX_GROUP_CHANNELS || _older) return;
|
||||
// histEntryForChannel is newest-first: index 0 = newest, higher = older;
|
||||
// the unread from opening are _unread_base onwards. Count everything
|
||||
// actually rendered on screen as seen — not just the highlighted row — so
|
||||
@@ -1207,6 +1278,7 @@ public:
|
||||
}
|
||||
|
||||
void reset() {
|
||||
dropOlder();
|
||||
_phase = MODE_SELECT;
|
||||
_mode_sel = 0;
|
||||
_contact_sel = _contact_scroll = 0;
|
||||
@@ -1540,7 +1612,7 @@ public:
|
||||
display.translateUTF8ToBlocks(filtered_name, _sel_contact.name, sizeof(filtered_name));
|
||||
|
||||
if (_dm_fs.active && _dm_hist_sel >= 0) {
|
||||
int ring_pos = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
||||
int ring_pos = dmViewPos(_dm_hist_sel);
|
||||
if (ring_pos >= 0) {
|
||||
const DmHistEntry& e = _history.dmAtPos(ring_pos);
|
||||
char sender_buf[33];
|
||||
@@ -1549,7 +1621,7 @@ public:
|
||||
const char* body = dmDisplayParts(e, _sel_contact.type == ADV_TYPE_ROOM,
|
||||
filtered_name, sender_buf, sizeof(sender_buf));
|
||||
const char* sender = sender_buf;
|
||||
int dm_count = _history.dmHistCountForContact(_sel_contact.id.pub_key);
|
||||
int dm_count = dmViewCount();
|
||||
int ret = _dm_fs.render(display, sender, body,
|
||||
_dm_hist_sel < dm_count - 1,
|
||||
_dm_hist_sel > 0);
|
||||
@@ -1576,7 +1648,7 @@ public:
|
||||
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
||||
drawScreenHeader(display, title, -1, 0, true, _ctx_menu.active);
|
||||
|
||||
int dm_count = _history.dmHistCountForContact(_sel_contact.id.pub_key);
|
||||
int dm_count = dmViewCount();
|
||||
uint32_t now_ts = rtc_clock.getCurrentTime();
|
||||
bool is_room = (_sel_contact.type == ADV_TYPE_ROOM);
|
||||
|
||||
@@ -1605,13 +1677,13 @@ public:
|
||||
return t;
|
||||
};
|
||||
auto bodyAt = [&](int idx, char* sb, size_t sbn) -> const char* {
|
||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, idx);
|
||||
int rp = dmViewPos(idx);
|
||||
if (rp < 0) return nullptr;
|
||||
return skipReplyPrefix(dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name, sb, sbn));
|
||||
};
|
||||
// Height of compact bubble idx at a given scrollbar reserve.
|
||||
auto compactH = [&](int idx, int rsv) {
|
||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, idx);
|
||||
int rp = dmViewPos(idx);
|
||||
if (rp < 0) return lh + 1;
|
||||
char sb[33];
|
||||
const char* body = bodyAt(idx, sb, sizeof(sb));
|
||||
@@ -1651,7 +1723,7 @@ public:
|
||||
for (int ii = 0; ii < MAX_VIS_BOXES && (s + ii) < dm_count; ii++) {
|
||||
int bh = compact_dm ? compactH(s + ii, reserve) : fixed_bh;
|
||||
if (expand) {
|
||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, s + ii);
|
||||
int rp = dmViewPos(s + ii);
|
||||
if (rp >= 0) {
|
||||
char hsb[33];
|
||||
const char* hbody = skipReplyPrefix(dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name, hsb, sizeof(hsb)));
|
||||
@@ -1683,7 +1755,7 @@ public:
|
||||
int y = box_ys[i];
|
||||
int bh = box_hs[i];
|
||||
|
||||
int ring_pos = _history.dmHistEntryForContact(_sel_contact.id.pub_key, item);
|
||||
int ring_pos = dmViewPos(item);
|
||||
if (ring_pos < 0) continue;
|
||||
|
||||
const DmHistEntry& e = _history.dmAtPos(ring_pos);
|
||||
@@ -1780,8 +1852,8 @@ public:
|
||||
|
||||
} else if (_phase == CHANNEL_HIST) {
|
||||
if (_fs.active && _hist_sel >= 0) {
|
||||
int fs_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
||||
int ring_pos = _history.histEntryForChannel(_sel_channel_idx, _hist_sel);
|
||||
int fs_hist_count = chViewCount();
|
||||
int ring_pos = chViewPos(_hist_sel);
|
||||
if (ring_pos >= 0) {
|
||||
const char* ftext = _history.chAtPos(ring_pos).text;
|
||||
const char* fsep = strstr(ftext, ": ");
|
||||
@@ -1834,7 +1906,7 @@ public:
|
||||
}
|
||||
drawScreenHeader(display, title, -1, 0, true, _ctx_menu.active);
|
||||
|
||||
int ch_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
||||
int ch_hist_count = chViewCount();
|
||||
uint32_t now_ts = rtc_clock.getCurrentTime();
|
||||
|
||||
// On a tall enough screen (expandHistory): boxes as tall as their whole
|
||||
@@ -1847,7 +1919,7 @@ public:
|
||||
return computeHistScroll(display, expand, ch_hist_count, _hist_scroll,
|
||||
hist_start_y, cby, lh,
|
||||
[&](int idx) -> const char* {
|
||||
int rp = _history.histEntryForChannel(_sel_channel_idx, idx);
|
||||
int rp = chViewPos(idx);
|
||||
if (rp < 0) return nullptr;
|
||||
const char* t = _history.chAtPos(rp).text;
|
||||
const char* s = strstr(t, ": ");
|
||||
@@ -1868,7 +1940,7 @@ public:
|
||||
for (int ii = 0; ii < MAX_VIS_BOXES && (s + ii) < ch_hist_count; ii++) {
|
||||
int bh = fixed_bh;
|
||||
if (expand) {
|
||||
int rp = _history.histEntryForChannel(_sel_channel_idx, s + ii);
|
||||
int rp = chViewPos(s + ii);
|
||||
if (rp >= 0) {
|
||||
const char* rtext = _history.chAtPos(rp).text;
|
||||
const char* rsep = strstr(rtext, ": ");
|
||||
@@ -1903,7 +1975,7 @@ public:
|
||||
int y = box_ys[i];
|
||||
int bh = box_hs[i];
|
||||
|
||||
int ring_pos = _history.histEntryForChannel(_sel_channel_idx, item);
|
||||
int ring_pos = chViewPos(item);
|
||||
if (ring_pos < 0) continue;
|
||||
|
||||
const char* text = _history.chAtPos(ring_pos).text;
|
||||
@@ -2402,7 +2474,7 @@ public:
|
||||
}
|
||||
|
||||
} else if (_phase == DM_HIST) {
|
||||
int dm_count = _history.dmHistCountForContact(_sel_contact.id.pub_key);
|
||||
int dm_count = dmViewCount();
|
||||
if (_dm_fs.active) {
|
||||
if (_ctx_menu.active) {
|
||||
auto res = _ctx_menu.handleInput(c);
|
||||
@@ -2418,12 +2490,14 @@ public:
|
||||
auto res = _dm_fs.handleInput(c);
|
||||
if (res == FullscreenMsgView::PREV) {
|
||||
if (_dm_hist_sel < dm_count - 1) { _dm_hist_sel++; _dm_fs.scroll = 0; }
|
||||
else if (turnOlder(false)) _dm_fs.scroll = 0;
|
||||
} else if (res == FullscreenMsgView::NEXT) {
|
||||
if (_dm_hist_sel > 0) { _dm_hist_sel--; _dm_fs.scroll = 0; }
|
||||
else if (turnNewer(false)) _dm_fs.scroll = 0;
|
||||
} else if (res == FullscreenMsgView::CLOSE) {
|
||||
_dm_fs.active = false;
|
||||
} else if (res == FullscreenMsgView::REPLY) {
|
||||
int ring_pos = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
||||
int ring_pos = dmViewPos(_dm_hist_sel);
|
||||
if (ring_pos >= 0) {
|
||||
bool reply_ok = !_history.dmAtPos(ring_pos).outgoing;
|
||||
if (reply_ok) buildDmReplyPrefix(_history.dmAtPos(ring_pos));
|
||||
@@ -2443,6 +2517,7 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CANCEL && _older) { leaveOlder(false); return true; }
|
||||
if (c == KEY_CANCEL) {
|
||||
if (_direct_entry) {
|
||||
_direct_entry = false;
|
||||
@@ -2458,12 +2533,14 @@ public:
|
||||
// now walks toward newer (lower index, physically downward) and off the
|
||||
// bottom (index 0) reaches the compose row, which sits right below it.
|
||||
if (c == KEY_UP) {
|
||||
if (dm_count == 0 && turnOlder(false)) return true; // the ring has lost this conversation
|
||||
if (_dm_hist_sel == -1 && dm_count > 0) { _dm_hist_sel = 0; _dm_hist_scroll = 0; }
|
||||
else if (_dm_hist_sel >= 0 && _dm_hist_sel < dm_count - 1) {
|
||||
_dm_hist_sel++;
|
||||
if (_dm_hist_sel >= _dm_hist_scroll + _hist_visible)
|
||||
_dm_hist_scroll = _dm_hist_sel - _hist_visible + 1;
|
||||
} else if (_dm_hist_sel == dm_count - 1) {
|
||||
if (turnOlder(false)) return true;
|
||||
// Oldest (top of the list) -> wrap straight to the newest (bottom
|
||||
// of the list), same ring-wrap every other list in the UI does at
|
||||
// its ends -- the compose row sits below the list itself and is
|
||||
@@ -2478,6 +2555,7 @@ public:
|
||||
_dm_hist_sel--;
|
||||
if (_dm_hist_sel < _dm_hist_scroll) _dm_hist_scroll = _dm_hist_sel;
|
||||
} else if (_dm_hist_sel == 0) {
|
||||
if (turnNewer(false)) return true;
|
||||
_dm_hist_sel = -1;
|
||||
} else if (_dm_hist_sel == -1 && dm_count > 0) {
|
||||
// Compose row -> wrap to the oldest message (top of the list).
|
||||
@@ -2497,7 +2575,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CONTEXT_MENU && _dm_hist_sel >= 0) {
|
||||
int ring_pos = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
||||
int ring_pos = dmViewPos(_dm_hist_sel);
|
||||
if (ring_pos >= 0) {
|
||||
bool reply_ok = !_history.dmAtPos(ring_pos).outgoing;
|
||||
if (reply_ok) buildDmReplyPrefix(_history.dmAtPos(ring_pos));
|
||||
@@ -2507,7 +2585,7 @@ public:
|
||||
}
|
||||
|
||||
} else if (_phase == CHANNEL_HIST) {
|
||||
int ch_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
||||
int ch_hist_count = chViewCount();
|
||||
if (_fs.active) {
|
||||
if (_ctx_menu.active) {
|
||||
auto res = _ctx_menu.handleInput(c);
|
||||
@@ -2523,12 +2601,14 @@ public:
|
||||
auto res = _fs.handleInput(c);
|
||||
if (res == FullscreenMsgView::PREV) {
|
||||
if (_hist_sel < ch_hist_count - 1) { _hist_sel++; _fs.scroll = 0; updateChannelUnread(); }
|
||||
else if (turnOlder(true)) _fs.scroll = 0;
|
||||
} else if (res == FullscreenMsgView::NEXT) {
|
||||
if (_hist_sel > 0) { _hist_sel--; _fs.scroll = 0; updateChannelUnread(); }
|
||||
else if (turnNewer(true)) _fs.scroll = 0;
|
||||
} else if (res == FullscreenMsgView::CLOSE) {
|
||||
_fs.active = false;
|
||||
} else if (res == FullscreenMsgView::REPLY) {
|
||||
int ring_pos = _history.histEntryForChannel(_sel_channel_idx, _hist_sel);
|
||||
int ring_pos = chViewPos(_hist_sel);
|
||||
if (ring_pos >= 0)
|
||||
buildFsMenu(_history.chAtPos(ring_pos).text, buildChannelReplyPrefix(_history.chAtPos(ring_pos).text), ring_pos, true);
|
||||
}
|
||||
@@ -2545,6 +2625,7 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CANCEL && _older) { leaveOlder(true); return true; }
|
||||
if (c == KEY_CANCEL) {
|
||||
if (_direct_entry) { _direct_entry = false; _task->gotoHomeScreen(); }
|
||||
else { enterChannelList(_sel_channel_idx); }
|
||||
@@ -2554,11 +2635,13 @@ public:
|
||||
// render block above) — UP/DOWN swap direction accordingly, same as the
|
||||
// DM history handler above.
|
||||
if (c == KEY_UP) {
|
||||
if (ch_hist_count == 0 && turnOlder(true)) return true; // the ring has lost this channel
|
||||
if (_hist_sel == -1 && ch_hist_count > 0) { _hist_sel = 0; _hist_scroll = 0; }
|
||||
else if (_hist_sel >= 0 && _hist_sel < ch_hist_count - 1) {
|
||||
_hist_sel++;
|
||||
if (_hist_sel >= _hist_scroll + _hist_visible) _hist_scroll = _hist_sel - _hist_visible + 1;
|
||||
} else if (_hist_sel == ch_hist_count - 1) {
|
||||
if (turnOlder(true)) return true;
|
||||
// Oldest -> wrap straight to the newest, same as the DM history handler.
|
||||
_hist_sel = 0;
|
||||
_hist_scroll = 0;
|
||||
@@ -2568,7 +2651,7 @@ public:
|
||||
}
|
||||
if (c == KEY_DOWN) {
|
||||
if (_hist_sel > 0) { _hist_sel--; if (_hist_sel < _hist_scroll) _hist_scroll = _hist_sel; }
|
||||
else if (_hist_sel == 0) _hist_sel = -1;
|
||||
else if (_hist_sel == 0) { if (turnNewer(true)) return true; _hist_sel = -1; }
|
||||
else if (_hist_sel == -1 && ch_hist_count > 0) {
|
||||
// Compose row -> wrap to the oldest message.
|
||||
_hist_sel = ch_hist_count - 1;
|
||||
@@ -2589,7 +2672,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CONTEXT_MENU && _hist_sel >= 0) {
|
||||
int ring_pos = _history.histEntryForChannel(_sel_channel_idx, _hist_sel);
|
||||
int ring_pos = chViewPos(_hist_sel);
|
||||
if (ring_pos >= 0)
|
||||
buildFsMenu(_history.chAtPos(ring_pos).text, buildChannelReplyPrefix(_history.chAtPos(ring_pos).text), ring_pos, true);
|
||||
return true;
|
||||
|
||||
@@ -73,6 +73,9 @@ class SettingsScreen : public UIScreen {
|
||||
// Messages section
|
||||
SECTION_MESSAGES,
|
||||
SCHEMA_MESSAGES,
|
||||
#ifdef HIST_FLASH
|
||||
DELETE_HISTORY,
|
||||
#endif
|
||||
MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4,
|
||||
MSG_SLOT_5, MSG_SLOT_6, MSG_SLOT_7, MSG_SLOT_8, MSG_SLOT_9,
|
||||
Count,
|
||||
@@ -585,6 +588,10 @@ class SettingsScreen : public UIScreen {
|
||||
val("Rooms", (p && p->room_fav_only) ? "Fav" : "All");
|
||||
} else if (item == PRUNE_NOW) {
|
||||
display.print("Prune now"); // action row: Enter counts + confirms + removes
|
||||
#ifdef HIST_FLASH
|
||||
} else if (item == DELETE_HISTORY) {
|
||||
display.print("Delete history"); // action row: Enter confirms + erases
|
||||
#endif
|
||||
} else if (isMsgSlot(item)) {
|
||||
// A message template reads from the left, cut at the edge (scrolls when selected).
|
||||
int slot = msgSlotIndex(item);
|
||||
@@ -994,6 +1001,15 @@ public:
|
||||
// Contacts > "Prune now" confirm
|
||||
if (_prune_confirm.active) {
|
||||
auto res = _prune_confirm.handleInput(c);
|
||||
#ifdef HIST_FLASH
|
||||
if (_selected == DELETE_HISTORY) {
|
||||
if (res == PopupMenu::SELECTED && _prune_confirm.selectedIndex() == 0) { // "Delete"
|
||||
_task->core().deleteHistory();
|
||||
_task->showAlert("History deleted", 1400);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (res == PopupMenu::SELECTED && _prune_confirm.selectedIndex() == 0) { // "Remove"
|
||||
int n = the_mesh.pruneStaleContacts();
|
||||
char msg[24];
|
||||
@@ -1238,6 +1254,12 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#ifdef HIST_FLASH
|
||||
if (_selected == DELETE_HISTORY && enter) {
|
||||
_prune_confirm.beginConfirm("Delete all messages?", "Delete");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (isMsgSlot(_selected) && enter) {
|
||||
int slot = msgSlotIndex(_selected);
|
||||
_edit_slot = slot;
|
||||
|
||||
Reference in New Issue
Block a user