mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
refactor(ui-core): add UI Core skeleton; move MessageHistory + DM unread into it
New examples/companion_radio/ui-core/ (header-only for now, reached from ui-new/UITask.cpp by relative include, so no platformio.ini changes): UiCore facade, MessageHistory (moved verbatim), DmUnreadTable (extracted from UITask). UITask heap-allocates the Core in begin(); MessagesScreen binds to core().history. Behaviour unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
// Per-contact DM unread counters. Kept apart from the DM ring in MessageHistory
|
||||
// (which has no per-contact index), keyed by the same 4-byte pub_key prefix,
|
||||
// and always read back clamped to what the ring still holds for that contact --
|
||||
// the same self-healing shape as MessageHistory::chUnread() for channels.
|
||||
//
|
||||
// Header-only UI Core model (see docs/development/ui-core.md); no drawing, no
|
||||
// screen state.
|
||||
|
||||
class DmUnreadTable {
|
||||
public:
|
||||
static const int SIZE = 16;
|
||||
|
||||
DmUnreadTable() { clearAll(); }
|
||||
|
||||
// An incoming DM from pub_key is about to be stored (called before the ring
|
||||
// insert). A sender that doesn't fit in the fixed table is simply not counted.
|
||||
void onIncoming(const uint8_t* pub_key) {
|
||||
int slot = -1, empty_slot = -1;
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
if (_t[i].count > 0 && memcmp(_t[i].prefix, pub_key, 4) == 0) { slot = i; break; }
|
||||
if (empty_slot < 0 && _t[i].count == 0) empty_slot = i;
|
||||
}
|
||||
if (slot >= 0) {
|
||||
if (_t[slot].count < 99) _t[slot].count++;
|
||||
} else if (empty_slot >= 0) {
|
||||
memcpy(_t[empty_slot].prefix, pub_key, 4);
|
||||
_t[empty_slot].count = 1;
|
||||
_t[empty_slot].overflow = false; // fresh contact -- don't inherit a stale flag from whoever held this slot before
|
||||
}
|
||||
// Eviction/overflow is checked in afterInsert(), after the ring insert.
|
||||
}
|
||||
|
||||
// The DM ring (unlike the channel ring) doesn't proactively decrement the
|
||||
// unread counters as it evicts old entries, so catch it here, right after
|
||||
// the insert: a raw count claiming more unread than the ring still holds
|
||||
// for that contact means one of their unread entries was just evicted. Any
|
||||
// contact can lose one -- not just this sender -- so check every slot.
|
||||
// Clamp back to the honest value and flag it (mirrors MessageHistory's
|
||||
// channel-side fix). Must run after the insert, not in onIncoming() (called
|
||||
// before it), or the new message itself reads as evicted.
|
||||
void afterInsert(const MessageHistory& h) {
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
if (_t[i].count == 0) continue;
|
||||
int held = h.dmHistCountForContact(_t[i].prefix);
|
||||
if (_t[i].count > held) {
|
||||
_t[i].count = (uint8_t)held;
|
||||
_t[i].overflow = held > 0; // held == 0 frees the slot -- nothing left to flag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int total(const MessageHistory& h) const {
|
||||
int total = 0;
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
if (_t[i].count == 0) continue;
|
||||
int held = h.dmHistCountForContact(_t[i].prefix);
|
||||
total += (_t[i].count < held) ? _t[i].count : held;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
uint8_t get(const MessageHistory& h, const uint8_t* pub_key) const {
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
if (_t[i].count > 0 && memcmp(_t[i].prefix, pub_key, 4) == 0) {
|
||||
int held = h.dmHistCountForContact(pub_key);
|
||||
return _t[i].count < held ? _t[i].count : (uint8_t)held;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// True once an unread entry for this contact was evicted off the DM ring
|
||||
// before ever being seen -- count is honest but understates the real total.
|
||||
bool overflow(const uint8_t* pub_key) const {
|
||||
for (int i = 0; i < SIZE; i++)
|
||||
if (_t[i].count > 0 && memcmp(_t[i].prefix, pub_key, 4) == 0)
|
||||
return _t[i].overflow;
|
||||
return false;
|
||||
}
|
||||
bool anyOverflow() const {
|
||||
for (int i = 0; i < SIZE; i++)
|
||||
if (_t[i].count > 0 && _t[i].overflow) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear(const uint8_t* pub_key) {
|
||||
for (int i = 0; i < SIZE; i++)
|
||||
if (_t[i].count > 0 && memcmp(_t[i].prefix, pub_key, 4) == 0)
|
||||
{ _t[i].count = 0; _t[i].overflow = false; return; }
|
||||
}
|
||||
void clearAll() { memset(_t, 0, sizeof(_t)); }
|
||||
|
||||
// Frees any slot whose ring occupancy has dropped to zero (evicted or
|
||||
// deduped-away messages) so a genuinely new sender isn't starved once the
|
||||
// fixed table fills with stale entries.
|
||||
void reconcile(const MessageHistory& h) {
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
if (_t[i].count == 0) continue;
|
||||
if (h.dmHistCountForContact(_t[i].prefix) == 0) {
|
||||
_t[i].count = 0; // ring no longer holds anything for this sender -- free the slot
|
||||
_t[i].overflow = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry { uint8_t prefix[4]; uint8_t count; bool overflow; };
|
||||
Entry _t[SIZE];
|
||||
};
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
// scroll, the unread "viewing session" bookkeeping, the room-login table, and
|
||||
// all rendering, and reaches entries through the accessors below.
|
||||
//
|
||||
// Single-TU fragment: included by UITask.cpp before MessagesScreen.h. AckState,
|
||||
// UI Core model, owned by UiCore (UiCore.h). Single-TU fragment: reached from
|
||||
// the frontend's UITask.cpp via UiCore.h, before MessagesScreen.h. AckState,
|
||||
// MSG_TEXT_BUF and the two entry structs are file-scope (not nested) so the
|
||||
// phase machine in MessagesScreen keeps referring to them unqualified.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
// UI Core: hardware-independent UI state and logic shared by every frontend
|
||||
// (ui-new today, ui-lvgl later). See docs/development/ui-core.md.
|
||||
//
|
||||
// For now the Core is header-only and compiled as part of the frontend's single
|
||||
// translation unit (ui-new/UITask.cpp includes this file), so no platformio.ini
|
||||
// needs a new source filter or include path. It relies on the same globals the
|
||||
// frontend does (the_mesh, rtc_clock).
|
||||
|
||||
#include "MessageHistory.h"
|
||||
#include "DmUnreadTable.h"
|
||||
|
||||
class UiCore {
|
||||
public:
|
||||
// ── Models ────────────────────────────────────────────────────────────────
|
||||
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
||||
DmUnreadTable dm_unread; // per-contact DM unread counters
|
||||
|
||||
// ── DM unread (clamped to what the DM ring still holds) ──────────────────
|
||||
void noteIncomingDM(const uint8_t* pub_key) { dm_unread.onIncoming(pub_key); }
|
||||
void afterDMInsert() { dm_unread.afterInsert(history); }
|
||||
int dmUnreadTotal() const { return dm_unread.total(history); }
|
||||
uint8_t dmUnread(const uint8_t* pub_key) const { return dm_unread.get(history, pub_key); }
|
||||
bool dmUnreadOverflow(const uint8_t* pub_key) const { return dm_unread.overflow(pub_key); }
|
||||
bool anyDMUnreadOverflow() const { return dm_unread.anyOverflow(); }
|
||||
void clearDMUnread(const uint8_t* pub_key) { dm_unread.clear(pub_key); }
|
||||
void clearAllDMUnread() { dm_unread.clearAll(); }
|
||||
void reconcileDMUnread() { dm_unread.reconcile(history); }
|
||||
|
||||
// All messages synced to the app: nothing is unread any more (room unread
|
||||
// still lives in the frontend).
|
||||
void clearAllUnread() {
|
||||
dm_unread.clearAll();
|
||||
history.clearAllChannelUnread();
|
||||
}
|
||||
};
|
||||
@@ -136,12 +136,13 @@ class MessagesScreen : public UIScreen {
|
||||
bool _pick_bot_room = false;
|
||||
|
||||
// The message-history rings (channel + DM), their per-entry delivery state and
|
||||
// per-channel unread counters live in this store (see MessageHistory.h). The
|
||||
// per-channel unread counters live in the UI Core's store (ui-core/
|
||||
// MessageHistory.h); this screen binds to it by reference. The
|
||||
// phase machine below keeps only the view state — selection, scroll, the
|
||||
// fullscreen readers — and reaches entries through _history's accessors. The
|
||||
// shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are file-
|
||||
// scope, so they're still referred to unqualified throughout this screen.
|
||||
MessageHistory _history;
|
||||
MessageHistory& _history;
|
||||
|
||||
// DM_HIST view state (the ring itself is in _history).
|
||||
int _dm_hist_sel, _dm_hist_scroll;
|
||||
@@ -942,10 +943,11 @@ public:
|
||||
_msg_sel(0), _msg_scroll(0), _active_msg_count(0),
|
||||
_hist_sel(0), _hist_scroll(0),
|
||||
_unread_at_entry(0), _viewing_max_seen(0),
|
||||
_history(task->core().history),
|
||||
_dm_hist_sel(-1), _dm_hist_scroll(0),
|
||||
_ctx_dirty(false), _pin_picker_active(false), _direct_entry(false), _reply_mode(false),
|
||||
_ch_view(task) {
|
||||
// The history rings + per-channel unread counters init in MessageHistory.
|
||||
// The history rings + per-channel unread counters live in UiCore.
|
||||
}
|
||||
|
||||
// Forwarded to UITask's GPS duty-cycle hold — true while showing the
|
||||
@@ -1111,7 +1113,7 @@ public:
|
||||
bool anyChannelUnreadOverflow() const { return _history.anyChannelUnreadOverflow(); }
|
||||
|
||||
// How many DM ring entries this contact/room currently holds -- lets UITask
|
||||
// clamp its separate _dm_unread_table counters to what the shared 32-slot DM
|
||||
// clamp the Core's separate DM unread counters to what the shared 32-slot DM
|
||||
// ring actually still has, the same self-healing shape as the channel fix.
|
||||
int dmHistCountForContact(const uint8_t* pub_key) const { return _history.dmHistCountForContact(pub_key); }
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ static const int QUICK_MSGS_MAX = 10;
|
||||
#include "FullscreenMsgView.h"
|
||||
#include "SensorPlaceholders.h"
|
||||
#include "SettingsScreen.h"
|
||||
#include "MessageHistory.h" // RAM history rings (DM + channel) used by MessagesScreen
|
||||
#include "../ui-core/UiCore.h" // shared UI Core: history rings + unread models (MessagesScreen views them)
|
||||
#include "MessagesScreen.h"
|
||||
|
||||
// ── Custom screens (separate files to ease upstream merges) ───────────────────
|
||||
@@ -1654,6 +1654,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
_ping_snr_back_x4 = 0;
|
||||
_ping_rtt_ms = 0;
|
||||
|
||||
_core = new UiCore(); // before any screen -- MessagesScreen binds to its history
|
||||
splash = new SplashScreen(this);
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
syncLockToHome(); // booted locked (e.g. cover closed) → home starts on the LOCK page
|
||||
@@ -2010,53 +2011,15 @@ void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, u
|
||||
uint32_t ack_tag, uint32_t ack_deadline_ms, uint8_t resends,
|
||||
const uint8_t* path, uint8_t path_len) {
|
||||
((MessagesScreen*)messages_screen)->addDMMsg(pub_key, outgoing, text, sender_timestamp, ack_tag, ack_deadline_ms, resends, path, path_len);
|
||||
// The DM ring (unlike the channel ring) doesn't proactively decrement the
|
||||
// unread counters as it evicts old entries, so catch it here, right after
|
||||
// the insert: a raw count claiming more unread than the ring still holds
|
||||
// for that contact means one of their unread entries was just evicted. Any
|
||||
// contact can lose one -- not just this sender -- so check every slot.
|
||||
// Clamp back to the honest value and flag it (mirrors MessageHistory's
|
||||
// channel-side fix). Must run after the insert, not in newMsg() (called
|
||||
// before it), or the new message itself reads as evicted.
|
||||
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++) {
|
||||
if (_dm_unread_table[i].count == 0) continue;
|
||||
int held = ((MessagesScreen*)messages_screen)->dmHistCountForContact(_dm_unread_table[i].prefix);
|
||||
if (_dm_unread_table[i].count > held) {
|
||||
_dm_unread_table[i].count = (uint8_t)held;
|
||||
_dm_unread_table[i].overflow = held > 0; // held == 0 frees the slot -- nothing left to flag
|
||||
}
|
||||
}
|
||||
_core->afterDMInsert();
|
||||
}
|
||||
|
||||
int UITask::getDMUnreadTotal() const {
|
||||
int total = 0;
|
||||
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++) {
|
||||
if (_dm_unread_table[i].count == 0) continue;
|
||||
int held = ((MessagesScreen*)messages_screen)->dmHistCountForContact(_dm_unread_table[i].prefix);
|
||||
total += (_dm_unread_table[i].count < held) ? _dm_unread_table[i].count : held;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
uint8_t UITask::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) {
|
||||
int held = ((MessagesScreen*)messages_screen)->dmHistCountForContact(pub_key);
|
||||
return _dm_unread_table[i].count < held ? _dm_unread_table[i].count : (uint8_t)held;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void UITask::reconcileDMUnread() {
|
||||
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++) {
|
||||
if (_dm_unread_table[i].count == 0) continue;
|
||||
if (((MessagesScreen*)messages_screen)->dmHistCountForContact(_dm_unread_table[i].prefix) == 0) {
|
||||
_dm_unread_table[i].count = 0; // ring no longer holds anything for this sender -- free the slot
|
||||
_dm_unread_table[i].overflow = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
int UITask::getDMUnreadTotal() const { return _core->dmUnreadTotal(); }
|
||||
uint8_t UITask::getDMUnread(const uint8_t* pub_key) const { return _core->dmUnread(pub_key); }
|
||||
bool UITask::getDMUnreadOverflow(const uint8_t* pub_key) const { return _core->dmUnreadOverflow(pub_key); }
|
||||
bool UITask::getAnyDMUnreadOverflow() const { return _core->anyDMUnreadOverflow(); }
|
||||
void UITask::clearDMUnread(const uint8_t* pub_key) { _core->clearDMUnread(pub_key); }
|
||||
void UITask::clearAllDMUnread() { _core->clearAllDMUnread(); }
|
||||
|
||||
void UITask::showAlert(const char* text, int duration_millis) {
|
||||
snprintf(_alert, sizeof(_alert), "%s", text);
|
||||
@@ -2108,8 +2071,7 @@ void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
if (msgcount == 0) {
|
||||
_room_unread = 0;
|
||||
memset(_dm_unread_table, 0, sizeof(_dm_unread_table));
|
||||
((MessagesScreen*)messages_screen)->clearAllChannelUnread();
|
||||
_core->clearAllUnread();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2119,19 +2081,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
if (contact_type == ADV_TYPE_CHAT && pub_key != nullptr) {
|
||||
memcpy(_last_notif_dm_prefix, pub_key, 4);
|
||||
_last_notif_dm_valid = true;
|
||||
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;
|
||||
_dm_unread_table[empty_slot].overflow = false; // fresh contact -- don't inherit a stale flag from whoever held this slot before
|
||||
}
|
||||
// Eviction/overflow is checked in addDMMsg(), after the ring insert.
|
||||
_core->noteIncomingDM(pub_key); // eviction/overflow is checked in addDMMsg(), after the ring insert
|
||||
}
|
||||
|
||||
char alert_buf[80];
|
||||
@@ -2646,7 +2596,7 @@ void UITask::loop() {
|
||||
// Background delivery: resend pending on-device DMs whose ACK timed out, and
|
||||
// finalise the ✗ marker — runs regardless of which screen is active.
|
||||
((MessagesScreen*)messages_screen)->tickDmResends();
|
||||
reconcileDMUnread();
|
||||
_core->reconcileDMUnread();
|
||||
#if UI_HAS_JOYSTICK
|
||||
uint8_t joy_rot = _node_prefs ? _node_prefs->joystick_rotation : JOYSTICK_ROTATION;
|
||||
int ev = user_btn.check();
|
||||
@@ -3273,7 +3223,7 @@ void UITask::clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6) {
|
||||
// cleared here, so a removed contact can't leave a dangling reference. If you
|
||||
// add such a field, add its cleanup below (and mark the field in NodePrefs.h).
|
||||
// Currently covered: favourite_contacts, locator_key, loc_share_dm_prefix,
|
||||
// dm_notif[], dm_melody[]. Also clears _dm_unread_table (RAM-only, not a
|
||||
// dm_notif[], dm_melody[]. Also clears the DM unread table (RAM-only, not a
|
||||
// NodePrefs field, so no savePrefs() needed for it) -- same 4-byte-prefix
|
||||
// shape and same 16-slot starvation risk as dm_notif/dm_melody above. Called
|
||||
// for both explicit removal and silent auto-eviction (see MyMesh
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
#include "../LiveTrack.h"
|
||||
#include "KeyboardWidget.h"
|
||||
|
||||
class UiCore;
|
||||
|
||||
class UITask : public AbstractUITask {
|
||||
DisplayDriver* _display;
|
||||
SensorManager* _sensors;
|
||||
@@ -71,12 +73,9 @@ class UITask : public AbstractUITask {
|
||||
int _last_notif_ch_idx;
|
||||
uint8_t _last_notif_dm_prefix[4];
|
||||
bool _last_notif_dm_valid;
|
||||
// overflow: an unread entry for this contact was evicted off the DM ring
|
||||
// before ever being seen (see newMsg()) -- count is honest but understates
|
||||
// the real total. Cleared with the slot (memset) or on a read back to 0.
|
||||
struct DMUnreadEntry { uint8_t prefix[4]; uint8_t count; bool overflow; };
|
||||
static const int DM_UNREAD_TABLE_SIZE = 16;
|
||||
DMUnreadEntry _dm_unread_table[DM_UNREAD_TABLE_SIZE];
|
||||
// Shared UI Core (../ui-core): message history + unread models. Heap-allocated
|
||||
// in begin(), before any screen, like the screens themselves.
|
||||
UiCore* _core = nullptr;
|
||||
unsigned long ui_started_at, next_batt_chck;
|
||||
uint16_t _batt_mv; // EMA-filtered battery voltage
|
||||
unsigned long next_backlight_btn_check = 0;
|
||||
@@ -307,7 +306,6 @@ public:
|
||||
_last_notif_ch_idx = -1;
|
||||
_last_notif_dm_valid = false;
|
||||
memset(_last_notif_dm_prefix, 0, sizeof(_last_notif_dm_prefix));
|
||||
memset(_dm_unread_table, 0, sizeof(_dm_unread_table));
|
||||
curr = NULL;
|
||||
}
|
||||
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
|
||||
@@ -457,35 +455,17 @@ public:
|
||||
bool getAnyChannelUnreadOverflow() const;
|
||||
int getRoomUnreadCount() const { return _room_unread; }
|
||||
void clearRoomUnread() { _room_unread = 0; }
|
||||
// Clamped to the DM ring's actual occupancy for this contact -- defined in
|
||||
// UITask.cpp (needs MessagesScreen to be a complete type). Same self-healing
|
||||
// shape as MessageHistory::chUnread() for channels.
|
||||
// Clamped to the DM ring's actual occupancy for this contact (UiCore).
|
||||
uint8_t getDMUnread(const uint8_t* pub_key) const;
|
||||
bool getDMUnreadOverflow(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].overflow;
|
||||
return false;
|
||||
}
|
||||
bool getAnyDMUnreadOverflow() const {
|
||||
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++)
|
||||
if (_dm_unread_table[i].count > 0 && _dm_unread_table[i].overflow) return true;
|
||||
return false;
|
||||
}
|
||||
bool getDMUnreadOverflow(const uint8_t* pub_key) const;
|
||||
bool getAnyDMUnreadOverflow() const;
|
||||
// Aggregate for the clock/lock dashboard's single "Msgs" field -- true if
|
||||
// ANY channel or DM contact has permanently lost an unread entry to its
|
||||
// ring cap (rooms have no local ring to overflow the same way).
|
||||
bool getAnyUnreadOverflow() const;
|
||||
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; _dm_unread_table[i].overflow = false; return; }
|
||||
}
|
||||
void clearAllDMUnread() { memset(_dm_unread_table, 0, sizeof(_dm_unread_table)); }
|
||||
// Frees any table slot whose ring occupancy has dropped to zero (evicted or
|
||||
// deduped-away messages) so a genuinely new sender isn't starved once the
|
||||
// fixed 16-slot table fills with stale entries. Called once per loop().
|
||||
void reconcileDMUnread();
|
||||
void clearDMUnread(const uint8_t* pub_key);
|
||||
void clearAllDMUnread();
|
||||
UiCore& core() { return *_core; }
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
DisplayDriver* getDisplay() const { return _display; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user