From 4a4ff76f10f84afc4676f64efa0b139305156ef1 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 13 May 2026 10:25:55 +0200 Subject: [PATCH] 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 --- examples/companion_radio/AbstractUITask.h | 3 +- examples/companion_radio/MyMesh.cpp | 20 +- examples/companion_radio/MyMeshBot.h | 17 +- examples/companion_radio/ui-new/UITask.cpp | 222 +++++++++++++++++++-- examples/companion_radio/ui-new/UITask.h | 19 +- 5 files changed, 255 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index ecac65d5..105f00a1 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -40,8 +40,9 @@ public: void enableSerial() { _serial->enable(); } void disableSerial() { _serial->disable(); } 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 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; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4d8669fc..a670be14 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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 bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; 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); + if (from.type == ADV_TYPE_CHAT) + _ui->addDMMsg(from.id.pub_key, false, text); } #endif } @@ -881,6 +883,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.bot_trigger[0] = '\0'; _prefs.bot_reply_dm[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.tz_offset_hours = 0; // UTC 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); uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame if (recipient) { + uint8_t saved_type = recipient->type; // type is authoritative from advert, not app updateContactFromFrame(*recipient, last_mod, cmd_frame, len); + if (saved_type != ADV_TYPE_NONE) recipient->type = saved_type; recipient->lastmod = last_mod; dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); writeOKFrame(); @@ -1690,13 +1695,22 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_NOT_FOUND); } } 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) { uint8_t channel_idx = cmd_frame[1]; ChannelDetails channel; StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32); 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)) { saveChannels(); writeOKFrame(); diff --git a/examples/companion_radio/MyMeshBot.h b/examples/companion_radio/MyMeshBot.h index a413f76a..b5068237 100644 --- a/examples/companion_radio/MyMeshBot.h +++ b/examples/companion_radio/MyMeshBot.h @@ -3,6 +3,7 @@ // Included at the bottom of MyMesh.cpp after all class definitions. 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] && millis() - _bot_last_reply_ms > 10000UL)) return; @@ -24,8 +25,12 @@ void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text) { sensors.node_lat != 0.0 || sensors.node_lon != 0.0, ts, _prefs.tz_offset_hours); 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(); +#ifdef DISPLAY_CLASS + if (_ui) _ui->addDMMsg(from.id.pub_key, true, expanded); +#endif + } } 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, ts, _prefs.tz_offset_hours); 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(); +#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 + } } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 1a33d263..d3939492 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -295,7 +295,7 @@ class SettingsScreen : public UIScreen { bool homePageVisible(int item, const NodePrefs* p) const { uint16_t bit = homePageBit(item); 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; } @@ -780,7 +780,7 @@ const char* SettingsScreen::BATT_DISPLAY_LABELS[3] = { "icon", "%", "V" }; class QuickMsgScreen : public UIScreen { 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; // MODE_SELECT @@ -789,7 +789,7 @@ class QuickMsgScreen : public UIScreen { // CONTACT_PICK int _contact_sel, _contact_scroll; int _num_contacts; - uint8_t _sorted[MAX_CONTACTS]; + uint16_t _sorted[MAX_CONTACTS]; ContactInfo _sel_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]; 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 ITEM_H = 12; static const int START_Y = 12; @@ -928,6 +935,40 @@ class QuickMsgScreen : public UIScreen { 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) { if (ok && _sending_to_channel) { _hist_sel = 0; @@ -942,8 +983,14 @@ class QuickMsgScreen : public UIScreen { _unread_at_entry = 0; _viewing_max_seen = 0; _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 { - _task->showAlert(ok ? "Sent!" : "Send failed", 1500); + _task->showAlert("Send failed", 1500); _task->gotoHomeScreen(); } } @@ -1029,6 +1076,8 @@ public: _hist_fullscreen(false), _hist_fs_scroll(0), _unread_at_entry(0), _viewing_max_seen(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_caps(false), _kb_ph_mode(false), _kb_ph_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 total = 0; 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); const char* opts[] = { "Direct message", "Channels", "Room Servers" }; int badges[3] = { - _task->getMsgCount() - _task->getRoomUnreadCount(), // DM only + getDMUnreadTotal(), _task->getChannelUnreadCount(), _task->getRoomUnreadCount() }; - if (badges[0] < 0) badges[0] = 0; for (int i = 0; i < 3; i++) { int y = START_Y + i * ITEM_H; bool sel = (i == _mode_sel); @@ -1159,11 +1215,18 @@ public: display.print(sel ? ">" : " "); char filtered[sizeof(c.name)]; display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered)); - display.drawTextEllipsized(8, y, display.width() - 24, filtered); - char hop[5]; - snprintf(hop, sizeof(hop), c.out_path_len == 0xFF ? "D" : "%dh", (int)c.out_path_len); - display.setCursor(display.width() - display.getTextWidth(hop) - 1, y); - display.print(hop); + uint8_t dm_unread = _task->getDMUnread(c.id.pub_key); + char badge[5] = ""; + int bw = 0; + if (dm_unread > 0) { + 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); @@ -1243,6 +1306,74 @@ public: 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) { if (_hist_fullscreen && _hist_sel >= 0) { int fs_hist_count = histCountForChannel(_sel_channel_idx); @@ -1529,9 +1660,10 @@ public: } if (c == KEY_ENTER && _num_contacts > 0) { if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) { - _sending_to_channel = false; - setupMsgPick(); - _phase = MSG_PICK; + _task->clearDMUnread(_sel_contact.id.pub_key); + _dm_hist_sel = -1; + _dm_hist_scroll = 0; + _phase = DM_HIST; } return true; } @@ -1588,6 +1720,34 @@ public: 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) { int ch_hist_count = histCountForChannel(_sel_channel_idx); if (_hist_fullscreen) { @@ -1722,7 +1882,7 @@ public: } else { // MSG_PICK int total_msg_items = 1 + _active_msg_count; if (c == KEY_CANCEL) { - _phase = _sending_to_channel ? CHANNEL_HIST : CONTACT_PICK; + _phase = _sending_to_channel ? CHANNEL_HIST : DM_HIST; return true; } if (c == KEY_UP && _msg_sel > 0) { @@ -1811,7 +1971,7 @@ class HomeScreen : public UIScreen { bool isPageVisible(int page) const { int bit = pageBit(page); 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; } @@ -2208,8 +2368,7 @@ public: display.setColor(DisplayDriver::LIGHT); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 22, "Messages"); - int total_unread = _task->getMsgCount() + _task->getChannelUnreadCount(); - // getMsgCount() already includes room msgs via offline_queue_len; avoid double-count + int total_unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount(); if (total_unread > 0) { char badge[20]; snprintf(badge, sizeof(badge), "%d unread", total_unread); @@ -2398,6 +2557,17 @@ int UITask::getChannelUnreadCount() const { 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) { snprintf(_alert, sizeof(_alert), "%s", text); _alert_expiry = millis() + duration_millis; @@ -2455,12 +2625,26 @@ void UITask::msgRead(int msgcount) { _msgcount = msgcount; if (msgcount == 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; 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]; snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", from_name); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 6b809c01..f538d742 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -38,6 +38,9 @@ class UITask : public AbstractUITask { int _msgcount; int _room_unread; 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; uint16_t _batt_mv; // EMA-filtered battery voltage int next_backlight_btn_check = 0; @@ -78,6 +81,7 @@ public: _batt_mv = 0; _msgcount = _room_unread = 0; _last_notif_ch_idx = -1; + memset(_dm_unread_table, 0, sizeof(_dm_unread_table)); curr = NULL; } void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); @@ -95,10 +99,23 @@ public: bool isMelodyPlaying(); void showAlert(const char* text, int duration_millis); 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 getChannelUnreadCount() const; int getRoomUnreadCount() const { return _room_unread; } 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 isButtonPressed() const; @@ -130,7 +147,7 @@ public: // from AbstractUITask 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 loop() override;