mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3620,22 +3620,33 @@ void UITask::setKeyboardVisible(bool show) {
|
||||
}
|
||||
}
|
||||
|
||||
static const int THREAD_MAX_SHOWN = 50; // newest bubbles built per conversation
|
||||
|
||||
// Opens on the first unread (newest-first index `first`, -1: none), "New"
|
||||
// above it: the page that holds it, a few read ones above.
|
||||
void UITask::threadOpenAt(int first) {
|
||||
_thread_new = first;
|
||||
_thread_new_on = false;
|
||||
_thread_skip = first < THREAD_MAX_SHOWN ? 0 : first - (THREAD_MAX_SHOWN - 4);
|
||||
}
|
||||
|
||||
void UITask::openChannel(uint8_t channel_idx) {
|
||||
_thread_is_channel = true;
|
||||
_thread_channel = channel_idx;
|
||||
_thread_skip = 0;
|
||||
_core->history.setChUnread(channel_idx, 0);
|
||||
MessageHistory& h = _core->history;
|
||||
threadOpenAt(h.firstUnreadChannel(channel_idx, h.chUnread(channel_idx)));
|
||||
h.setChUnread(channel_idx, 0);
|
||||
_screen = SCR_THREAD;
|
||||
buildThread();
|
||||
}
|
||||
|
||||
void UITask::openDM(const uint8_t* pub_key) {
|
||||
_thread_is_channel = false;
|
||||
_thread_skip = 0;
|
||||
memset(_thread_key, 0, sizeof(_thread_key));
|
||||
ContactInfo c;
|
||||
if (MessageHistory::contactByPrefix(pub_key, c)) memcpy(_thread_key, c.id.pub_key, PUB_KEY_SIZE);
|
||||
else memcpy(_thread_key, pub_key, 4);
|
||||
threadOpenAt(_core->history.firstUnreadDm(_thread_key, _core->dmUnread(_thread_key)));
|
||||
_core->clearDMUnread(_thread_key);
|
||||
_screen = SCR_THREAD;
|
||||
buildThread();
|
||||
@@ -3742,7 +3753,6 @@ uint32_t UITask::threadSignature() const {
|
||||
// Positions found in the shown messages ([WAY] / [LOC] / plain "lat,lon"),
|
||||
// for the Go / Save buttons under such a bubble.
|
||||
struct MsgLoc { int32_t lat, lon; char label[WAYPOINT_LABEL_LEN * 2]; };
|
||||
static const int THREAD_MAX_SHOWN = 50; // newest bubbles built per conversation
|
||||
static MsgLoc* s_msg_locs = psramBuf<MsgLoc>(THREAD_MAX_SHOWN);
|
||||
static int s_msg_loc_n = 0;
|
||||
|
||||
@@ -3765,6 +3775,28 @@ struct ThreadRow { uint32_t key, state; lv_obj_t* row; lv_obj_t* age; };
|
||||
static ThreadRow* s_th_rows = psramBuf<ThreadRow>(THREAD_MAX_SHOWN);
|
||||
static lv_obj_t* s_older_lbl = nullptr; // "Older messages (n)" on the page's top
|
||||
static bool s_th_rows_newest = false; // built for the newest page (the one patched)
|
||||
static lv_obj_t* s_new_div = nullptr; // "New" above the first unread
|
||||
|
||||
// The line with "New" over the first message unread when the thread opened.
|
||||
static lv_obj_t* newDivider(lv_obj_t* list) {
|
||||
lv_obj_t* d = lv_obj_create(list);
|
||||
lv_obj_remove_style_all(d);
|
||||
lv_obj_set_size(d, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(d, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(d, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(d, 6, 0);
|
||||
lv_obj_remove_flag(d, LV_OBJ_FLAG_CLICKABLE);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (i == 1) { label(d, "New", THEME_FONT_SMALL, theme::ACCENT); continue; }
|
||||
lv_obj_t* l = lv_obj_create(d);
|
||||
lv_obj_remove_style_all(l);
|
||||
lv_obj_set_height(l, 1);
|
||||
lv_obj_set_flex_grow(l, 1);
|
||||
lv_obj_set_style_bg_color(l, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_set_style_bg_opa(l, LV_OPA_COVER, 0);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
// The shown message a bubble (or a button in it) belongs to, or -1.
|
||||
static int threadRowOf(lv_obj_t* o) {
|
||||
@@ -4134,7 +4166,11 @@ void UITask::refreshThread() {
|
||||
drop--;
|
||||
}
|
||||
if (kept > 0) {
|
||||
for (int r = 0; r < drop; r++) lv_obj_delete(s_th_rows[r].row);
|
||||
bool at_end = lv_obj_get_scroll_bottom(_thread_list) <= theme::PAD; // not when reading further up
|
||||
for (int r = 0; r < drop; r++) {
|
||||
if (s_new_div && s_th_rows[r].key == _thread_new_key) { lv_obj_delete(s_new_div); s_new_div = nullptr; }
|
||||
lv_obj_delete(s_th_rows[r].row);
|
||||
}
|
||||
memmove(s_th_rows, s_th_rows + drop, kept * sizeof(ThreadRow));
|
||||
memmove(s_msg_meta, s_msg_meta + drop, kept * sizeof(MsgMeta));
|
||||
for (int r = 0; r < kept; r++) {
|
||||
@@ -4155,7 +4191,7 @@ void UITask::refreshThread() {
|
||||
for (int r = kept; r < n; r++) build(r);
|
||||
if (s_older_lbl) { char t[40]; olderText(t, sizeof(t)); setText(s_older_lbl, t); }
|
||||
s_th_rows_n = s_msg_meta_n = s_msg_loc_n = n;
|
||||
if (n > kept) { // new messages: down to them
|
||||
if (n > kept && at_end) { // new messages: down to them
|
||||
layoutNow(_thread_list);
|
||||
lv_obj_scroll_to_y(_thread_list, LV_COORD_MAX, LV_ANIM_OFF);
|
||||
}
|
||||
@@ -4171,13 +4207,20 @@ void UITask::refreshThread() {
|
||||
pageButton(_thread_list, t, 1);
|
||||
s_older_lbl = lv_obj_get_child(lv_obj_get_child(_thread_list, -1), 0);
|
||||
}
|
||||
for (int i = 0; i < n; i++) build(i); // oldest first
|
||||
s_new_div = nullptr;
|
||||
for (int i = 0; i < n; i++) { // oldest first
|
||||
if (_thread_new >= 0 && _thread_skip + n - 1 - i == _thread_new) { _thread_new_key = keyOf(i); _thread_new_on = true; }
|
||||
if (_thread_new_on && !s_new_div && keyOf(i) == _thread_new_key && (i > 0 || s_older_lbl)) s_new_div = newDivider(_thread_list);
|
||||
build(i);
|
||||
}
|
||||
_thread_new = -1;
|
||||
s_th_rows_n = s_msg_meta_n = s_msg_loc_n = n;
|
||||
s_th_rows_newest = _thread_skip == 0; // a page back is built whole each time
|
||||
if (_thread_skip > 0) pageButton(_thread_list, LV_SYMBOL_DOWN " Newer messages", -1);
|
||||
if (n == 0) label(_thread_list, "No messages yet", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||
layoutNow(_thread_list);
|
||||
lv_obj_scroll_to_y(_thread_list, _thread_scroll_top ? 0 : LV_COORD_MAX, LV_ANIM_OFF);
|
||||
if (s_new_div && !_thread_scroll_top) lv_obj_scroll_to_view(s_new_div, LV_ANIM_OFF); // from the bottom: "New" on top
|
||||
_thread_scroll_top = false;
|
||||
}
|
||||
|
||||
@@ -4673,6 +4716,11 @@ void UITask::simDm() {
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::simUnread() {
|
||||
_core->simUnreadDemo();
|
||||
if (_screen == SCR_CHATS) refreshChats();
|
||||
}
|
||||
|
||||
// Sim tests: straight to a screen by name.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||
if (!s_ui) return;
|
||||
@@ -4697,6 +4745,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||
if (!strncmp(name, "compose@", 8)) { s_ui->simCompose(name + 8); return; }
|
||||
if (!strcmp(name, "dm")) { s_ui->simDm(); return; }
|
||||
if (!strcmp(name, "send")) { s_ui->simSend(); return; }
|
||||
if (!strcmp(name, "unread")) { s_ui->simUnread(); return; }
|
||||
if (!strcmp(name, "vector")) { s_ui->setVectorMap(true); return; }
|
||||
if (!strcmp(name, "maptools")) { s_ui->navToolsPopup(); return; }
|
||||
if (!strcmp(name, "areasel")) { s_ui->areaSelectBegin(); return; }
|
||||
|
||||
@@ -130,6 +130,7 @@ public:
|
||||
void simMapAt(const char* spec); // Map tools > Live tiles
|
||||
void simCompose(const char* text); // the open thread's field: this text, keyboard up
|
||||
void simDm(); // the first chat contact's thread
|
||||
void simUnread(); // read and new messages to open on
|
||||
void simSend() { sendFromCompose(); } // the field, sent
|
||||
// Navigation map (NavMap.h)
|
||||
void navTargetsPopup();
|
||||
@@ -425,6 +426,7 @@ private:
|
||||
void buildNode();
|
||||
void refreshNode();
|
||||
void buildThread();
|
||||
void threadOpenAt(int first);
|
||||
void refreshThread();
|
||||
void refreshThreadAges();
|
||||
uint32_t threadSignature() const;
|
||||
@@ -539,6 +541,9 @@ private:
|
||||
bool _thread_is_channel = false;
|
||||
int _thread_skip = 0; // newest messages above the page shown (Older / Newer)
|
||||
bool _thread_scroll_top = false;
|
||||
int _thread_new = -1; // on opening: newest-first index of the first unread (-1: none)
|
||||
uint32_t _thread_new_key = 0; // and that message, marked with "New" while the thread is open
|
||||
bool _thread_new_on = false;
|
||||
int loadThreadPage(int& total);
|
||||
uint8_t _thread_channel = 0;
|
||||
uint8_t _thread_key[PUB_KEY_SIZE] = {0};
|
||||
|
||||
@@ -47,7 +47,8 @@ class MessagesScreen : public UIScreen {
|
||||
int _hist_sel, _hist_scroll;
|
||||
FullscreenMsgView _fs;
|
||||
int _unread_at_entry; // channel unread count when entering CHANNEL_HIST
|
||||
int _viewing_max_seen; // highest _hist_sel reached in current session
|
||||
int _unread_base = 0; // messages filed since: the unread are indices base.. base+count-1
|
||||
int _seen_lo = 0, _seen_hi = -1; // the indices shown so far this session
|
||||
|
||||
// KEYBOARD
|
||||
KeyboardWidget* _kb;
|
||||
@@ -143,6 +144,11 @@ class MessagesScreen : public UIScreen {
|
||||
int _dm_hist_sel, _dm_hist_scroll;
|
||||
FullscreenMsgView _dm_fs;
|
||||
|
||||
// "New" over the first message unread when the conversation opened (index
|
||||
// as _hist_sel / _dm_hist_sel, -1: none); the first render scrolls it up top.
|
||||
int _new_idx = -1;
|
||||
bool _anchor_new = false;
|
||||
|
||||
int _hist_visible = 2; // updated in render(); for history list scroll clamping
|
||||
|
||||
void expandMsg(const char* tmpl, char* out, int out_len) const { // ui-core/MessageText.h
|
||||
@@ -681,7 +687,6 @@ class MessagesScreen : public UIScreen {
|
||||
// Filed at index 0 by sendText(): the unread index range is stale, and
|
||||
// the user is active in this channel -- fully read.
|
||||
_unread_at_entry = 0;
|
||||
_viewing_max_seen = 0;
|
||||
} else {
|
||||
_dm_hist_sel = 0;
|
||||
_dm_hist_scroll = 0;
|
||||
@@ -969,7 +974,7 @@ public:
|
||||
_sel_channel_idx(0), _sending_to_channel(false),
|
||||
_msg_sel(0), _msg_scroll(0), _active_msg_count(0),
|
||||
_hist_sel(0), _hist_scroll(0),
|
||||
_unread_at_entry(0), _viewing_max_seen(0),
|
||||
_unread_at_entry(0),
|
||||
_history(task->core().history),
|
||||
_dm_hist_sel(-1), _dm_hist_scroll(0),
|
||||
_ctx_dirty(false), _pin_picker_active(false), _direct_entry(false), _reply_mode(false),
|
||||
@@ -1003,8 +1008,14 @@ public:
|
||||
return _phase == DM_HIST && memcmp(_sel_contact.id.pub_key, pub_key, 4) == 0;
|
||||
}
|
||||
void onViewedHistoryGrew(bool channel) {
|
||||
if (channel) { if (_hist_sel > 0) { _hist_sel++; _hist_scroll++; } }
|
||||
else { if (_dm_hist_sel > 0) { _dm_hist_sel++; _dm_hist_scroll++; } }
|
||||
if (channel) {
|
||||
if (_hist_sel > 0) { _hist_sel++; _hist_scroll++; }
|
||||
_unread_base++;
|
||||
if (_seen_hi >= 0) { _seen_lo++; _seen_hi++; }
|
||||
} else {
|
||||
if (_dm_hist_sel > 0) { _dm_hist_sel++; _dm_hist_scroll++; }
|
||||
}
|
||||
if (_new_idx >= 0) _new_idx++;
|
||||
}
|
||||
|
||||
// Room logins (who is logged in this session, the saved passwords, the one
|
||||
@@ -1037,11 +1048,56 @@ public:
|
||||
// the scroll/selection view state. Shared by the Enter-on-contact path and the
|
||||
// post-login auto-enter.
|
||||
void openDmHistory() {
|
||||
_task->clearDMUnread(_sel_contact.id.pub_key);
|
||||
_dm_hist_sel = -1;
|
||||
beginDmView();
|
||||
_phase = DM_HIST;
|
||||
}
|
||||
|
||||
// Opening a conversation: on its first unread message ("New" above it), else
|
||||
// on the compose row under the newest.
|
||||
void beginDmView() {
|
||||
const uint8_t* key = _sel_contact.id.pub_key;
|
||||
_new_idx = _history.firstUnreadDm(key, _task->getDMUnread(key));
|
||||
_anchor_new = _new_idx >= 0;
|
||||
_task->clearDMUnread(key);
|
||||
_dm_hist_sel = _new_idx;
|
||||
_dm_hist_scroll = 0;
|
||||
_dm_fs.active = false;
|
||||
_phase = DM_HIST;
|
||||
}
|
||||
void beginChannelView(uint8_t channel_idx) {
|
||||
_sel_channel_idx = channel_idx;
|
||||
_unread_at_entry = (int)_history.chUnread(channel_idx);
|
||||
_unread_base = 0;
|
||||
_seen_lo = 0; _seen_hi = -1;
|
||||
_new_idx = _history.firstUnreadChannel(channel_idx, _unread_at_entry);
|
||||
_anchor_new = _new_idx >= 0;
|
||||
_hist_scroll = 0;
|
||||
_hist_sel = _new_idx >= 0 ? _new_idx : _history.histCountForChannel(channel_idx) > 0 ? 0 : -1;
|
||||
_fs.active = false;
|
||||
}
|
||||
|
||||
// On the first unread (sel), opening: the scroll showing it highest with
|
||||
// "New" above it (stack(s) lays out from s, sets div_y when "New" fits).
|
||||
template <class Stack> static int anchorScroll(Stack stack, const int& div_y, int sel, int count) {
|
||||
int pick = -1;
|
||||
for (int s = sel > 8 ? sel - 8 : 0; s <= sel; s++) {
|
||||
int n = stack(s);
|
||||
if (s + n - 1 < sel) continue;
|
||||
if (div_y >= 0 || sel + 1 >= count) return s;
|
||||
if (pick < 0) pick = s;
|
||||
}
|
||||
return pick >= 0 ? pick : sel;
|
||||
}
|
||||
|
||||
// "New" between two lines, lh tall, over the first message unread on opening.
|
||||
static void drawNewDivider(DisplayDriver& d, int y, int w, int lh) {
|
||||
const char* t = "New";
|
||||
int tw = d.getTextWidth(t);
|
||||
int tx = (w - tw) / 2, mid = y + lh / 2;
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
d.fillRect(1, mid, tx - 4, 1);
|
||||
d.fillRect(tx + tw + 3, mid, w - 1 - (tx + tw + 3), 1);
|
||||
d.setCursor(tx, y + 1);
|
||||
d.print(t);
|
||||
}
|
||||
|
||||
|
||||
@@ -1070,15 +1126,21 @@ public:
|
||||
// "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;
|
||||
// histEntryForChannel is newest-first: index 0 = newest (unread), higher = older.
|
||||
// Count everything actually rendered on screen as seen — not just the
|
||||
// highlighted row — so a taller screen that fits more boxes at once marks
|
||||
// more read up front, instead of requiring a press per row.
|
||||
int seen_to = _hist_scroll + _hist_visible - 1;
|
||||
if (_hist_sel > seen_to) seen_to = _hist_sel;
|
||||
if (seen_to > _viewing_max_seen) _viewing_max_seen = seen_to;
|
||||
// Each step down from 0 sees one more message; seen count = max_seen + 1.
|
||||
int remaining = _unread_at_entry - (_viewing_max_seen + 1);
|
||||
// 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
|
||||
// a taller screen that fits more boxes at once marks more read up front,
|
||||
// instead of requiring a press per row. Reading starts at the first unread
|
||||
// and goes down to the newest, so what's been shown is one stretch.
|
||||
int lo = _hist_scroll, hi = _hist_scroll + _hist_visible - 1;
|
||||
if (_hist_sel >= 0 && _hist_sel < lo) lo = _hist_sel;
|
||||
if (_hist_sel > hi) hi = _hist_sel;
|
||||
if (_seen_hi < 0 || lo < _seen_lo) _seen_lo = lo;
|
||||
if (hi > _seen_hi) _seen_hi = hi;
|
||||
int a = _seen_lo > _unread_base ? _seen_lo : _unread_base;
|
||||
int b = _unread_base + _unread_at_entry - 1;
|
||||
if (_seen_hi < b) b = _seen_hi;
|
||||
int remaining = _unread_at_entry - (b >= a ? b - a + 1 : 0);
|
||||
_history.setChUnread(_sel_channel_idx, (uint8_t)(remaining > 0 ? remaining : 0));
|
||||
}
|
||||
|
||||
@@ -1109,7 +1171,8 @@ public:
|
||||
_pick_fav_slot = -1;
|
||||
_direct_entry = false;
|
||||
_unread_at_entry = 0;
|
||||
_viewing_max_seen = 0;
|
||||
_new_idx = -1;
|
||||
_anchor_new = false;
|
||||
_ch_view.reset();
|
||||
_ch_view_pending_rebuild = false;
|
||||
}
|
||||
@@ -1235,10 +1298,7 @@ public:
|
||||
|
||||
void enterDM(const ContactInfo& ci) {
|
||||
_sel_contact = ci;
|
||||
_task->clearDMUnread(ci.id.pub_key);
|
||||
_dm_hist_sel = -1;
|
||||
_dm_hist_scroll = 0;
|
||||
_dm_fs.active = false;
|
||||
beginDmView();
|
||||
_room_mode = false;
|
||||
_phase = DM_HIST;
|
||||
_direct_entry = true;
|
||||
@@ -1249,10 +1309,7 @@ public:
|
||||
// to. Caller must have already reset() the screen.
|
||||
void enterRoom(const ContactInfo& ci) {
|
||||
_sel_contact = ci;
|
||||
_task->clearDMUnread(ci.id.pub_key);
|
||||
_dm_hist_sel = -1;
|
||||
_dm_hist_scroll = 0;
|
||||
_dm_fs.active = false;
|
||||
beginDmView();
|
||||
_room_mode = true;
|
||||
_phase = DM_HIST;
|
||||
_direct_entry = true;
|
||||
@@ -1262,12 +1319,7 @@ public:
|
||||
// Open a channel's history directly (used by the Favourites dial). Caller
|
||||
// must have already reset() the screen.
|
||||
void enterChannel(uint8_t channel_idx) {
|
||||
_sel_channel_idx = channel_idx;
|
||||
_unread_at_entry = (int)_history.chUnread(channel_idx);
|
||||
_hist_scroll = 0;
|
||||
_hist_sel = _history.histCountForChannel(channel_idx) > 0 ? 0 : -1;
|
||||
_viewing_max_seen = _hist_sel >= 0 ? _hist_sel : 0;
|
||||
_fs.active = false;
|
||||
beginChannelView(channel_idx);
|
||||
_phase = CHANNEL_HIST;
|
||||
_direct_entry = true;
|
||||
}
|
||||
@@ -1503,7 +1555,8 @@ public:
|
||||
// Fixed-track scrollbar metrics + a stable gutter reserve (decided by a
|
||||
// whole-list fit test, not last frame's visible count) so message boxes
|
||||
// don't reflow their width as messages arrive.
|
||||
HistScroll hs = compact_dm
|
||||
auto scrollMetrics = [&]() {
|
||||
return compact_dm
|
||||
? computeHistScrollH(display, dm_count, _dm_hist_scroll, hist_start_y, cby, 1, compactH)
|
||||
: computeHistScroll(display, expand, dm_count, _dm_hist_scroll,
|
||||
hist_start_y, cby, lh,
|
||||
@@ -1514,20 +1567,24 @@ public:
|
||||
char tmp_sender[33];
|
||||
return bodyAt(idx, tmp_sender, sizeof(tmp_sender));
|
||||
});
|
||||
};
|
||||
HistScroll hs = scrollMetrics();
|
||||
int reserve = hs.reserve;
|
||||
{
|
||||
// Stack boxes upward from just above the compose row, so item 0 (the
|
||||
// newest message) lands at the bottom of the list and older messages
|
||||
// sit progressively higher — same anchor convention as a typical
|
||||
// messenger, instead of newest-at-top. box_ys[i] still corresponds
|
||||
// to item (_dm_hist_scroll + i), same as before; only its y flips.
|
||||
int div_y = -1; // "New", when shown
|
||||
// Stack boxes upward from just above the compose row, so item 0 (the
|
||||
// newest message) lands at the bottom of the list and older messages
|
||||
// sit progressively higher — same anchor convention as a typical
|
||||
// messenger, instead of newest-at-top. box_ys[i] still corresponds
|
||||
// to item (s + i), same as before; only its y flips.
|
||||
auto stack = [&](int s) {
|
||||
const int fixed_bh = 2 * lh + 1;
|
||||
const int box_gap = expand ? 2 : 1;
|
||||
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
||||
for (int ii = 0; ii < MAX_VIS_BOXES && (_dm_hist_scroll + ii) < dm_count; ii++) {
|
||||
int bh = compact_dm ? compactH(_dm_hist_scroll + ii, reserve) : fixed_bh;
|
||||
n_vis = 0; div_y = -1;
|
||||
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, _dm_hist_scroll + ii);
|
||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, s + ii);
|
||||
if (rp >= 0) {
|
||||
char hsb[33];
|
||||
const char* hbody = skipReplyPrefix(dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name, hsb, sizeof(hsb)));
|
||||
@@ -1538,8 +1595,20 @@ public:
|
||||
if (box_top < hist_start_y) break;
|
||||
box_ys[n_vis] = box_top; box_hs[n_vis] = bh; n_vis++;
|
||||
cur_y = box_top - box_gap;
|
||||
if (s + ii == _new_idx && _new_idx + 1 < dm_count) { // "New" over it, below the older ones
|
||||
if (cur_y - lh < hist_start_y) break;
|
||||
div_y = cur_y - lh;
|
||||
cur_y = div_y - box_gap;
|
||||
}
|
||||
}
|
||||
return n_vis;
|
||||
};
|
||||
if (_anchor_new) {
|
||||
_anchor_new = false;
|
||||
_dm_hist_scroll = anchorScroll(stack, div_y, _dm_hist_sel, dm_count);
|
||||
hs = scrollMetrics();
|
||||
}
|
||||
stack(_dm_hist_scroll);
|
||||
_hist_visible = n_vis;
|
||||
for (int i = 0; i < n_vis && (_dm_hist_scroll + i) < dm_count; i++) {
|
||||
int item = _dm_hist_scroll + i;
|
||||
@@ -1614,6 +1683,8 @@ public:
|
||||
drawHeadedBubble(display, y, bh, lh, sel, reserve, e.outgoing, sender, body, age, mark, expand, mq_delay);
|
||||
}
|
||||
|
||||
if (div_y >= 0) drawNewDivider(display, div_y, display.width() - reserve, lh);
|
||||
|
||||
if (dm_count == 0) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
|
||||
@@ -1705,7 +1776,8 @@ public:
|
||||
const int MAX_VIS_BOXES = 8;
|
||||
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
||||
// Fixed-track scrollbar metrics + stable gutter reserve (see DM history above).
|
||||
HistScroll hs = computeHistScroll(display, expand, ch_hist_count, _hist_scroll,
|
||||
auto scrollMetrics = [&]() {
|
||||
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);
|
||||
@@ -1714,18 +1786,22 @@ public:
|
||||
const char* s = strstr(t, ": ");
|
||||
return skipReplyPrefix(s ? s + 2 : t);
|
||||
});
|
||||
};
|
||||
HistScroll hs = scrollMetrics();
|
||||
int reserve = hs.reserve;
|
||||
{
|
||||
// Stack boxes upward from just above the compose row — see the DM
|
||||
// history block above for why (newest at the bottom, like a typical
|
||||
// messenger). box_ys[i] still corresponds to item (_hist_scroll + i).
|
||||
int div_y = -1; // "New", when shown
|
||||
// Stack boxes upward from just above the compose row — see the DM
|
||||
// history block above for why (newest at the bottom, like a typical
|
||||
// messenger). box_ys[i] still corresponds to item (s + i).
|
||||
auto stack = [&](int s) {
|
||||
const int fixed_bh = 2 * lh + 1;
|
||||
const int box_gap = expand ? 2 : 1;
|
||||
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
||||
for (int ii = 0; ii < MAX_VIS_BOXES && (_hist_scroll + ii) < ch_hist_count; ii++) {
|
||||
n_vis = 0; div_y = -1;
|
||||
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, _hist_scroll + ii);
|
||||
int rp = _history.histEntryForChannel(_sel_channel_idx, s + ii);
|
||||
if (rp >= 0) {
|
||||
const char* rtext = _history.chAtPos(rp).text;
|
||||
const char* rsep = strstr(rtext, ": ");
|
||||
@@ -1737,8 +1813,20 @@ public:
|
||||
if (box_top < hist_start_y) break;
|
||||
box_ys[n_vis] = box_top; box_hs[n_vis] = bh; n_vis++;
|
||||
cur_y = box_top - box_gap;
|
||||
if (s + ii == _new_idx && _new_idx + 1 < ch_hist_count) { // "New" over it, below the older ones
|
||||
if (cur_y - lh < hist_start_y) break;
|
||||
div_y = cur_y - lh;
|
||||
cur_y = div_y - box_gap;
|
||||
}
|
||||
}
|
||||
return n_vis;
|
||||
};
|
||||
if (_anchor_new) {
|
||||
_anchor_new = false;
|
||||
_hist_scroll = anchorScroll(stack, div_y, _hist_sel, ch_hist_count);
|
||||
hs = scrollMetrics();
|
||||
}
|
||||
stack(_hist_scroll);
|
||||
_hist_visible = n_vis;
|
||||
updateChannelUnread(); // mark everything in the just-computed visible window as seen
|
||||
|
||||
@@ -1784,6 +1872,8 @@ public:
|
||||
drawHeadedBubble(display, y, bh, lh, sel, reserve, outgoing, sender, body, age, mark, expand, mq_delay);
|
||||
}
|
||||
|
||||
if (div_y >= 0) drawNewDivider(display, div_y, display.width() - reserve, lh);
|
||||
|
||||
if (ch_hist_count == 0) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
|
||||
@@ -2200,16 +2290,12 @@ public:
|
||||
if (_pick_target) { commitPickTargetChannel(_sel_channel_idx); return true; }
|
||||
if (_pick_fav_slot >= 0) { commitPickFavChannel(_sel_channel_idx); return true; }
|
||||
if (_pick_bot_channel) { commitPickBotChannel(_sel_channel_idx); return true; }
|
||||
int hc = _history.histCountForChannel(_sel_channel_idx);
|
||||
_unread_at_entry = (int)_history.chUnread(_sel_channel_idx);
|
||||
_hist_scroll = 0;
|
||||
_hist_sel = hc > 0 ? 0 : -1;
|
||||
_viewing_max_seen = _hist_sel >= 0 ? _hist_sel : 0;
|
||||
beginChannelView(_sel_channel_idx);
|
||||
_phase = CHANNEL_HIST;
|
||||
// Not updateChannelUnread() here: _hist_visible is still whatever this
|
||||
// channel's first render() hasn't computed yet (stale, shared with
|
||||
// DM_HIST) — calling it now could ratchet _viewing_max_seen past what's
|
||||
// actually about to be shown. render() calls it once that's fresh.
|
||||
// DM_HIST) — calling it now could mark more seen than is about to be
|
||||
// shown. render() calls it once that's fresh.
|
||||
if (_share_mode) beginShareCompose(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2210,6 +2210,17 @@ void UITask::simMessage(const char* text) {
|
||||
onMessageArrived(ev);
|
||||
}
|
||||
|
||||
// Sim tests: read and new messages (UiCore::simUnreadDemo), then channel 0
|
||||
// (which 0) or the first chat contact (1) opened as from the list.
|
||||
void UITask::simUnread(int which) {
|
||||
if (which < 0) { _core->simUnreadDemo(); return; }
|
||||
if (which == 0) { openChannelHistory(0); return; }
|
||||
for (int i = 0; i < the_mesh.getNumContacts(); i++) {
|
||||
ContactInfo c;
|
||||
if (the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c) && c.type == ADV_TYPE_CHAT) { openContactDM(c); return; }
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::simCompose(const char* text) {
|
||||
gotoMessagesScreen();
|
||||
((MessagesScreen*)messages_screen)->simCompose(text);
|
||||
@@ -2716,6 +2727,11 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_compose(const char* text) {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simCompose(text);
|
||||
}
|
||||
|
||||
// Sim tests: -1 files read and new messages, 0 opens channel 0, 1 the first DM.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_unread(int which) {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simUnread(which);
|
||||
}
|
||||
|
||||
// Sim tests: a channel message arrives, alerted in Settings > Message alert's
|
||||
// style (0 Normal, 1 Compact, 2 Off).
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_message(const char* text, int style) {
|
||||
|
||||
@@ -284,6 +284,7 @@ public:
|
||||
#ifdef SIM_PLATFORM
|
||||
void simCompose(const char* text); // sim tests: a channel post's keyboard holding `text`
|
||||
void simMessage(const char* text); // sim tests: a channel message arrives
|
||||
void simUnread(int which); // sim tests: read and new messages, opened
|
||||
#endif
|
||||
void openContactDM(const ContactInfo& ci);
|
||||
void openChannelHistory(uint8_t channel_idx); // Favourites dial: open a pinned channel
|
||||
|
||||
Reference in New Issue
Block a user