feat(ui): flag unread counts understated by a full history ring

DM and channel history each live in a fixed-size ring (32 DM / 48
channel entries). A conversation that outpaces reading can fill its
ring, evicting the oldest entries — including unread ones that were
never opened — so the unread badge silently caps at ring capacity
instead of reflecting how many messages actually arrived.

MessageHistory now tracks a sticky per-channel overflow flag, set at
the exact point an unread channel entry gets evicted (mirroring the
existing unread-decrement logic) and cleared once the count reads back
down to 0. DMUnreadEntry gains the same bit for DM contacts, detected
by comparing the raw per-contact counter against actual ring occupancy
on each new message (the DM ring, unlike the channel ring, doesn't
proactively decrement on eviction).

DisplayDriver::drawUnreadBadge/unreadBadgeWidth/fmtBadgeCount take an
optional overflow flag (default false, existing call sites unchanged)
that appends "+" to the badge. Wired into every place an unread count
is shown: DM/favourites and channel lists, the DM/Channels/Rooms
mode-select screen, favourite tiles, and the aggregate "Msgs" field on
the clock and lock dashboards plus the Messages home-carousel page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-23 13:45:07 +02:00
co-authored by Claude Sonnet 5
parent 26e6bc877a
commit 81cee3846f
7 changed files with 122 additions and 26 deletions
+14 -5
View File
@@ -187,8 +187,16 @@ public:
// Format a small unread count into buf: "1".."99", then "99+". count >= 1.
// No stdio — DisplayDriver.h only pulls stdint/string.
static void fmtBadgeCount(char* buf, int count) {
// overflow: the count itself is honest (how many still-held entries are
// unread) but the underlying ring is capped, and at least one unread entry
// has already been evicted off the end before ever being seen -- there were
// genuinely more than this. Appends "+" (still within the 4-char buffer) so
// the badge doesn't silently understate a permanently-lost backlog as an
// exact count.
static void fmtBadgeCount(char* buf, int count, bool overflow = false) {
if (count > 99) { buf[0]='9'; buf[1]='9'; buf[2]='+'; buf[3]=0; }
else if (overflow && count >= 10) { buf[0]=(char)('0'+count/10); buf[1]=(char)('0'+count%10); buf[2]='+'; buf[3]=0; }
else if (overflow) { buf[0]=(char)('0'+count); buf[1]='+'; buf[2]=0; }
else if (count >= 10) { buf[0]=(char)('0'+count/10); buf[1]=(char)('0'+count%10); buf[2]=0; }
else { buf[0]=(char)('0'+count); buf[1]=0; }
}
@@ -203,8 +211,8 @@ public:
virtual int textWidthTrailingGap() const { return 0; }
// Pixel width the pill from drawUnreadBadge(count) occupies — for reserving
// the name column before it. Mirrors the pill's horizontal padding.
int unreadBadgeWidth(int count) {
char buf[5]; fmtBadgeCount(buf, count);
int unreadBadgeWidth(int count, bool overflow = false) {
char buf[5]; fmtBadgeCount(buf, count, overflow);
int pad = sepH() + 1;
return getTextWidth(buf) - textWidthTrailingGap() + pad * 2;
}
@@ -213,9 +221,10 @@ public:
// knocked out. On a selected/inverted row pass sel=true so the pill inverts
// too (paper capsule + ink digits) and stays visible. The four corners are
// knocked back to the surrounding colour for a rounded-capsule look.
// overflow: see fmtBadgeCount -- appends "+" for a ring-capped, understated count.
// Restores ink to LIGHT. Returns the pill width.
int drawUnreadBadge(int right_x, int y, int count, bool sel) {
char buf[5]; fmtBadgeCount(buf, count);
int drawUnreadBadge(int right_x, int y, int count, bool sel, bool overflow = false) {
char buf[5]; fmtBadgeCount(buf, count, overflow);
int pad = sepH() + 1;
int pw = getTextWidth(buf) - textWidthTrailingGap() + pad * 2;
int ph = getLineHeight();