v1.5.4: Hop-based eviction, first-boot data cleanup, message performance

- Evict highest-hop unsaved nodes first when peer list is full (nearby peers survive)
- Add first-boot SD data cleanup screen before name input
- Conversation summaries cache for faster message list rendering
- Optimize message view refresh with summary-based change detection
- Streamline unread tracking through MessageStore instead of LXMFManager
This commit is contained in:
DeFiDude
2026-03-08 17:53:43 -06:00
parent 76b03a7508
commit 1e22924159
15 changed files with 418 additions and 73 deletions
+2 -2
View File
@@ -6,8 +6,8 @@
#define RATDECK_VERSION_MAJOR 1
#define RATDECK_VERSION_MINOR 5
#define RATDECK_VERSION_PATCH 3
#define RATDECK_VERSION_STRING "1.5.3"
#define RATDECK_VERSION_PATCH 4
#define RATDECK_VERSION_STRING "1.5.4"
// --- Feature Flags ---
#define HAS_DISPLAY true
+27 -3
View File
@@ -31,6 +31,7 @@
#include "ui/screens/LvHelpOverlay.h"
// Map screen removed
#include "ui/screens/LvNameInputScreen.h"
#include "ui/screens/LvDataCleanScreen.h"
#include "storage/FlashStore.h"
#include "storage/SDStore.h"
#include "storage/MessageStore.h"
@@ -100,6 +101,7 @@ LvSettingsScreen lvSettingsScreen;
LvHelpOverlay lvHelpOverlay;
// LvMapScreen removed
LvNameInputScreen lvNameInputScreen;
LvDataCleanScreen lvDataCleanScreen;
// Tab-screen mapping (4 tabs) — LVGL versions
LvScreen* lvTabScreens[LvTabBar::TAB_COUNT] = {};
@@ -749,6 +751,23 @@ void setup() {
if (lvTabScreens[tab]) ui.setLvScreen(lvTabScreens[tab]);
});
// Data clean screen (first boot only — when SD has old data)
lvDataCleanScreen.setDoneCallback([](bool wipe) {
if (wipe) {
Serial.println("[BOOT] User chose to wipe old data");
lvDataCleanScreen.showStatus("Clearing old data...");
sdStore.wipeRatputer();
if (announceManager) announceManager->clearAll();
Serial.println("[BOOT] Old data cleared");
lvDataCleanScreen.showStatus("Done! Rebooting...");
delay(1500);
ESP.restart();
} else {
Serial.println("[BOOT] User chose to keep old data");
ui.setLvScreen(&lvNameInputScreen);
}
});
// Name input screen (first boot only — when no display name is set)
lvNameInputScreen.setDoneCallback([](const String& name) {
String finalName = name;
@@ -780,9 +799,14 @@ void setup() {
});
if (userConfig.settings().displayName.isEmpty()) {
// Show name input screen (boot mode keeps status/tab bars hidden)
ui.setLvScreen(&lvNameInputScreen);
Serial.println("[BOOT] Showing name input screen");
// First boot — check if SD has old data that should be cleaned
if (sdStore.isReady() && sdStore.hasExistingData()) {
ui.setLvScreen(&lvDataCleanScreen);
Serial.println("[BOOT] Old SD data found, showing data clean screen");
} else {
ui.setLvScreen(&lvNameInputScreen);
Serial.println("[BOOT] Showing name input screen");
}
} else {
// Name already set — go straight to home
ui.setBootMode(false);
+16 -4
View File
@@ -155,15 +155,19 @@ void AnnounceManager::received_announce(
if ((int)_nodes.size() >= MAX_NODES) {
evictStale();
if ((int)_nodes.size() >= MAX_NODES) {
uint8_t maxHops = 0;
unsigned long oldest = ULONG_MAX;
int oldestIdx = -1;
int evictIdx = -1;
for (int i = 0; i < (int)_nodes.size(); i++) {
if (!_nodes[i].saved && _nodes[i].lastSeen < oldest) {
if (_nodes[i].saved) continue;
if (_nodes[i].hops > maxHops ||
(_nodes[i].hops == maxHops && _nodes[i].lastSeen < oldest)) {
maxHops = _nodes[i].hops;
oldest = _nodes[i].lastSeen;
oldestIdx = i;
evictIdx = i;
}
}
if (oldestIdx >= 0) _nodes.erase(_nodes.begin() + oldestIdx);
if (evictIdx >= 0) _nodes.erase(_nodes.begin() + evictIdx);
}
}
if ((int)_nodes.size() >= MAX_NODES) return;
@@ -251,6 +255,14 @@ void AnnounceManager::clearTransientNodes() {
}
}
void AnnounceManager::clearAll() {
_nodes.clear();
_nameCache.clear();
_contactsDirty = false;
_nameCacheDirty = false;
Serial.println("[ANNOUNCE] Cleared all nodes and name cache");
}
void AnnounceManager::saveContact(const DiscoveredNode& node) {
std::string hexHash = node.hash.toHex();
JsonDocument doc;
+1
View File
@@ -50,6 +50,7 @@ public:
void addManualContact(const std::string& hexHash, const std::string& name);
void evictStale(unsigned long maxAgeMs = 3600000);
void clearTransientNodes();
void clearAll();
private:
void saveContact(const DiscoveredNode& node);
+7 -20
View File
@@ -146,8 +146,6 @@ void LXMFManager::processIncoming(const uint8_t* data, size_t len, const RNS::By
Serial.printf("[LXMF] Message from %s (%d bytes) content_len=%d\n",
msg.sourceHash.toHex().substr(0, 8).c_str(), (int)len, (int)msg.content.size());
if (_store) { _store->saveMessage(msg); }
std::string peerHex = msg.sourceHash.toHex();
_unread[peerHex]++;
if (_onMessage) { _onMessage(msg); }
}
@@ -163,28 +161,17 @@ std::vector<LXMFMessage> LXMFManager::getMessages(const std::string& peerHex) co
}
int LXMFManager::unreadCount(const std::string& peerHex) const {
if (!_unreadComputed) { const_cast<LXMFManager*>(this)->computeUnreadFromDisk(); }
if (peerHex.empty()) {
int total = 0;
for (auto& kv : _unread) total += kv.second;
return total;
}
auto it = _unread.find(peerHex);
return (it != _unread.end()) ? it->second : 0;
if (!_store) return 0;
if (peerHex.empty()) return _store->totalUnreadCount();
const ConversationSummary* s = _store->getSummary(peerHex);
return s ? s->unreadCount : 0;
}
void LXMFManager::computeUnreadFromDisk() {
_unreadComputed = true;
if (!_store) return;
for (auto& conv : _store->conversations()) {
auto msgs = _store->loadConversation(conv);
int count = 0;
for (auto& m : msgs) { if (m.incoming && !m.read) count++; }
if (count > 0) _unread[conv] = count;
}
const ConversationSummary* LXMFManager::getConversationSummary(const std::string& peerHex) const {
if (!_store) return nullptr;
return _store->getSummary(peerHex);
}
void LXMFManager::markRead(const std::string& peerHex) {
_unread[peerHex] = 0;
if (_store) { _store->markConversationRead(peerHex); }
}
+1 -4
View File
@@ -27,6 +27,7 @@ public:
std::vector<LXMFMessage> getMessages(const std::string& peerHex) const;
int unreadCount(const std::string& peerHex = "") const;
void markRead(const std::string& peerHex);
const ConversationSummary* getConversationSummary(const std::string& peerHex) const;
private:
bool sendDirect(LXMFMessage& msg);
@@ -40,10 +41,6 @@ private:
StatusCallback _statusCb;
std::deque<LXMFMessage> _outQueue;
void computeUnreadFromDisk();
mutable bool _unreadComputed = false;
mutable std::map<std::string, int> _unread;
// Deduplication: recently seen message IDs
std::set<std::string> _seenMessageIds;
static constexpr int MAX_SEEN_IDS = 100;
+159 -15
View File
@@ -24,6 +24,7 @@ bool MessageStore::begin(FlashStore* flash, SDStore* sd) {
migrateTruncatedDirs();
initReceiveCounter();
refreshConversations();
buildSummaries();
Serial.printf("[MSGSTORE] %d conversations found, receive counter=%lu\n",
(int)_conversations.size(), (unsigned long)_nextReceiveCounter);
return true;
@@ -305,6 +306,19 @@ bool MessageStore::saveMessage(const LXMFMessage& msg) {
if (sdOk) enforceSDLimit(peerHex);
if (flashOk) enforceFlashLimit(peerHex);
// Update summary cache
{
auto& s = _summaries[peerHex];
s.lastTimestamp = msg.timestamp;
s.lastIncoming = msg.incoming;
std::string prefix = msg.incoming ? "Them: " : "You: ";
std::string content = msg.content;
if (content.size() > 15) content = content.substr(0, 15) + "...";
s.lastPreview = prefix + content;
s.totalCount++;
if (msg.incoming && !msg.read) s.unreadCount++;
}
return sdOk || flashOk;
}
@@ -433,35 +447,54 @@ bool MessageStore::deleteConversation(const std::string& peerHex) {
_conversations.erase(
std::remove(_conversations.begin(), _conversations.end(), peerHex),
_conversations.end());
_summaries.erase(peerHex);
return true;
}
void MessageStore::markConversationRead(const std::string& peerHex) {
auto markInDir = [&](auto openFn, auto writeFn, const String& dir) {
// Collect only incoming (_i.json) filenames
std::vector<String> incomingFiles;
File d = openFn(dir.c_str());
if (!d || !d.isDirectory()) return;
File entry = d.openNextFile();
while (entry) {
if (!entry.isDirectory() && isJsonFile(entry.name())) {
size_t size = entry.size();
if (size > 0 && size < 4096) {
String json = entry.readString();
JsonDocument doc;
if (!deserializeJson(doc, json)) {
bool incoming = doc["incoming"] | false;
bool isRead = doc["read"] | false;
if (incoming && !isRead) {
doc["read"] = true;
String updated;
serializeJson(doc, updated);
String path = dir + "/" + entry.name();
writeFn(path.c_str(), updated);
}
}
String name = entry.name();
int len = name.length();
// Check for _i.json suffix (incoming)
if (len >= 7 && name[len - 6] == 'i') {
incomingFiles.push_back(name);
}
}
entry = d.openNextFile();
}
// Sort descending (newest first) to stop early at first already-read
std::sort(incomingFiles.begin(), incomingFiles.end(),
[](const String& a, const String& b) { return a > b; });
for (const auto& fname : incomingFiles) {
String path = dir + "/" + fname;
// Read file via the appropriate storage
String json;
File f = openFn(path.c_str());
if (f && !f.isDirectory()) {
size_t size = f.size();
if (size > 0 && size < 4096) json = f.readString();
f.close();
}
if (json.length() == 0) continue;
JsonDocument doc;
if (deserializeJson(doc, json)) continue;
bool isRead = doc["read"] | false;
if (isRead) break; // all older must be read too
doc["read"] = true;
String updated;
serializeJson(doc, updated);
writeFn(path.c_str(), updated);
}
};
if (_sd && _sd->isReady()) {
@@ -477,6 +510,117 @@ void MessageStore::markConversationRead(const std::string& peerHex) {
[&](const char* p, const String& d) { _flash->writeString(p, d); return true; },
dir);
}
_summaries[peerHex].unreadCount = 0;
}
void MessageStore::buildSummaries() {
_summaries.clear();
for (const auto& peerHex : _conversations) {
ConversationSummary summary;
// Collect filenames from the conversation directory
std::vector<String> files;
auto collectFiles = [&](File& d) {
File entry = d.openNextFile();
while (entry) {
if (!entry.isDirectory() && isJsonFile(entry.name())) {
files.push_back(entry.name());
}
entry = d.openNextFile();
}
};
bool loadedFromSD = false;
if (_sd && _sd->isReady()) {
String sdDir = sdConversationDir(peerHex);
File d = _sd->openDir(sdDir.c_str());
if (d && d.isDirectory()) {
collectFiles(d);
loadedFromSD = true;
}
}
if (!loadedFromSD && _flash) {
String dir = conversationDir(peerHex);
File d = LittleFS.open(dir);
if (d && d.isDirectory()) {
collectFiles(d);
}
}
summary.totalCount = (int)files.size();
if (files.empty()) {
_summaries[peerHex] = summary;
continue;
}
// Sort filenames — highest counter = most recent (last element)
std::sort(files.begin(), files.end());
// Read the last file for preview/timestamp
String lastFilename = files.back();
String basePath = loadedFromSD ? sdConversationDir(peerHex) : conversationDir(peerHex);
String lastPath = basePath + "/" + lastFilename;
auto readJsonFile = [&](const String& path) -> String {
if (loadedFromSD && _sd && _sd->isReady()) {
return _sd->readString(path.c_str());
} else if (_flash) {
return _flash->readString(path.c_str());
}
return String("");
};
String json = readJsonFile(lastPath);
if (json.length() > 0) {
JsonDocument doc;
if (!deserializeJson(doc, json)) {
summary.lastTimestamp = doc["ts"] | 0.0;
std::string content = doc["content"] | "";
summary.lastIncoming = doc["incoming"] | false;
std::string prefix = summary.lastIncoming ? "Them: " : "You: ";
if (content.size() > 15) content = content.substr(0, 15) + "...";
summary.lastPreview = prefix + content;
}
}
// Count unreads: scan _i.json files (incoming), newest first, stop at first already-read
int unread = 0;
for (int i = (int)files.size() - 1; i >= 0; i--) {
const String& fname = files[i];
// Only check incoming files (suffix _i.json)
int flen = fname.length();
if (flen < 7 || fname[flen - 6] != 'i') continue; // not _i.json
String fpath = basePath + "/" + fname;
String fjson = readJsonFile(fpath);
if (fjson.length() > 0) {
JsonDocument fdoc;
if (!deserializeJson(fdoc, fjson)) {
bool isRead = fdoc["read"] | false;
if (isRead) break; // all older messages must be read too
unread++;
}
}
}
summary.unreadCount = unread;
_summaries[peerHex] = summary;
}
Serial.printf("[MSGSTORE] Built summaries for %d conversations\n", (int)_summaries.size());
}
const ConversationSummary* MessageStore::getSummary(const std::string& peerHex) const {
auto it = _summaries.find(peerHex);
return (it != _summaries.end()) ? &it->second : nullptr;
}
int MessageStore::totalUnreadCount() const {
int total = 0;
for (const auto& kv : _summaries) total += kv.second.unreadCount;
return total;
}
String MessageStore::conversationDir(const std::string& peerHex) const {
+13
View File
@@ -8,6 +8,14 @@
#include <string>
#include <map>
struct ConversationSummary {
double lastTimestamp = 0;
std::string lastPreview; // first ~20 chars of last message
bool lastIncoming = false;
int unreadCount = 0;
int totalCount = 0;
};
class MessageStore {
public:
bool begin(FlashStore* flash, SDStore* sd = nullptr);
@@ -20,6 +28,9 @@ public:
bool deleteConversation(const std::string& peerHex);
void markConversationRead(const std::string& peerHex);
const ConversationSummary* getSummary(const std::string& peerHex) const;
int totalUnreadCount() const;
private:
String conversationDir(const std::string& peerHex) const;
String sdConversationDir(const std::string& peerHex) const;
@@ -28,9 +39,11 @@ private:
void migrateFlashToSD();
void migrateTruncatedDirs();
void initReceiveCounter();
void buildSummaries();
FlashStore* _flash = nullptr;
SDStore* _sd = nullptr;
std::vector<std::string> _conversations;
std::map<std::string, ConversationSummary> _summaries;
uint32_t _nextReceiveCounter = 0;
};
+15
View File
@@ -188,6 +188,21 @@ void SDStore::wipeDir(const char* path) {
dir.close();
}
bool SDStore::hasExistingData() {
if (!_ready) return false;
if (SD.exists("/ratputer/config.json")) return true;
if (SD.exists("/ratputer/identity/identity")) return true;
File dir = SD.open("/ratputer/messages");
if (dir && dir.isDirectory()) {
File entry = dir.openNextFile();
bool found = (bool)entry;
if (entry) entry.close();
dir.close();
if (found) return true;
}
return false;
}
bool SDStore::formatForRatputer() {
if (!_ready) return false;
Serial.println("[SD] Creating Ratputer directory structure...");
+1
View File
@@ -27,6 +27,7 @@ public:
bool formatForRatputer();
bool wipeRatputer();
bool hasExistingData();
private:
void wipeDir(const char* path);
+115
View File
@@ -0,0 +1,115 @@
#include "LvDataCleanScreen.h"
#include "ui/Theme.h"
#include "ui/LvTheme.h"
#include "config/Config.h"
void LvDataCleanScreen::createUI(lv_obj_t* parent) {
_screen = parent;
lv_obj_clear_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_color(parent, lv_color_hex(Theme::BG), 0);
// Title: "RATSPEAK"
lv_obj_t* title = lv_label_create(parent);
lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(title, lv_color_hex(Theme::PRIMARY), 0);
lv_label_set_text(title, "RATSPEAK");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
// Subtitle
lv_obj_t* sub = lv_label_create(parent);
lv_obj_set_style_text_font(sub, &lv_font_montserrat_12, 0);
lv_obj_set_style_text_color(sub, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(sub, "ratspeak.org");
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 42);
// Message
lv_obj_t* msg = lv_label_create(parent);
lv_obj_set_style_text_font(msg, &lv_font_montserrat_12, 0);
lv_obj_set_style_text_color(msg, lv_color_hex(Theme::SECONDARY), 0);
lv_label_set_text(msg, "Old data found on SD card.");
lv_obj_align(msg, LV_ALIGN_TOP_MID, 0, 75);
// Prompt
lv_obj_t* prompt = lv_label_create(parent);
lv_obj_set_style_text_font(prompt, &lv_font_montserrat_12, 0);
lv_obj_set_style_text_color(prompt, lv_color_hex(Theme::SECONDARY), 0);
lv_label_set_text(prompt, "Remove old data and start fresh?");
lv_obj_align(prompt, LV_ALIGN_TOP_MID, 0, 95);
// Yes label
_yesLabel = lv_label_create(parent);
lv_obj_set_style_text_font(_yesLabel, &lv_font_montserrat_14, 0);
lv_label_set_text(_yesLabel, "[ Yes ]");
lv_obj_align(_yesLabel, LV_ALIGN_TOP_MID, -50, 135);
// No label
_noLabel = lv_label_create(parent);
lv_obj_set_style_text_font(_noLabel, &lv_font_montserrat_14, 0);
lv_label_set_text(_noLabel, "[ No ]");
lv_obj_align(_noLabel, LV_ALIGN_TOP_MID, 50, 135);
updateSelection();
// Hint
_hintLabel = lv_label_create(parent);
lv_obj_set_style_text_font(_hintLabel, &lv_font_montserrat_12, 0);
lv_obj_set_style_text_color(_hintLabel, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(_hintLabel, "[</>] Select [Enter] OK");
lv_obj_align(_hintLabel, LV_ALIGN_TOP_MID, 0, 165);
// Status label (hidden until showStatus is called)
_statusLabel = lv_label_create(parent);
lv_obj_set_style_text_font(_statusLabel, &lv_font_montserrat_12, 0);
lv_obj_set_style_text_color(_statusLabel, lv_color_hex(Theme::PRIMARY), 0);
lv_label_set_text(_statusLabel, "");
lv_obj_align(_statusLabel, LV_ALIGN_TOP_MID, 0, 140);
lv_obj_add_flag(_statusLabel, LV_OBJ_FLAG_HIDDEN);
// Version
lv_obj_t* ver = lv_label_create(parent);
lv_obj_set_style_text_font(ver, &lv_font_montserrat_10, 0);
lv_obj_set_style_text_color(ver, lv_color_hex(Theme::MUTED), 0);
char verBuf[32];
snprintf(verBuf, sizeof(verBuf), "v%s", RATDECK_VERSION_STRING);
lv_label_set_text(ver, verBuf);
lv_obj_align(ver, LV_ALIGN_BOTTOM_MID, 0, -10);
}
void LvDataCleanScreen::updateSelection() {
if (_selectedYes) {
lv_obj_set_style_text_color(_yesLabel, lv_color_hex(Theme::ACCENT), 0);
lv_obj_set_style_text_color(_noLabel, lv_color_hex(Theme::MUTED), 0);
} else {
lv_obj_set_style_text_color(_yesLabel, lv_color_hex(Theme::MUTED), 0);
lv_obj_set_style_text_color(_noLabel, lv_color_hex(Theme::ACCENT), 0);
}
}
void LvDataCleanScreen::showStatus(const char* msg) {
if (_yesLabel) lv_obj_add_flag(_yesLabel, LV_OBJ_FLAG_HIDDEN);
if (_noLabel) lv_obj_add_flag(_noLabel, LV_OBJ_FLAG_HIDDEN);
if (_hintLabel) lv_obj_add_flag(_hintLabel, LV_OBJ_FLAG_HIDDEN);
if (_statusLabel) {
lv_label_set_text(_statusLabel, msg);
lv_obj_clear_flag(_statusLabel, LV_OBJ_FLAG_HIDDEN);
}
lv_timer_handler();
}
bool LvDataCleanScreen::handleKey(const KeyEvent& event) {
if (event.left) {
_selectedYes = true;
updateSelection();
return true;
}
if (event.right) {
_selectedYes = false;
updateSelection();
return true;
}
if (event.enter || event.character == '\n' || event.character == '\r') {
if (_doneCb) _doneCb(_selectedYes);
return true;
}
return true; // Consume all keys
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "ui/UIManager.h"
#include <functional>
class LvDataCleanScreen : public LvScreen {
public:
void createUI(lv_obj_t* parent) override;
bool handleKey(const KeyEvent& event) override;
const char* title() const override { return "Setup"; }
// Callback: true = user chose Yes (wipe), false = user chose No (skip)
void setDoneCallback(std::function<void(bool)> cb) { _doneCb = cb; }
// Replace buttons with a status message (for showing progress)
void showStatus(const char* msg);
private:
lv_obj_t* _yesLabel = nullptr;
lv_obj_t* _noLabel = nullptr;
lv_obj_t* _hintLabel = nullptr;
lv_obj_t* _statusLabel = nullptr;
bool _selectedYes = true;
std::function<void(bool)> _doneCb;
void updateSelection();
};
+29 -16
View File
@@ -114,11 +114,12 @@ void LvMessageView::onEnter() {
// Register status callback for live updates
std::string peer = _peerHex;
_lxmf->setStatusCallback([this, peer](const std::string& peerHex, double, LXMFStatus) {
if (peerHex == peer) _lastRefreshMs = 0; // Force rebuild on next refreshUI
if (peerHex == peer) _statusDirty = true;
});
}
_lastMsgCount = -1;
_lastRefreshMs = 0;
_statusDirty = false;
_inputText.clear();
if (_lblHeader) {
@@ -129,6 +130,7 @@ void LvMessageView::onEnter() {
if (_textarea) {
lv_textarea_set_text(_textarea, "");
}
_cachedMsgs.clear(); // Force fresh load
rebuildMessages();
}
@@ -141,28 +143,38 @@ void LvMessageView::onExit() {
void LvMessageView::refreshUI() {
if (!_lxmf) return;
unsigned long now = millis();
if (now - _lastRefreshMs >= REFRESH_INTERVAL_MS) {
auto newMsgs = _lxmf->getMessages(_peerHex);
_lastRefreshMs = now;
// Rebuild if count changed or any status changed
bool changed = (newMsgs.size() != _cachedMsgs.size());
if (!changed) {
for (size_t i = 0; i < newMsgs.size(); i++) {
if (newMsgs[i].status != _cachedMsgs[i].status) { changed = true; break; }
}
}
if (changed) {
_cachedMsgs = newMsgs;
_lastMsgCount = (int)_cachedMsgs.size();
rebuildMessages();
if (now - _lastRefreshMs < REFRESH_INTERVAL_MS) return;
_lastRefreshMs = now;
// Quick check: if summary count unchanged and no status callback fired, skip disk load
auto* summary = _lxmf->getConversationSummary(_peerHex);
if (summary && !_statusDirty && summary->totalCount == (int)_cachedMsgs.size()) return;
_statusDirty = false;
auto newMsgs = _lxmf->getMessages(_peerHex);
// Rebuild if count changed or any status changed
bool changed = (newMsgs.size() != _cachedMsgs.size());
if (!changed) {
for (size_t i = 0; i < newMsgs.size(); i++) {
if (newMsgs[i].status != _cachedMsgs[i].status) { changed = true; break; }
}
}
if (changed) {
_cachedMsgs = std::move(newMsgs);
_lastMsgCount = (int)_cachedMsgs.size();
rebuildMessages();
}
}
void LvMessageView::rebuildMessages() {
if (!_lxmf || !_msgScroll) return;
_cachedMsgs = _lxmf->getMessages(_peerHex);
// Only load from disk if _cachedMsgs is empty (first call or after send)
if (_cachedMsgs.empty()) {
_cachedMsgs = _lxmf->getMessages(_peerHex);
}
_lastMsgCount = (int)_cachedMsgs.size();
_lastRefreshMs = millis();
lv_obj_clean(_msgScroll);
@@ -269,6 +281,7 @@ void LvMessageView::sendCurrentMessage() {
_inputText.clear();
if (_textarea) lv_textarea_set_text(_textarea, "");
_cachedMsgs.clear(); // Force fresh load in rebuildMessages
rebuildMessages();
}
+1
View File
@@ -42,6 +42,7 @@ private:
int _lastMsgCount = -1;
unsigned long _lastRefreshMs = 0;
std::vector<LXMFMessage> _cachedMsgs;
bool _statusDirty = false;
// LVGL widgets
lv_obj_t* _header = nullptr;
+5 -9
View File
@@ -96,15 +96,11 @@ void LvMessagesScreen::rebuildList() {
for (int i = 0; i < count; i++) {
ConvInfo ci;
ci.peerHex = convs[i];
ci.hasUnread = _lxmf->unreadCount(ci.peerHex) > 0;
auto msgs = _lxmf->getMessages(ci.peerHex);
if (!msgs.empty()) {
const auto& last = msgs.back();
ci.lastTs = last.timestamp;
std::string prefix = last.incoming ? "Them: " : "You: ";
std::string content = last.content;
if (content.size() > 15) content = content.substr(0, 15) + "...";
ci.preview = prefix + content;
auto* s = _lxmf->getConversationSummary(ci.peerHex);
if (s) {
ci.lastTs = s->lastTimestamp;
ci.preview = s->lastPreview;
ci.hasUnread = s->unreadCount > 0;
}
sorted.push_back(ci);
}