diff --git a/examples/companion_radio/ui-new/SMSScreen.h b/examples/companion_radio/ui-new/SMSScreen.h index 6c0440f9..38646400 100644 --- a/examples/companion_radio/ui-new/SMSScreen.h +++ b/examples/companion_radio/ui-new/SMSScreen.h @@ -240,9 +240,10 @@ public: smsStore.saveMessage(phone, body, false, timestamp); } if (_view == CONVERSATION && strcmp(_activePhone, phone) == 0) { + smsStore.markConversationRead(_activePhone); refreshConversation(); } - if (_view == INBOX) { + if (_view == INBOX || _view == APP_MENU) { refreshInbox(); } _needsRefresh = true; @@ -348,10 +349,12 @@ public: else display.print(" "); display.print("SMS Inbox"); - // Show conversation count hint - if (_convCount > 0) { + // Show unread count hint (hidden when there are none) + int unread = 0; + for (int i = 0; i < _convCount; i++) unread += _conversations[i].unreadCount; + if (unread > 0) { char countHint[12]; - snprintf(countHint, sizeof(countHint), " [%d]", _convCount); + snprintf(countHint, sizeof(countHint), " [%d]", unread); display.setColor(DisplayDriver::LIGHT); display.print(countHint); } @@ -1317,6 +1320,8 @@ public: case '\r': // Enter - open conversation if (_convCount > 0 && _inboxCursor < _convCount) { strncpy(_activePhone, _conversations[_inboxCursor].phone, SMS_PHONE_LEN - 1); + smsStore.markConversationRead(_activePhone); + _conversations[_inboxCursor].unreadCount = 0; refreshConversation(); _view = CONVERSATION; } diff --git a/examples/companion_radio/ui-new/SMSStore.cpp b/examples/companion_radio/ui-new/SMSStore.cpp index 8d8cd854..0275767c 100644 --- a/examples/companion_radio/ui-new/SMSStore.cpp +++ b/examples/companion_radio/ui-new/SMSStore.cpp @@ -14,6 +14,17 @@ void SMSStore::begin() { MESH_DEBUG_PRINTLN("[SMSStore] created %s", SMS_DIR); } _ready = true; + + // One-time migration: history saved before read-tracking existed has read=0, + // which would otherwise all show as unread. Mark it read once, gated by a + // marker file so genuine unread state survives later reboots. + if (!SD.exists(SMS_READ_MIGRATED)) { + migrateExistingAsRead(); + File m = SD.open(SMS_READ_MIGRATED, FILE_WRITE); + if (m) m.close(); + digitalWrite(SDCARD_CS, HIGH); + } + MESH_DEBUG_PRINTLN("[SMSStore] ready"); } @@ -43,6 +54,7 @@ bool SMSStore::saveMessage(const char* phone, const char* body, bool isSent, uin memset(&rec, 0, sizeof(rec)); rec.timestamp = timestamp; rec.isSent = isSent ? 1 : 0; + rec.read = isSent ? 1 : 0; // sent messages are never unread; received start unread rec.bodyLen = strlen(body); if (rec.bodyLen >= SMS_BODY_LEN) rec.bodyLen = SMS_BODY_LEN - 1; strncpy(rec.phone, phone, SMS_PHONE_LEN - 1); @@ -86,13 +98,20 @@ int SMSStore::loadConversations(SMSConversation* out, int maxCount) { int numRecords = fileSize / sizeof(SMSRecord); - // Read the last record for preview + // Scan all records: count unread received messages, keep the last for preview + SMSRecord rec; SMSRecord lastRec; - entry.seek(fileSize - sizeof(SMSRecord)); - if (entry.read((uint8_t*)&lastRec, sizeof(SMSRecord)) != sizeof(SMSRecord)) { - entry.close(); - continue; + memset(&lastRec, 0, sizeof(lastRec)); + int unread = 0; + bool haveLast = false; + for (int i = 0; i < numRecords; i++) { + entry.seek((size_t)i * sizeof(SMSRecord)); + if (entry.read((uint8_t*)&rec, sizeof(SMSRecord)) != sizeof(SMSRecord)) continue; + if (rec.isSent == 0 && rec.read == 0) unread++; + lastRec = rec; + haveLast = true; } + if (!haveLast) { entry.close(); continue; } SMSConversation& conv = out[count]; memset(&conv, 0, sizeof(SMSConversation)); @@ -101,7 +120,7 @@ int SMSStore::loadConversations(SMSConversation* out, int maxCount) { conv.preview[39] = '\0'; conv.lastTimestamp = lastRec.timestamp; conv.messageCount = numRecords; - conv.unreadCount = 0; // TODO: track read state + conv.unreadCount = unread; conv.valid = true; count++; @@ -193,4 +212,63 @@ int SMSStore::getMessageCount(const char* phone) { return count; } +void SMSStore::markFileRead(const char* filepath) { + // In-place flag update: open read+write without truncating ("r+"). + // Caller releases SDCARD_CS afterwards. + File f = SD.open(filepath, "r+"); + if (!f) return; + + size_t fileSize = f.size(); + int numRecords = fileSize / sizeof(SMSRecord); + + SMSRecord rec; + for (int i = 0; i < numRecords; i++) { + f.seek((size_t)i * sizeof(SMSRecord)); + if (f.read((uint8_t*)&rec, sizeof(SMSRecord)) != sizeof(SMSRecord)) continue; + if (rec.isSent == 0 && rec.read == 0) { + rec.read = 1; + f.seek((size_t)i * sizeof(SMSRecord)); + f.write((uint8_t*)&rec, sizeof(SMSRecord)); + } + } + + f.close(); +} + +void SMSStore::markConversationRead(const char* phone) { + if (!_ready) return; + + char filepath[64]; + phoneToFilename(phone, filepath, sizeof(filepath)); + markFileRead(filepath); + + digitalWrite(SDCARD_CS, HIGH); +} + +void SMSStore::migrateExistingAsRead() { + File dir = SD.open(SMS_DIR); + if (!dir || !dir.isDirectory()) return; + + File entry; + while ((entry = dir.openNextFile())) { + const char* name = entry.name(); + if (!strstr(name, ".sms")) { entry.close(); continue; } + + // name() may be a bare basename or a full path depending on core version + char fullpath[64]; + if (name[0] == '/') { + strncpy(fullpath, name, sizeof(fullpath) - 1); + fullpath[sizeof(fullpath) - 1] = '\0'; + } else { + snprintf(fullpath, sizeof(fullpath), "%s/%s", SMS_DIR, name); + } + entry.close(); + + markFileRead(fullpath); + } + dir.close(); + + digitalWrite(SDCARD_CS, HIGH); +} + #endif // HAS_4G_MODEM \ No newline at end of file diff --git a/examples/companion_radio/ui-new/SMSStore.h b/examples/companion_radio/ui-new/SMSStore.h index 32359096..4ce9ac81 100644 --- a/examples/companion_radio/ui-new/SMSStore.h +++ b/examples/companion_radio/ui-new/SMSStore.h @@ -22,12 +22,14 @@ #define SMS_BODY_LEN 161 #define SMS_MAX_CONVERSATIONS 20 #define SMS_DIR "/sms" +#define SMS_READ_MIGRATED "/sms/rdmig.dat" // one-time read-state migration marker // Fixed-size on-disk record (256 bytes, easy alignment) struct SMSRecord { uint32_t timestamp; // epoch seconds uint8_t isSent; // 1=sent, 0=received - uint8_t reserved[2]; + uint8_t read; // 1=read, 0=unread (received messages only) + uint8_t reserved[1]; uint8_t bodyLen; // actual length of body char phone[SMS_PHONE_LEN]; // 20 char body[SMS_BODY_LEN]; // 161 @@ -73,11 +75,20 @@ public: // Get total message count for a phone number int getMessageCount(const char* phone); + // Mark all received messages in a conversation as read (persisted to SD) + void markConversationRead(const char* phone); + private: bool _ready = false; // Convert phone number to safe filename void phoneToFilename(const char* phone, char* out, size_t outLen); + + // Set read=1 on every received-unread record in a conversation file + void markFileRead(const char* filepath); + + // One-time: mark all pre-existing history read (it pre-dates read tracking) + void migrateExistingAsRead(); }; // Global singleton