fix(ui): age messages from the device clock, not the phone's timestamp

A DM/channel post sent from the phone app was mirrored into the device history
with the app's timestamp. A phone clock ahead of the device's (or on local
time) made "now - timestamp" negative, which fmtAgeShort clamps to 0, so the
age sat at "0s" until the device clock caught up.

MessageHistory::displayTimestamp(): outgoing entries use the device's own
clock (they were sent just now); incoming entries with an unknown or future
sender timestamp use receipt time. Sender timestamps still drive incoming
dedupe and resends (msg_ts is untouched); an on-air timestamp is unchanged.

Release notes: add this fix, and say "older than v1.27" for the prefs cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-19 13:19:46 +02:00
co-authored by Claude Sonnet 5
parent dff56a3c53
commit d49acc4e55
2 changed files with 18 additions and 4 deletions
@@ -134,7 +134,7 @@ public:
_hist_head = (_hist_head + 1) % CH_HIST_MAX;
}
_hist[pos].ch_idx = ch_idx;
_hist[pos].timestamp = timestamp ? timestamp : rtc_clock.getCurrentTime();
_hist[pos].timestamp = displayTimestamp(timestamp, own_message);
strncpy(_hist[pos].text, text, sizeof(_hist[pos].text) - 1);
_hist[pos].text[sizeof(_hist[pos].text) - 1] = '\0';
_hist[pos].relay_status = ACK_NONE;
@@ -256,6 +256,18 @@ public:
// for incoming); resends = remaining auto-resends for an outgoing pending DM.
// path/path_len_packed: the hop path this incoming DM actually took, or
// nullptr/0 for outgoing (no path concept there -- see DmHistEntry).
// The timestamp an entry's age is measured from, on THIS device's clock.
// An outgoing message was sent just now, so its own send time is simply
// "now" -- taking the phone app's timestamp instead (CMD_SEND_*) put it on
// the phone's clock, and a phone running ahead of the device (or on local
// time) left the age reading "0s" until the device caught up. A sender
// timestamp that is unknown or still in the future is likewise clamped to
// receipt time, so it ages normally from here on instead of sticking at 0s.
static uint32_t displayTimestamp(uint32_t ts, bool outgoing) {
uint32_t now = rtc_clock.getCurrentTime();
return (outgoing || ts == 0 || ts > now) ? now : ts;
}
void storeDMMsg(const uint8_t* pub_key, bool outgoing, const char* text,
uint32_t ack_tag = 0, uint32_t ack_deadline_ms = 0,
uint32_t msg_ts = 0, uint8_t resends = 0,
@@ -273,8 +285,9 @@ public:
// Prefer the sender's own timestamp — a room-sync replay or an
// offline-queued message held by a repeater can arrive long after it was
// actually sent, so "now" would mislabel every backlog message as fresh.
// Fall back to receipt time only when the sender's timestamp is unknown.
_dm_hist[pos].timestamp = msg_ts ? msg_ts : rtc_clock.getCurrentTime();
// Fall back to receipt time when the sender's timestamp is unknown or
// ahead of our clock, and always use our own clock for outgoing.
_dm_hist[pos].timestamp = displayTimestamp(msg_ts, outgoing);
strncpy(_dm_hist[pos].text, text, sizeof(DmHistEntry::text) - 1);
_dm_hist[pos].text[sizeof(DmHistEntry::text) - 1] = '\0';
_dm_hist[pos].ack_status = (outgoing && ack_tag) ? ACK_PENDING : ACK_NONE;
+2 -1
View File
@@ -12,7 +12,8 @@
- **Long messages no longer stretch their chat bubble across the whole row**, which hid which side a message came from. A bubble now always leaves a gutter on the side away from its sender — on the left for your own messages, on the right for received ones — and long text wraps a little earlier to fit.
- **The same channel could be added twice from the device.** "+ Add channel" happily saved a second copy of Public (or of any channel you already had), which is the same channel listed twice with split history and unread counts. Adding a channel whose key already exists is now refused ("Already added" / "Channel already exists"); renaming an existing channel still works. Duplicates you already have are left alone.
- **Upgrading from a very old firmware** (one that predates several past settings-layout changes) no longer runs a chain of one-time migrations on first boot: the few settings they used to backfill now start at today's plain defaults, the same as a fresh device. Current-version upgrades are unaffected.
- **A message sent from the phone app could show "0s" on the device indefinitely.** Its age was measured from the phone's timestamp, so a phone clock ahead of the device's (or on local time) made the age negative and pinned it at 0s until the device's clock caught up. Sent messages now age from the moment the device saw them, and a received message whose timestamp is unknown or ahead of the device clock ages from its receipt time.
- **Upgrading from a firmware older than v1.27** (one that predates several past settings-layout changes) no longer runs a chain of one-time migrations on first boot: the few settings they used to backfill now start at today's plain defaults, the same as a fresh device. Current-version upgrades are unaffected.
- **Heltec V3/V4 with a wired joystick: Enter felt laggy and unreliable.** The Enter contact had no internal pull-up and buffered every click for ~280 ms waiting for a double/triple click that the joystick code ignores, so a quick double-tap was silently dropped. It's now wired like the direction and Back contacts. Boards without a joystick are unaffected.
- **Every altitude reading (baro + GPS, Clock and Lock screen) and the GPS home page's "alt" row now respect Settings System Units**, same as every other distance in the UI — none of them checked metric/imperial before. Locator's Radius row had the same gap and is fixed the same way.
- **The Radio home page's noise floor showed "n/a" whenever Pwr save (RX duty-cycle) was on**, on the assumption duty-cycle RX can't sample it. That's stale — the radio already recalibrates it periodically even under duty-cycle, which is why Diagnostics' own reading was already showing something real. Radio home page now matches.