mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +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:
@@ -16,6 +16,10 @@ and a small hop count: on a received message, how many repeaters it came
|
|||||||
through; on your own, how many repeaters were heard passing it on. In
|
through; on your own, how many repeaters were heard passing it on. In
|
||||||
channels and rooms it also names the sender.
|
channels and rooms it also names the sender.
|
||||||
|
|
||||||
|
A conversation with unread messages opens on the first of them, under a
|
||||||
|
**New** line, so you read down to the newest. With nothing unread it opens
|
||||||
|
at the newest.
|
||||||
|
|
||||||
**Enter** on a message opens it full screen; **Left / Right** there pages to the
|
**Enter** on a message opens it full screen; **Left / Right** there pages to the
|
||||||
older and newer one.
|
older and newer one.
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,31 @@ public:
|
|||||||
return -1;
|
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
|
// 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
|
// deadline has passed AND no auto-resends remain — while resends_left > 0 the
|
||||||
// entry stays pending (tickDmResends() retries / finalises it). Safety net for
|
// entry stays pending (tickDmResends() retries / finalises it). Safety net for
|
||||||
|
|||||||
@@ -224,6 +224,28 @@ public:
|
|||||||
if (viewing && pos >= 0) _host->onViewedHistoryGrew(true);
|
if (viewing && pos >= 0) _host->onViewedHistoryGrew(true);
|
||||||
return pos;
|
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 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 {
|
void onChannelRelayed(uint32_t seq, const uint8_t* repeater_hash = nullptr, uint8_t hash_size = 0) override {
|
||||||
history.markChannelRelayed(seq, repeater_hash, hash_size);
|
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) {
|
void UITask::openChannel(uint8_t channel_idx) {
|
||||||
_thread_is_channel = true;
|
_thread_is_channel = true;
|
||||||
_thread_channel = channel_idx;
|
_thread_channel = channel_idx;
|
||||||
_thread_skip = 0;
|
MessageHistory& h = _core->history;
|
||||||
_core->history.setChUnread(channel_idx, 0);
|
threadOpenAt(h.firstUnreadChannel(channel_idx, h.chUnread(channel_idx)));
|
||||||
|
h.setChUnread(channel_idx, 0);
|
||||||
_screen = SCR_THREAD;
|
_screen = SCR_THREAD;
|
||||||
buildThread();
|
buildThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::openDM(const uint8_t* pub_key) {
|
void UITask::openDM(const uint8_t* pub_key) {
|
||||||
_thread_is_channel = false;
|
_thread_is_channel = false;
|
||||||
_thread_skip = 0;
|
|
||||||
memset(_thread_key, 0, sizeof(_thread_key));
|
memset(_thread_key, 0, sizeof(_thread_key));
|
||||||
ContactInfo c;
|
ContactInfo c;
|
||||||
if (MessageHistory::contactByPrefix(pub_key, c)) memcpy(_thread_key, c.id.pub_key, PUB_KEY_SIZE);
|
if (MessageHistory::contactByPrefix(pub_key, c)) memcpy(_thread_key, c.id.pub_key, PUB_KEY_SIZE);
|
||||||
else memcpy(_thread_key, pub_key, 4);
|
else memcpy(_thread_key, pub_key, 4);
|
||||||
|
threadOpenAt(_core->history.firstUnreadDm(_thread_key, _core->dmUnread(_thread_key)));
|
||||||
_core->clearDMUnread(_thread_key);
|
_core->clearDMUnread(_thread_key);
|
||||||
_screen = SCR_THREAD;
|
_screen = SCR_THREAD;
|
||||||
buildThread();
|
buildThread();
|
||||||
@@ -3742,7 +3753,6 @@ uint32_t UITask::threadSignature() const {
|
|||||||
// Positions found in the shown messages ([WAY] / [LOC] / plain "lat,lon"),
|
// Positions found in the shown messages ([WAY] / [LOC] / plain "lat,lon"),
|
||||||
// for the Go / Save buttons under such a bubble.
|
// for the Go / Save buttons under such a bubble.
|
||||||
struct MsgLoc { int32_t lat, lon; char label[WAYPOINT_LABEL_LEN * 2]; };
|
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 MsgLoc* s_msg_locs = psramBuf<MsgLoc>(THREAD_MAX_SHOWN);
|
||||||
static int s_msg_loc_n = 0;
|
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 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 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 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.
|
// The shown message a bubble (or a button in it) belongs to, or -1.
|
||||||
static int threadRowOf(lv_obj_t* o) {
|
static int threadRowOf(lv_obj_t* o) {
|
||||||
@@ -4134,7 +4166,11 @@ void UITask::refreshThread() {
|
|||||||
drop--;
|
drop--;
|
||||||
}
|
}
|
||||||
if (kept > 0) {
|
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_th_rows, s_th_rows + drop, kept * sizeof(ThreadRow));
|
||||||
memmove(s_msg_meta, s_msg_meta + drop, kept * sizeof(MsgMeta));
|
memmove(s_msg_meta, s_msg_meta + drop, kept * sizeof(MsgMeta));
|
||||||
for (int r = 0; r < kept; r++) {
|
for (int r = 0; r < kept; r++) {
|
||||||
@@ -4155,7 +4191,7 @@ void UITask::refreshThread() {
|
|||||||
for (int r = kept; r < n; r++) build(r);
|
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); }
|
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;
|
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);
|
layoutNow(_thread_list);
|
||||||
lv_obj_scroll_to_y(_thread_list, LV_COORD_MAX, LV_ANIM_OFF);
|
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);
|
pageButton(_thread_list, t, 1);
|
||||||
s_older_lbl = lv_obj_get_child(lv_obj_get_child(_thread_list, -1), 0);
|
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_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
|
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 (_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);
|
if (n == 0) label(_thread_list, "No messages yet", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||||
layoutNow(_thread_list);
|
layoutNow(_thread_list);
|
||||||
lv_obj_scroll_to_y(_thread_list, _thread_scroll_top ? 0 : LV_COORD_MAX, LV_ANIM_OFF);
|
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;
|
_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.
|
// Sim tests: straight to a screen by name.
|
||||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||||
if (!s_ui) return;
|
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 (!strncmp(name, "compose@", 8)) { s_ui->simCompose(name + 8); return; }
|
||||||
if (!strcmp(name, "dm")) { s_ui->simDm(); return; }
|
if (!strcmp(name, "dm")) { s_ui->simDm(); return; }
|
||||||
if (!strcmp(name, "send")) { s_ui->simSend(); 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, "vector")) { s_ui->setVectorMap(true); return; }
|
||||||
if (!strcmp(name, "maptools")) { s_ui->navToolsPopup(); return; }
|
if (!strcmp(name, "maptools")) { s_ui->navToolsPopup(); return; }
|
||||||
if (!strcmp(name, "areasel")) { s_ui->areaSelectBegin(); return; }
|
if (!strcmp(name, "areasel")) { s_ui->areaSelectBegin(); return; }
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ public:
|
|||||||
void simMapAt(const char* spec); // Map tools > Live tiles
|
void simMapAt(const char* spec); // Map tools > Live tiles
|
||||||
void simCompose(const char* text); // the open thread's field: this text, keyboard up
|
void simCompose(const char* text); // the open thread's field: this text, keyboard up
|
||||||
void simDm(); // the first chat contact's thread
|
void simDm(); // the first chat contact's thread
|
||||||
|
void simUnread(); // read and new messages to open on
|
||||||
void simSend() { sendFromCompose(); } // the field, sent
|
void simSend() { sendFromCompose(); } // the field, sent
|
||||||
// Navigation map (NavMap.h)
|
// Navigation map (NavMap.h)
|
||||||
void navTargetsPopup();
|
void navTargetsPopup();
|
||||||
@@ -425,6 +426,7 @@ private:
|
|||||||
void buildNode();
|
void buildNode();
|
||||||
void refreshNode();
|
void refreshNode();
|
||||||
void buildThread();
|
void buildThread();
|
||||||
|
void threadOpenAt(int first);
|
||||||
void refreshThread();
|
void refreshThread();
|
||||||
void refreshThreadAges();
|
void refreshThreadAges();
|
||||||
uint32_t threadSignature() const;
|
uint32_t threadSignature() const;
|
||||||
@@ -539,6 +541,9 @@ private:
|
|||||||
bool _thread_is_channel = false;
|
bool _thread_is_channel = false;
|
||||||
int _thread_skip = 0; // newest messages above the page shown (Older / Newer)
|
int _thread_skip = 0; // newest messages above the page shown (Older / Newer)
|
||||||
bool _thread_scroll_top = false;
|
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);
|
int loadThreadPage(int& total);
|
||||||
uint8_t _thread_channel = 0;
|
uint8_t _thread_channel = 0;
|
||||||
uint8_t _thread_key[PUB_KEY_SIZE] = {0};
|
uint8_t _thread_key[PUB_KEY_SIZE] = {0};
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ class MessagesScreen : public UIScreen {
|
|||||||
int _hist_sel, _hist_scroll;
|
int _hist_sel, _hist_scroll;
|
||||||
FullscreenMsgView _fs;
|
FullscreenMsgView _fs;
|
||||||
int _unread_at_entry; // channel unread count when entering CHANNEL_HIST
|
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
|
// KEYBOARD
|
||||||
KeyboardWidget* _kb;
|
KeyboardWidget* _kb;
|
||||||
@@ -143,6 +144,11 @@ class MessagesScreen : public UIScreen {
|
|||||||
int _dm_hist_sel, _dm_hist_scroll;
|
int _dm_hist_sel, _dm_hist_scroll;
|
||||||
FullscreenMsgView _dm_fs;
|
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
|
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
|
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
|
// Filed at index 0 by sendText(): the unread index range is stale, and
|
||||||
// the user is active in this channel -- fully read.
|
// the user is active in this channel -- fully read.
|
||||||
_unread_at_entry = 0;
|
_unread_at_entry = 0;
|
||||||
_viewing_max_seen = 0;
|
|
||||||
} else {
|
} else {
|
||||||
_dm_hist_sel = 0;
|
_dm_hist_sel = 0;
|
||||||
_dm_hist_scroll = 0;
|
_dm_hist_scroll = 0;
|
||||||
@@ -969,7 +974,7 @@ public:
|
|||||||
_sel_channel_idx(0), _sending_to_channel(false),
|
_sel_channel_idx(0), _sending_to_channel(false),
|
||||||
_msg_sel(0), _msg_scroll(0), _active_msg_count(0),
|
_msg_sel(0), _msg_scroll(0), _active_msg_count(0),
|
||||||
_hist_sel(0), _hist_scroll(0),
|
_hist_sel(0), _hist_scroll(0),
|
||||||
_unread_at_entry(0), _viewing_max_seen(0),
|
_unread_at_entry(0),
|
||||||
_history(task->core().history),
|
_history(task->core().history),
|
||||||
_dm_hist_sel(-1), _dm_hist_scroll(0),
|
_dm_hist_sel(-1), _dm_hist_scroll(0),
|
||||||
_ctx_dirty(false), _pin_picker_active(false), _direct_entry(false), _reply_mode(false),
|
_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;
|
return _phase == DM_HIST && memcmp(_sel_contact.id.pub_key, pub_key, 4) == 0;
|
||||||
}
|
}
|
||||||
void onViewedHistoryGrew(bool channel) {
|
void onViewedHistoryGrew(bool channel) {
|
||||||
if (channel) { if (_hist_sel > 0) { _hist_sel++; _hist_scroll++; } }
|
if (channel) {
|
||||||
else { if (_dm_hist_sel > 0) { _dm_hist_sel++; _dm_hist_scroll++; } }
|
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
|
// 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
|
// the scroll/selection view state. Shared by the Enter-on-contact path and the
|
||||||
// post-login auto-enter.
|
// post-login auto-enter.
|
||||||
void openDmHistory() {
|
void openDmHistory() {
|
||||||
_task->clearDMUnread(_sel_contact.id.pub_key);
|
beginDmView();
|
||||||
_dm_hist_sel = -1;
|
_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_hist_scroll = 0;
|
||||||
_dm_fs.active = false;
|
_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.
|
// "what has the user seen on screen" logic is pure phase-machine state.
|
||||||
void updateChannelUnread() {
|
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) return;
|
||||||
// histEntryForChannel is newest-first: index 0 = newest (unread), higher = older.
|
// histEntryForChannel is newest-first: index 0 = newest, higher = older;
|
||||||
// Count everything actually rendered on screen as seen — not just the
|
// the unread from opening are _unread_base onwards. Count everything
|
||||||
// highlighted row — so a taller screen that fits more boxes at once marks
|
// actually rendered on screen as seen — not just the highlighted row — so
|
||||||
// more read up front, instead of requiring a press per row.
|
// a taller screen that fits more boxes at once marks more read up front,
|
||||||
int seen_to = _hist_scroll + _hist_visible - 1;
|
// instead of requiring a press per row. Reading starts at the first unread
|
||||||
if (_hist_sel > seen_to) seen_to = _hist_sel;
|
// and goes down to the newest, so what's been shown is one stretch.
|
||||||
if (seen_to > _viewing_max_seen) _viewing_max_seen = seen_to;
|
int lo = _hist_scroll, hi = _hist_scroll + _hist_visible - 1;
|
||||||
// Each step down from 0 sees one more message; seen count = max_seen + 1.
|
if (_hist_sel >= 0 && _hist_sel < lo) lo = _hist_sel;
|
||||||
int remaining = _unread_at_entry - (_viewing_max_seen + 1);
|
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));
|
_history.setChUnread(_sel_channel_idx, (uint8_t)(remaining > 0 ? remaining : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,7 +1171,8 @@ public:
|
|||||||
_pick_fav_slot = -1;
|
_pick_fav_slot = -1;
|
||||||
_direct_entry = false;
|
_direct_entry = false;
|
||||||
_unread_at_entry = 0;
|
_unread_at_entry = 0;
|
||||||
_viewing_max_seen = 0;
|
_new_idx = -1;
|
||||||
|
_anchor_new = false;
|
||||||
_ch_view.reset();
|
_ch_view.reset();
|
||||||
_ch_view_pending_rebuild = false;
|
_ch_view_pending_rebuild = false;
|
||||||
}
|
}
|
||||||
@@ -1235,10 +1298,7 @@ public:
|
|||||||
|
|
||||||
void enterDM(const ContactInfo& ci) {
|
void enterDM(const ContactInfo& ci) {
|
||||||
_sel_contact = ci;
|
_sel_contact = ci;
|
||||||
_task->clearDMUnread(ci.id.pub_key);
|
beginDmView();
|
||||||
_dm_hist_sel = -1;
|
|
||||||
_dm_hist_scroll = 0;
|
|
||||||
_dm_fs.active = false;
|
|
||||||
_room_mode = false;
|
_room_mode = false;
|
||||||
_phase = DM_HIST;
|
_phase = DM_HIST;
|
||||||
_direct_entry = true;
|
_direct_entry = true;
|
||||||
@@ -1249,10 +1309,7 @@ public:
|
|||||||
// to. Caller must have already reset() the screen.
|
// to. Caller must have already reset() the screen.
|
||||||
void enterRoom(const ContactInfo& ci) {
|
void enterRoom(const ContactInfo& ci) {
|
||||||
_sel_contact = ci;
|
_sel_contact = ci;
|
||||||
_task->clearDMUnread(ci.id.pub_key);
|
beginDmView();
|
||||||
_dm_hist_sel = -1;
|
|
||||||
_dm_hist_scroll = 0;
|
|
||||||
_dm_fs.active = false;
|
|
||||||
_room_mode = true;
|
_room_mode = true;
|
||||||
_phase = DM_HIST;
|
_phase = DM_HIST;
|
||||||
_direct_entry = true;
|
_direct_entry = true;
|
||||||
@@ -1262,12 +1319,7 @@ public:
|
|||||||
// Open a channel's history directly (used by the Favourites dial). Caller
|
// Open a channel's history directly (used by the Favourites dial). Caller
|
||||||
// must have already reset() the screen.
|
// must have already reset() the screen.
|
||||||
void enterChannel(uint8_t channel_idx) {
|
void enterChannel(uint8_t channel_idx) {
|
||||||
_sel_channel_idx = channel_idx;
|
beginChannelView(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;
|
|
||||||
_phase = CHANNEL_HIST;
|
_phase = CHANNEL_HIST;
|
||||||
_direct_entry = true;
|
_direct_entry = true;
|
||||||
}
|
}
|
||||||
@@ -1503,7 +1555,8 @@ public:
|
|||||||
// Fixed-track scrollbar metrics + a stable gutter reserve (decided by a
|
// Fixed-track scrollbar metrics + a stable gutter reserve (decided by a
|
||||||
// whole-list fit test, not last frame's visible count) so message boxes
|
// whole-list fit test, not last frame's visible count) so message boxes
|
||||||
// don't reflow their width as messages arrive.
|
// 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)
|
? computeHistScrollH(display, dm_count, _dm_hist_scroll, hist_start_y, cby, 1, compactH)
|
||||||
: computeHistScroll(display, expand, dm_count, _dm_hist_scroll,
|
: computeHistScroll(display, expand, dm_count, _dm_hist_scroll,
|
||||||
hist_start_y, cby, lh,
|
hist_start_y, cby, lh,
|
||||||
@@ -1514,20 +1567,24 @@ public:
|
|||||||
char tmp_sender[33];
|
char tmp_sender[33];
|
||||||
return bodyAt(idx, tmp_sender, sizeof(tmp_sender));
|
return bodyAt(idx, tmp_sender, sizeof(tmp_sender));
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
HistScroll hs = scrollMetrics();
|
||||||
int reserve = hs.reserve;
|
int reserve = hs.reserve;
|
||||||
{
|
int div_y = -1; // "New", when shown
|
||||||
// Stack boxes upward from just above the compose row, so item 0 (the
|
// 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
|
// newest message) lands at the bottom of the list and older messages
|
||||||
// sit progressively higher — same anchor convention as a typical
|
// sit progressively higher — same anchor convention as a typical
|
||||||
// messenger, instead of newest-at-top. box_ys[i] still corresponds
|
// messenger, instead of newest-at-top. box_ys[i] still corresponds
|
||||||
// to item (_dm_hist_scroll + i), same as before; only its y flips.
|
// to item (s + i), same as before; only its y flips.
|
||||||
|
auto stack = [&](int s) {
|
||||||
const int fixed_bh = 2 * lh + 1;
|
const int fixed_bh = 2 * lh + 1;
|
||||||
const int box_gap = expand ? 2 : 1;
|
const int box_gap = expand ? 2 : 1;
|
||||||
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
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++) {
|
n_vis = 0; div_y = -1;
|
||||||
int bh = compact_dm ? compactH(_dm_hist_scroll + ii, reserve) : fixed_bh;
|
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) {
|
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) {
|
if (rp >= 0) {
|
||||||
char hsb[33];
|
char hsb[33];
|
||||||
const char* hbody = skipReplyPrefix(dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name, hsb, sizeof(hsb)));
|
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;
|
if (box_top < hist_start_y) break;
|
||||||
box_ys[n_vis] = box_top; box_hs[n_vis] = bh; n_vis++;
|
box_ys[n_vis] = box_top; box_hs[n_vis] = bh; n_vis++;
|
||||||
cur_y = box_top - box_gap;
|
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;
|
_hist_visible = n_vis;
|
||||||
for (int i = 0; i < n_vis && (_dm_hist_scroll + i) < dm_count; i++) {
|
for (int i = 0; i < n_vis && (_dm_hist_scroll + i) < dm_count; i++) {
|
||||||
int item = _dm_hist_scroll + 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);
|
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) {
|
if (dm_count == 0) {
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
|
display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
|
||||||
@@ -1705,7 +1776,8 @@ public:
|
|||||||
const int MAX_VIS_BOXES = 8;
|
const int MAX_VIS_BOXES = 8;
|
||||||
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
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).
|
// 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,
|
hist_start_y, cby, lh,
|
||||||
[&](int idx) -> const char* {
|
[&](int idx) -> const char* {
|
||||||
int rp = _history.histEntryForChannel(_sel_channel_idx, idx);
|
int rp = _history.histEntryForChannel(_sel_channel_idx, idx);
|
||||||
@@ -1714,18 +1786,22 @@ public:
|
|||||||
const char* s = strstr(t, ": ");
|
const char* s = strstr(t, ": ");
|
||||||
return skipReplyPrefix(s ? s + 2 : t);
|
return skipReplyPrefix(s ? s + 2 : t);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
HistScroll hs = scrollMetrics();
|
||||||
int reserve = hs.reserve;
|
int reserve = hs.reserve;
|
||||||
{
|
int div_y = -1; // "New", when shown
|
||||||
// Stack boxes upward from just above the compose row — see the DM
|
// Stack boxes upward from just above the compose row — see the DM
|
||||||
// history block above for why (newest at the bottom, like a typical
|
// history block above for why (newest at the bottom, like a typical
|
||||||
// messenger). box_ys[i] still corresponds to item (_hist_scroll + i).
|
// messenger). box_ys[i] still corresponds to item (s + i).
|
||||||
|
auto stack = [&](int s) {
|
||||||
const int fixed_bh = 2 * lh + 1;
|
const int fixed_bh = 2 * lh + 1;
|
||||||
const int box_gap = expand ? 2 : 1;
|
const int box_gap = expand ? 2 : 1;
|
||||||
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
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;
|
int bh = fixed_bh;
|
||||||
if (expand) {
|
if (expand) {
|
||||||
int rp = _history.histEntryForChannel(_sel_channel_idx, _hist_scroll + ii);
|
int rp = _history.histEntryForChannel(_sel_channel_idx, s + ii);
|
||||||
if (rp >= 0) {
|
if (rp >= 0) {
|
||||||
const char* rtext = _history.chAtPos(rp).text;
|
const char* rtext = _history.chAtPos(rp).text;
|
||||||
const char* rsep = strstr(rtext, ": ");
|
const char* rsep = strstr(rtext, ": ");
|
||||||
@@ -1737,8 +1813,20 @@ public:
|
|||||||
if (box_top < hist_start_y) break;
|
if (box_top < hist_start_y) break;
|
||||||
box_ys[n_vis] = box_top; box_hs[n_vis] = bh; n_vis++;
|
box_ys[n_vis] = box_top; box_hs[n_vis] = bh; n_vis++;
|
||||||
cur_y = box_top - box_gap;
|
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;
|
_hist_visible = n_vis;
|
||||||
updateChannelUnread(); // mark everything in the just-computed visible window as seen
|
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);
|
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) {
|
if (ch_hist_count == 0) {
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
|
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_target) { commitPickTargetChannel(_sel_channel_idx); return true; }
|
||||||
if (_pick_fav_slot >= 0) { commitPickFavChannel(_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; }
|
if (_pick_bot_channel) { commitPickBotChannel(_sel_channel_idx); return true; }
|
||||||
int hc = _history.histCountForChannel(_sel_channel_idx);
|
beginChannelView(_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;
|
|
||||||
_phase = CHANNEL_HIST;
|
_phase = CHANNEL_HIST;
|
||||||
// Not updateChannelUnread() here: _hist_visible is still whatever this
|
// Not updateChannelUnread() here: _hist_visible is still whatever this
|
||||||
// channel's first render() hasn't computed yet (stale, shared with
|
// channel's first render() hasn't computed yet (stale, shared with
|
||||||
// DM_HIST) — calling it now could ratchet _viewing_max_seen past what's
|
// DM_HIST) — calling it now could mark more seen than is about to be
|
||||||
// actually about to be shown. render() calls it once that's fresh.
|
// shown. render() calls it once that's fresh.
|
||||||
if (_share_mode) beginShareCompose(true);
|
if (_share_mode) beginShareCompose(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2210,6 +2210,17 @@ void UITask::simMessage(const char* text) {
|
|||||||
onMessageArrived(ev);
|
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) {
|
void UITask::simCompose(const char* text) {
|
||||||
gotoMessagesScreen();
|
gotoMessagesScreen();
|
||||||
((MessagesScreen*)messages_screen)->simCompose(text);
|
((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);
|
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
|
// Sim tests: a channel message arrives, alerted in Settings > Message alert's
|
||||||
// style (0 Normal, 1 Compact, 2 Off).
|
// style (0 Normal, 1 Compact, 2 Off).
|
||||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_message(const char* text, int style) {
|
extern "C" EMSCRIPTEN_KEEPALIVE void sim_message(const char* text, int style) {
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ public:
|
|||||||
#ifdef SIM_PLATFORM
|
#ifdef SIM_PLATFORM
|
||||||
void simCompose(const char* text); // sim tests: a channel post's keyboard holding `text`
|
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 simMessage(const char* text); // sim tests: a channel message arrives
|
||||||
|
void simUnread(int which); // sim tests: read and new messages, opened
|
||||||
#endif
|
#endif
|
||||||
void openContactDM(const ContactInfo& ci);
|
void openContactDM(const ContactInfo& ci);
|
||||||
void openChannelHistory(uint8_t channel_idx); // Favourites dial: open a pinned channel
|
void openChannelHistory(uint8_t channel_idx); // Favourites dial: open a pinned channel
|
||||||
|
|||||||
Reference in New Issue
Block a user