mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-01 17:56:12 +00:00
feat: DM chat history, per-contact unread badges, bot history, contact index fix
- Add DM_HIST phase in QuickMsgScreen: 32-entry ring buffer tracks incoming and outgoing DMs per contact; selecting a contact now opens history view before compose - Per-contact DM unread badge in contact list (replaces hop count display) - Bot replies (DM and channel) stored in history so they appear on-screen - Fix contact index truncation: _sorted was uint8_t (max 255), causing contacts at indices 256-349 to map to wrong entries; changed to uint16_t - Add addDMMsg() virtual to AbstractUITask; wire through MyMesh.cpp - Incoming chat DMs call addDMMsg(); outgoing after successful send too - msgRead(0) from companion app clears per-contact unread table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d1fcd56b59
commit
855f0dada4
@@ -40,8 +40,9 @@ public:
|
|||||||
void enableSerial() { _serial->enable(); }
|
void enableSerial() { _serial->enable(); }
|
||||||
void disableSerial() { _serial->disable(); }
|
void disableSerial() { _serial->disable(); }
|
||||||
virtual void msgRead(int msgcount) = 0;
|
virtual void msgRead(int msgcount) = 0;
|
||||||
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type = 0) = 0;
|
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type = 0, const uint8_t* pub_key = nullptr) = 0;
|
||||||
virtual void notify(UIEventType t = UIEventType::none) = 0;
|
virtual void notify(UIEventType t = UIEventType::none) = 0;
|
||||||
virtual void addChannelMsg(uint8_t channel_idx, const char* text) {}
|
virtual void addChannelMsg(uint8_t channel_idx, const char* text) {}
|
||||||
|
virtual void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {}
|
||||||
virtual void loop() = 0;
|
virtual void loop() = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -465,8 +465,10 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
|
|||||||
// we only want to show text messages on display, not cli data
|
// we only want to show text messages on display, not cli data
|
||||||
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
|
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
|
||||||
if (should_display && _ui) {
|
if (should_display && _ui) {
|
||||||
_ui->newMsg(path_len, from.name, text, offline_queue_len, from.type);
|
_ui->newMsg(path_len, from.name, text, offline_queue_len, from.type, from.id.pub_key);
|
||||||
_ui->notify(UIEventType::contactMessage);
|
_ui->notify(UIEventType::contactMessage);
|
||||||
|
if (from.type == ADV_TYPE_CHAT)
|
||||||
|
_ui->addDMMsg(from.id.pub_key, false, text);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -881,6 +883,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
|||||||
_prefs.bot_trigger[0] = '\0';
|
_prefs.bot_trigger[0] = '\0';
|
||||||
_prefs.bot_reply_dm[0] = '\0';
|
_prefs.bot_reply_dm[0] = '\0';
|
||||||
_prefs.bot_reply_ch[0] = '\0';
|
_prefs.bot_reply_ch[0] = '\0';
|
||||||
|
_prefs.dm_show_all = 1; // show all contacts by default
|
||||||
_prefs.auto_off_secs = 15; // 15 seconds auto-off by default
|
_prefs.auto_off_secs = 15; // 15 seconds auto-off by default
|
||||||
_prefs.tz_offset_hours = 0; // UTC by default
|
_prefs.tz_offset_hours = 0; // UTC by default
|
||||||
_prefs.low_batt_mv = 3400; // auto-shutdown at 3.4V by default
|
_prefs.low_batt_mv = 3400; // auto-shutdown at 3.4V by default
|
||||||
@@ -1272,7 +1275,9 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||||
uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame
|
uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
|
uint8_t saved_type = recipient->type; // type is authoritative from advert, not app
|
||||||
updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
|
updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
|
||||||
|
if (saved_type != ADV_TYPE_NONE) recipient->type = saved_type;
|
||||||
recipient->lastmod = last_mod;
|
recipient->lastmod = last_mod;
|
||||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||||
writeOKFrame();
|
writeOKFrame();
|
||||||
@@ -1690,13 +1695,22 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
writeErrFrame(ERR_CODE_NOT_FOUND);
|
writeErrFrame(ERR_CODE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
} else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32) {
|
} else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32) {
|
||||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // not supported (yet)
|
uint8_t channel_idx = cmd_frame[1];
|
||||||
|
ChannelDetails channel;
|
||||||
|
StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32);
|
||||||
|
memcpy(channel.channel.secret, &cmd_frame[2 + 32], 32); // 256-bit key
|
||||||
|
if (setChannel(channel_idx, channel)) {
|
||||||
|
saveChannels();
|
||||||
|
writeOKFrame();
|
||||||
|
} else {
|
||||||
|
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
|
||||||
|
}
|
||||||
} else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) {
|
} else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) {
|
||||||
uint8_t channel_idx = cmd_frame[1];
|
uint8_t channel_idx = cmd_frame[1];
|
||||||
ChannelDetails channel;
|
ChannelDetails channel;
|
||||||
StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32);
|
StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32);
|
||||||
memset(channel.channel.secret, 0, sizeof(channel.channel.secret));
|
memset(channel.channel.secret, 0, sizeof(channel.channel.secret));
|
||||||
memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // NOTE: only 128-bit supported
|
memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // 128-bit key
|
||||||
if (setChannel(channel_idx, channel)) {
|
if (setChannel(channel_idx, channel)) {
|
||||||
saveChannels();
|
saveChannels();
|
||||||
writeOKFrame();
|
writeOKFrame();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Included at the bottom of MyMesh.cpp after all class definitions.
|
// Included at the bottom of MyMesh.cpp after all class definitions.
|
||||||
|
|
||||||
void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text) {
|
void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text) {
|
||||||
|
if (from.type != ADV_TYPE_CHAT) return;
|
||||||
if (!(_prefs.bot_enabled && _prefs.bot_trigger[0] && _prefs.bot_reply_dm[0] &&
|
if (!(_prefs.bot_enabled && _prefs.bot_trigger[0] && _prefs.bot_reply_dm[0] &&
|
||||||
millis() - _bot_last_reply_ms > 10000UL))
|
millis() - _bot_last_reply_ms > 10000UL))
|
||||||
return;
|
return;
|
||||||
@@ -24,8 +25,12 @@ void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text) {
|
|||||||
sensors.node_lat != 0.0 || sensors.node_lon != 0.0,
|
sensors.node_lat != 0.0 || sensors.node_lon != 0.0,
|
||||||
ts, _prefs.tz_offset_hours);
|
ts, _prefs.tz_offset_hours);
|
||||||
uint32_t expected_ack, est_timeout;
|
uint32_t expected_ack, est_timeout;
|
||||||
if (sendMessage(from, ts, 0, expanded, expected_ack, est_timeout) != MSG_SEND_FAILED)
|
if (sendMessage(from, ts, 0, expanded, expected_ack, est_timeout) != MSG_SEND_FAILED) {
|
||||||
_bot_last_reply_ms = millis();
|
_bot_last_reply_ms = millis();
|
||||||
|
#ifdef DISPLAY_CLASS
|
||||||
|
if (_ui) _ui->addDMMsg(from.id.pub_key, true, expanded);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text) {
|
void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text) {
|
||||||
@@ -54,6 +59,14 @@ void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text) {
|
|||||||
sensors.node_lat != 0.0 || sensors.node_lon != 0.0,
|
sensors.node_lat != 0.0 || sensors.node_lon != 0.0,
|
||||||
ts, _prefs.tz_offset_hours);
|
ts, _prefs.tz_offset_hours);
|
||||||
int rlen = strlen(expanded);
|
int rlen = strlen(expanded);
|
||||||
if (sendGroupMessage(ts, ch.channel, _prefs.node_name, expanded, rlen))
|
if (sendGroupMessage(ts, ch.channel, _prefs.node_name, expanded, rlen)) {
|
||||||
_bot_last_reply_ms = millis();
|
_bot_last_reply_ms = millis();
|
||||||
|
#ifdef DISPLAY_CLASS
|
||||||
|
if (_ui) {
|
||||||
|
char with_sender[160];
|
||||||
|
snprintf(with_sender, sizeof(with_sender), "%s: %s", _prefs.node_name, expanded);
|
||||||
|
_ui->addChannelMsg(channel_idx, with_sender);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ class SettingsScreen : public UIScreen {
|
|||||||
bool homePageVisible(int item, const NodePrefs* p) const {
|
bool homePageVisible(int item, const NodePrefs* p) const {
|
||||||
uint16_t bit = homePageBit(item);
|
uint16_t bit = homePageBit(item);
|
||||||
if (!bit) return false;
|
if (!bit) return false;
|
||||||
uint16_t mask = p ? p->home_pages_mask : HP_ALL;
|
uint16_t mask = (p && p->home_pages_mask) ? p->home_pages_mask : HP_ALL;
|
||||||
return (mask & bit) != 0;
|
return (mask & bit) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,7 +780,7 @@ const char* SettingsScreen::BATT_DISPLAY_LABELS[3] = { "icon", "%", "V" };
|
|||||||
class QuickMsgScreen : public UIScreen {
|
class QuickMsgScreen : public UIScreen {
|
||||||
UITask* _task;
|
UITask* _task;
|
||||||
|
|
||||||
enum Phase { MODE_SELECT, CONTACT_PICK, MSG_PICK, CHANNEL_PICK, CHANNEL_HIST, KEYBOARD };
|
enum Phase { MODE_SELECT, CONTACT_PICK, DM_HIST, MSG_PICK, CHANNEL_PICK, CHANNEL_HIST, KEYBOARD };
|
||||||
Phase _phase;
|
Phase _phase;
|
||||||
|
|
||||||
// MODE_SELECT
|
// MODE_SELECT
|
||||||
@@ -789,7 +789,7 @@ class QuickMsgScreen : public UIScreen {
|
|||||||
// CONTACT_PICK
|
// CONTACT_PICK
|
||||||
int _contact_sel, _contact_scroll;
|
int _contact_sel, _contact_scroll;
|
||||||
int _num_contacts;
|
int _num_contacts;
|
||||||
uint8_t _sorted[MAX_CONTACTS];
|
uint16_t _sorted[MAX_CONTACTS];
|
||||||
ContactInfo _sel_contact;
|
ContactInfo _sel_contact;
|
||||||
bool _room_mode; // true = picking a room server, false = picking a DM contact
|
bool _room_mode; // true = picking a room server, false = picking a DM contact
|
||||||
|
|
||||||
@@ -857,6 +857,13 @@ class QuickMsgScreen : public UIScreen {
|
|||||||
ChHistEntry _hist[CH_HIST_MAX];
|
ChHistEntry _hist[CH_HIST_MAX];
|
||||||
int _hist_head, _hist_count;
|
int _hist_head, _hist_count;
|
||||||
|
|
||||||
|
// DM_HIST
|
||||||
|
struct DmHistEntry { uint8_t prefix[4]; uint8_t outgoing; char text[80]; };
|
||||||
|
static const int DM_HIST_MAX = 32;
|
||||||
|
DmHistEntry _dm_hist[DM_HIST_MAX];
|
||||||
|
int _dm_hist_head, _dm_hist_count;
|
||||||
|
int _dm_hist_sel, _dm_hist_scroll;
|
||||||
|
|
||||||
static const int VISIBLE = 4;
|
static const int VISIBLE = 4;
|
||||||
static const int ITEM_H = 12;
|
static const int ITEM_H = 12;
|
||||||
static const int START_Y = 12;
|
static const int START_Y = 12;
|
||||||
@@ -928,6 +935,40 @@ class QuickMsgScreen : public UIScreen {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void storeDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
||||||
|
int pos;
|
||||||
|
if (_dm_hist_count < DM_HIST_MAX) {
|
||||||
|
pos = (_dm_hist_head + _dm_hist_count) % DM_HIST_MAX;
|
||||||
|
_dm_hist_count++;
|
||||||
|
} else {
|
||||||
|
pos = _dm_hist_head;
|
||||||
|
_dm_hist_head = (_dm_hist_head + 1) % DM_HIST_MAX;
|
||||||
|
}
|
||||||
|
memcpy(_dm_hist[pos].prefix, pub_key, 4);
|
||||||
|
_dm_hist[pos].outgoing = outgoing ? 1 : 0;
|
||||||
|
strncpy(_dm_hist[pos].text, text, sizeof(DmHistEntry::text) - 1);
|
||||||
|
_dm_hist[pos].text[sizeof(DmHistEntry::text) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
int dmHistCountForContact(const uint8_t* prefix) const {
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < _dm_hist_count; i++)
|
||||||
|
if (memcmp(_dm_hist[(_dm_hist_head + i) % DM_HIST_MAX].prefix, prefix, 4) == 0) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dmHistEntryForContact(const uint8_t* prefix, int j) const { // j=0 = newest
|
||||||
|
int n = 0;
|
||||||
|
for (int i = _dm_hist_count - 1; i >= 0; i--) {
|
||||||
|
int pos = (_dm_hist_head + i) % DM_HIST_MAX;
|
||||||
|
if (memcmp(_dm_hist[pos].prefix, prefix, 4) == 0) {
|
||||||
|
if (n == j) return pos;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
void afterSend(bool ok, const char* msg) {
|
void afterSend(bool ok, const char* msg) {
|
||||||
if (ok && _sending_to_channel) {
|
if (ok && _sending_to_channel) {
|
||||||
_hist_sel = 0;
|
_hist_sel = 0;
|
||||||
@@ -942,8 +983,14 @@ class QuickMsgScreen : public UIScreen {
|
|||||||
_unread_at_entry = 0;
|
_unread_at_entry = 0;
|
||||||
_viewing_max_seen = 0;
|
_viewing_max_seen = 0;
|
||||||
_task->showAlert("Sent!", 600);
|
_task->showAlert("Sent!", 600);
|
||||||
|
} else if (ok) {
|
||||||
|
storeDMMsg(_sel_contact.id.pub_key, true, msg);
|
||||||
|
_dm_hist_sel = 0;
|
||||||
|
_dm_hist_scroll = 0;
|
||||||
|
_phase = DM_HIST;
|
||||||
|
_task->showAlert("Sent!", 600);
|
||||||
} else {
|
} else {
|
||||||
_task->showAlert(ok ? "Sent!" : "Send failed", 1500);
|
_task->showAlert("Send failed", 1500);
|
||||||
_task->gotoHomeScreen();
|
_task->gotoHomeScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1029,6 +1076,8 @@ public:
|
|||||||
_hist_fullscreen(false), _hist_fs_scroll(0),
|
_hist_fullscreen(false), _hist_fs_scroll(0),
|
||||||
_unread_at_entry(0), _viewing_max_seen(0),
|
_unread_at_entry(0), _viewing_max_seen(0),
|
||||||
_hist_head(0), _hist_count(0),
|
_hist_head(0), _hist_count(0),
|
||||||
|
_dm_hist_head(0), _dm_hist_count(0),
|
||||||
|
_dm_hist_sel(-1), _dm_hist_scroll(0),
|
||||||
_kb_row(0), _kb_col(0), _kb_len(0),
|
_kb_row(0), _kb_col(0), _kb_len(0),
|
||||||
_kb_caps(false), _kb_ph_mode(false), _kb_ph_sel(0),
|
_kb_caps(false), _kb_ph_mode(false), _kb_ph_sel(0),
|
||||||
_ctx_open(false), _ctx_dirty(false), _ctx_sel(0) {
|
_ctx_open(false), _ctx_dirty(false), _ctx_sel(0) {
|
||||||
@@ -1055,6 +1104,14 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
||||||
|
storeDMMsg(pub_key, outgoing, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getDMUnreadTotal() const {
|
||||||
|
return _task->getDMUnreadTotal();
|
||||||
|
}
|
||||||
|
|
||||||
int getTotalChannelUnread() const {
|
int getTotalChannelUnread() const {
|
||||||
int total = 0;
|
int total = 0;
|
||||||
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) total += _ch_unread[i];
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) total += _ch_unread[i];
|
||||||
@@ -1101,11 +1158,10 @@ public:
|
|||||||
display.fillRect(0, 10, display.width(), 1);
|
display.fillRect(0, 10, display.width(), 1);
|
||||||
const char* opts[] = { "Direct message", "Channels", "Room Servers" };
|
const char* opts[] = { "Direct message", "Channels", "Room Servers" };
|
||||||
int badges[3] = {
|
int badges[3] = {
|
||||||
_task->getMsgCount() - _task->getRoomUnreadCount(), // DM only
|
getDMUnreadTotal(),
|
||||||
_task->getChannelUnreadCount(),
|
_task->getChannelUnreadCount(),
|
||||||
_task->getRoomUnreadCount()
|
_task->getRoomUnreadCount()
|
||||||
};
|
};
|
||||||
if (badges[0] < 0) badges[0] = 0;
|
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
int y = START_Y + i * ITEM_H;
|
int y = START_Y + i * ITEM_H;
|
||||||
bool sel = (i == _mode_sel);
|
bool sel = (i == _mode_sel);
|
||||||
@@ -1159,11 +1215,18 @@ public:
|
|||||||
display.print(sel ? ">" : " ");
|
display.print(sel ? ">" : " ");
|
||||||
char filtered[sizeof(c.name)];
|
char filtered[sizeof(c.name)];
|
||||||
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
||||||
display.drawTextEllipsized(8, y, display.width() - 24, filtered);
|
uint8_t dm_unread = _task->getDMUnread(c.id.pub_key);
|
||||||
char hop[5];
|
char badge[5] = "";
|
||||||
snprintf(hop, sizeof(hop), c.out_path_len == 0xFF ? "D" : "%dh", (int)c.out_path_len);
|
int bw = 0;
|
||||||
display.setCursor(display.width() - display.getTextWidth(hop) - 1, y);
|
if (dm_unread > 0) {
|
||||||
display.print(hop);
|
snprintf(badge, sizeof(badge), "%d", (int)dm_unread);
|
||||||
|
bw = display.getTextWidth(badge) + 2;
|
||||||
|
}
|
||||||
|
display.drawTextEllipsized(8, y, display.width() - 8 - bw - 1, filtered);
|
||||||
|
if (dm_unread > 0) {
|
||||||
|
display.setCursor(display.width() - bw + 1, y);
|
||||||
|
display.print(badge);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
@@ -1243,6 +1306,74 @@ public:
|
|||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (_phase == DM_HIST) {
|
||||||
|
char title[24];
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
|
char filtered_name[sizeof(_sel_contact.name)];
|
||||||
|
display.translateUTF8ToBlocks(filtered_name, _sel_contact.name, sizeof(filtered_name));
|
||||||
|
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
||||||
|
display.drawTextCentered(display.width()/2, 0, title);
|
||||||
|
display.fillRect(0, 9, display.width(), 1);
|
||||||
|
|
||||||
|
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
|
||||||
|
|
||||||
|
for (int i = 0; i < HIST_VISIBLE && (_dm_hist_scroll + i) < dm_count; i++) {
|
||||||
|
int item = _dm_hist_scroll + i;
|
||||||
|
bool sel = (item == _dm_hist_sel);
|
||||||
|
int y = HIST_START_Y + i * HIST_ITEM_H;
|
||||||
|
|
||||||
|
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, item);
|
||||||
|
if (ring_pos < 0) continue;
|
||||||
|
|
||||||
|
const DmHistEntry& e = _dm_hist[ring_pos];
|
||||||
|
const char* sender = e.outgoing ? "Me" : filtered_name;
|
||||||
|
|
||||||
|
if (sel) {
|
||||||
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
|
display.fillRect(0, y, display.width(), HIST_BOX_H);
|
||||||
|
display.setColor(DisplayDriver::DARK);
|
||||||
|
display.drawTextEllipsized(3, y + 1, display.width() - 6, sender);
|
||||||
|
display.drawTextEllipsized(3, y + 10, display.width() - 6, e.text);
|
||||||
|
} else {
|
||||||
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
|
display.drawRect(0, y, display.width(), HIST_BOX_H);
|
||||||
|
display.fillRect(1, y + 1, display.width() - 2, 8);
|
||||||
|
display.setColor(DisplayDriver::DARK);
|
||||||
|
display.drawTextEllipsized(3, y + 1, display.width() - 6, sender);
|
||||||
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
|
display.drawTextEllipsized(3, y + 10, display.width() - 6, e.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dm_count == 0) {
|
||||||
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
|
display.drawTextCentered(display.width()/2, 32, "No messages yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
|
if (_dm_hist_scroll > 0) {
|
||||||
|
display.setCursor(display.width() - 6, HIST_START_Y + 1);
|
||||||
|
display.print("^");
|
||||||
|
}
|
||||||
|
if (_dm_hist_scroll + HIST_VISIBLE < dm_count) {
|
||||||
|
display.setCursor(display.width() - 6, HIST_START_Y + HIST_VISIBLE * HIST_ITEM_H - 10);
|
||||||
|
display.print("v");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool compose_sel = (_dm_hist_sel == -1);
|
||||||
|
const char* ctxt = "[+ send]";
|
||||||
|
int ctw = display.getTextWidth(ctxt);
|
||||||
|
int cbx = 1, cby = 55;
|
||||||
|
if (compose_sel) {
|
||||||
|
display.fillRect(cbx, cby - 1, ctw + 4, 9);
|
||||||
|
display.setColor(DisplayDriver::DARK);
|
||||||
|
}
|
||||||
|
display.setCursor(cbx + 2, cby);
|
||||||
|
display.print(ctxt);
|
||||||
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
|
return dm_count > 0 ? 500 : 2000;
|
||||||
|
|
||||||
} else if (_phase == CHANNEL_HIST) {
|
} else if (_phase == CHANNEL_HIST) {
|
||||||
if (_hist_fullscreen && _hist_sel >= 0) {
|
if (_hist_fullscreen && _hist_sel >= 0) {
|
||||||
int fs_hist_count = histCountForChannel(_sel_channel_idx);
|
int fs_hist_count = histCountForChannel(_sel_channel_idx);
|
||||||
@@ -1529,9 +1660,10 @@ public:
|
|||||||
}
|
}
|
||||||
if (c == KEY_ENTER && _num_contacts > 0) {
|
if (c == KEY_ENTER && _num_contacts > 0) {
|
||||||
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
||||||
_sending_to_channel = false;
|
_task->clearDMUnread(_sel_contact.id.pub_key);
|
||||||
setupMsgPick();
|
_dm_hist_sel = -1;
|
||||||
_phase = MSG_PICK;
|
_dm_hist_scroll = 0;
|
||||||
|
_phase = DM_HIST;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1588,6 +1720,34 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (_phase == DM_HIST) {
|
||||||
|
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
|
||||||
|
if (c == KEY_CANCEL) { _phase = CONTACT_PICK; return true; }
|
||||||
|
if (c == KEY_UP) {
|
||||||
|
if (_dm_hist_sel > 0) {
|
||||||
|
_dm_hist_sel--;
|
||||||
|
if (_dm_hist_sel < _dm_hist_scroll) _dm_hist_scroll = _dm_hist_sel;
|
||||||
|
} else if (_dm_hist_sel == 0) {
|
||||||
|
_dm_hist_sel = -1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (c == KEY_DOWN) {
|
||||||
|
if (_dm_hist_sel == -1 && dm_count > 0) { _dm_hist_sel = 0; _dm_hist_scroll = 0; }
|
||||||
|
else if (_dm_hist_sel >= 0 && _dm_hist_sel < dm_count - 1) {
|
||||||
|
_dm_hist_sel++;
|
||||||
|
if (_dm_hist_sel >= _dm_hist_scroll + HIST_VISIBLE)
|
||||||
|
_dm_hist_scroll = _dm_hist_sel - HIST_VISIBLE + 1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (c == KEY_ENTER) {
|
||||||
|
_sending_to_channel = false;
|
||||||
|
setupMsgPick();
|
||||||
|
_phase = MSG_PICK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} else if (_phase == CHANNEL_HIST) {
|
} else if (_phase == CHANNEL_HIST) {
|
||||||
int ch_hist_count = histCountForChannel(_sel_channel_idx);
|
int ch_hist_count = histCountForChannel(_sel_channel_idx);
|
||||||
if (_hist_fullscreen) {
|
if (_hist_fullscreen) {
|
||||||
@@ -1722,7 +1882,7 @@ public:
|
|||||||
} else { // MSG_PICK
|
} else { // MSG_PICK
|
||||||
int total_msg_items = 1 + _active_msg_count;
|
int total_msg_items = 1 + _active_msg_count;
|
||||||
if (c == KEY_CANCEL) {
|
if (c == KEY_CANCEL) {
|
||||||
_phase = _sending_to_channel ? CHANNEL_HIST : CONTACT_PICK;
|
_phase = _sending_to_channel ? CHANNEL_HIST : DM_HIST;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == KEY_UP && _msg_sel > 0) {
|
if (c == KEY_UP && _msg_sel > 0) {
|
||||||
@@ -1811,7 +1971,7 @@ class HomeScreen : public UIScreen {
|
|||||||
bool isPageVisible(int page) const {
|
bool isPageVisible(int page) const {
|
||||||
int bit = pageBit(page);
|
int bit = pageBit(page);
|
||||||
if (bit < 0) return true;
|
if (bit < 0) return true;
|
||||||
uint16_t mask = _node_prefs ? _node_prefs->home_pages_mask : HP_ALL;
|
uint16_t mask = (_node_prefs && _node_prefs->home_pages_mask) ? _node_prefs->home_pages_mask : HP_ALL;
|
||||||
return (mask >> bit) & 1;
|
return (mask >> bit) & 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2208,8 +2368,7 @@ public:
|
|||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
display.drawTextCentered(display.width() / 2, 22, "Messages");
|
display.drawTextCentered(display.width() / 2, 22, "Messages");
|
||||||
int total_unread = _task->getMsgCount() + _task->getChannelUnreadCount();
|
int total_unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount();
|
||||||
// getMsgCount() already includes room msgs via offline_queue_len; avoid double-count
|
|
||||||
if (total_unread > 0) {
|
if (total_unread > 0) {
|
||||||
char badge[20];
|
char badge[20];
|
||||||
snprintf(badge, sizeof(badge), "%d unread", total_unread);
|
snprintf(badge, sizeof(badge), "%d unread", total_unread);
|
||||||
@@ -2398,6 +2557,17 @@ int UITask::getChannelUnreadCount() const {
|
|||||||
return ((QuickMsgScreen*)quick_msg)->getTotalChannelUnread();
|
return ((QuickMsgScreen*)quick_msg)->getTotalChannelUnread();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
||||||
|
((QuickMsgScreen*)quick_msg)->addDMMsg(pub_key, outgoing, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UITask::getDMUnreadTotal() const {
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++)
|
||||||
|
total += _dm_unread_table[i].count;
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
void UITask::showAlert(const char* text, int duration_millis) {
|
void UITask::showAlert(const char* text, int duration_millis) {
|
||||||
snprintf(_alert, sizeof(_alert), "%s", text);
|
snprintf(_alert, sizeof(_alert), "%s", text);
|
||||||
_alert_expiry = millis() + duration_millis;
|
_alert_expiry = millis() + duration_millis;
|
||||||
@@ -2455,12 +2625,26 @@ void UITask::msgRead(int msgcount) {
|
|||||||
_msgcount = msgcount;
|
_msgcount = msgcount;
|
||||||
if (msgcount == 0) {
|
if (msgcount == 0) {
|
||||||
_room_unread = 0;
|
_room_unread = 0;
|
||||||
|
memset(_dm_unread_table, 0, sizeof(_dm_unread_table));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type) {
|
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type, const uint8_t* pub_key) {
|
||||||
_msgcount = msgcount;
|
_msgcount = msgcount;
|
||||||
if (contact_type == ADV_TYPE_ROOM && _room_unread < _msgcount) _room_unread++;
|
if (contact_type == ADV_TYPE_ROOM && _room_unread < _msgcount) _room_unread++;
|
||||||
|
if (contact_type == ADV_TYPE_CHAT && pub_key != nullptr) {
|
||||||
|
int slot = -1, empty_slot = -1;
|
||||||
|
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++) {
|
||||||
|
if (_dm_unread_table[i].count > 0 && memcmp(_dm_unread_table[i].prefix, pub_key, 4) == 0) { slot = i; break; }
|
||||||
|
if (empty_slot < 0 && _dm_unread_table[i].count == 0) empty_slot = i;
|
||||||
|
}
|
||||||
|
if (slot >= 0) {
|
||||||
|
if (_dm_unread_table[slot].count < 99) _dm_unread_table[slot].count++;
|
||||||
|
} else if (empty_slot >= 0) {
|
||||||
|
memcpy(_dm_unread_table[empty_slot].prefix, pub_key, 4);
|
||||||
|
_dm_unread_table[empty_slot].count = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
char alert_buf[80];
|
char alert_buf[80];
|
||||||
snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", from_name);
|
snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", from_name);
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class UITask : public AbstractUITask {
|
|||||||
int _msgcount;
|
int _msgcount;
|
||||||
int _room_unread;
|
int _room_unread;
|
||||||
int _last_notif_ch_idx;
|
int _last_notif_ch_idx;
|
||||||
|
struct DMUnreadEntry { uint8_t prefix[4]; uint8_t count; };
|
||||||
|
static const int DM_UNREAD_TABLE_SIZE = 16;
|
||||||
|
DMUnreadEntry _dm_unread_table[DM_UNREAD_TABLE_SIZE];
|
||||||
unsigned long ui_started_at, next_batt_chck;
|
unsigned long ui_started_at, next_batt_chck;
|
||||||
uint16_t _batt_mv; // EMA-filtered battery voltage
|
uint16_t _batt_mv; // EMA-filtered battery voltage
|
||||||
int next_backlight_btn_check = 0;
|
int next_backlight_btn_check = 0;
|
||||||
@@ -78,6 +81,7 @@ public:
|
|||||||
_batt_mv = 0;
|
_batt_mv = 0;
|
||||||
_msgcount = _room_unread = 0;
|
_msgcount = _room_unread = 0;
|
||||||
_last_notif_ch_idx = -1;
|
_last_notif_ch_idx = -1;
|
||||||
|
memset(_dm_unread_table, 0, sizeof(_dm_unread_table));
|
||||||
curr = NULL;
|
curr = NULL;
|
||||||
}
|
}
|
||||||
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
|
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
|
||||||
@@ -95,10 +99,23 @@ public:
|
|||||||
bool isMelodyPlaying();
|
bool isMelodyPlaying();
|
||||||
void showAlert(const char* text, int duration_millis);
|
void showAlert(const char* text, int duration_millis);
|
||||||
void addChannelMsg(uint8_t channel_idx, const char* text) override;
|
void addChannelMsg(uint8_t channel_idx, const char* text) override;
|
||||||
|
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) override;
|
||||||
|
int getDMUnreadTotal() const;
|
||||||
int getMsgCount() const { return _msgcount; }
|
int getMsgCount() const { return _msgcount; }
|
||||||
int getChannelUnreadCount() const;
|
int getChannelUnreadCount() const;
|
||||||
int getRoomUnreadCount() const { return _room_unread; }
|
int getRoomUnreadCount() const { return _room_unread; }
|
||||||
void clearRoomUnread() { _room_unread = 0; }
|
void clearRoomUnread() { _room_unread = 0; }
|
||||||
|
uint8_t getDMUnread(const uint8_t* pub_key) const {
|
||||||
|
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++)
|
||||||
|
if (_dm_unread_table[i].count > 0 && memcmp(_dm_unread_table[i].prefix, pub_key, 4) == 0)
|
||||||
|
return _dm_unread_table[i].count;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
void clearDMUnread(const uint8_t* pub_key) {
|
||||||
|
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++)
|
||||||
|
if (_dm_unread_table[i].count > 0 && memcmp(_dm_unread_table[i].prefix, pub_key, 4) == 0)
|
||||||
|
{ _dm_unread_table[i].count = 0; return; }
|
||||||
|
}
|
||||||
bool hasDisplay() const { return _display != NULL; }
|
bool hasDisplay() const { return _display != NULL; }
|
||||||
bool isButtonPressed() const;
|
bool isButtonPressed() const;
|
||||||
|
|
||||||
@@ -130,7 +147,7 @@ public:
|
|||||||
|
|
||||||
// from AbstractUITask
|
// from AbstractUITask
|
||||||
void msgRead(int msgcount) override;
|
void msgRead(int msgcount) override;
|
||||||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type = 0) override;
|
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type = 0, const uint8_t* pub_key = nullptr) override;
|
||||||
void notify(UIEventType t = UIEventType::none) override;
|
void notify(UIEventType t = UIEventType::none) override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user