feat: open a conversation on its first unread message, under a "New" line

Both UIs: a channel, DM or room with unread messages opens on the oldest of
them (MessageHistory::firstUnreadChannel / firstUnreadDm, own messages not
counted), a "New" line above it, so it reads down to the newest. Nothing
unread: the newest, as before.

L1: the first unread is selected and scrolled up top with the line in view;
the channel's unread count now counts down as you read towards the newest.
L2: the page holding it, scrolled to the line; an incoming message scrolls
down only when the list is already at its end. Sim hooks: sim_unread (L1),
sim_open("unread") (L2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-08 22:20:04 +02:00
co-authored by Claude Opus 5.5
parent 5e87686183
commit aaf35351d7
8 changed files with 271 additions and 63 deletions
@@ -413,6 +413,31 @@ public:
return -1;
}
// Where reading resumes on opening a conversation with `unread` unread: the
// newest-first index (as histEntryForChannel / dmHistEntryForContact) of the
// oldest of the `unread` newest received messages -- our own don't count --
// or of the oldest held when some were pushed out. -1 when none is unread.
int firstUnreadChannel(int ch_idx, int unread) const {
int j = 0, last = -1;
for (int i = _hist_count - 1; i >= 0 && unread > 0; i--) {
const ChHistEntry& e = _hist[(_hist_head + i) % CH_HIST_MAX];
if (e.ch_idx != (uint8_t)ch_idx) continue;
last = j++;
if (strncmp(e.text, "Me: ", 4) != 0) unread--;
}
return last;
}
int firstUnreadDm(const uint8_t* prefix, int unread) const {
int j = 0, last = -1;
for (int i = _dm_hist_count - 1; i >= 0 && unread > 0; i--) {
const DmHistEntry& e = _dm_hist[(_dm_hist_head + i) % DM_HIST_MAX];
if (memcmp(e.prefix, prefix, 4) != 0) continue;
last = j++;
if (!e.outgoing) unread--;
}
return last;
}
// Effective status for display. A pending ACK only reads as failed once its
// deadline has passed AND no auto-resends remain — while resends_left > 0 the
// entry stays pending (tickDmResends() retries / finalises it). Safety net for
+22
View File
@@ -224,6 +224,28 @@ public:
if (viewing && pos >= 0) _host->onViewedHistoryGrew(true);
return pos;
}
#ifdef SIM_PLATFORM
// Sim tests: channel 0 and the first chat contact get 12 read messages, then 5 new.
void simUnreadDemo() {
uint32_t now = rtc_clock.getCurrentTime();
static const char* const W[] = { "Alice", "Bob", "Me", "Carol" };
const uint8_t* dm = nullptr;
ContactInfo c;
for (int i = 0; i < the_mesh.getNumContacts() && !dm; i++)
if (the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c) && c.type == ADV_TYPE_CHAT) dm = c.id.pub_key;
for (int i = 0; i < 17; i++) {
char t[48];
snprintf(t, sizeof(t), "%s: message %d", i < 12 ? W[i % 4] : W[i % 2], i + 1);
addChannelMsg(0, t, now - (17 - i) * 120);
if (dm) {
bool out = i < 12 && i % 3 == 2;
if (!out) dm_unread.onIncoming(dm);
addDMMsg(dm, out, strstr(t, ": ") + 2, now - (17 - i) * 120);
}
if (i == 11) { history.setChUnread(0, 0); if (dm) clearDMUnread(dm); }
}
}
#endif
void armChannelRelay(int pos, uint32_t seq) override { history.armChannelRelay(pos, seq); }
void onChannelRelayed(uint32_t seq, const uint8_t* repeater_hash = nullptr, uint8_t hash_size = 0) override {
history.markChannelRelayed(seq, repeater_hash, hash_size);