Fix twatch tilt raise lock screen functionality. Implement unread count on tw_picker. Fix channel missage history and full message view

This commit is contained in:
Pelgraine
2026-07-07 06:03:12 +10:00
parent 32d0055e93
commit b8b3353cf3
6 changed files with 201 additions and 82 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
#define FIRMWARE_VER_CODE 11
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "4 July 2026"
#define FIRMWARE_BUILD_DATE "7 July 2026"
#endif
#ifndef FIRMWARE_VERSION
@@ -182,7 +182,58 @@ public:
// Sanitize emoji: replace UTF-8 emoji sequences with single-byte escape codes
// The text already contains "Sender: message" format
#if defined(LILYGO_TWATCH_S3_PLUS)
// Watch build: drop emoji entirely instead of storing sprite escape bytes.
// The watch UI has no sprite rendering and no room for emoji at 120x120
// virtual. Non-emoji UTF-8 (accented letters etc.) passes through
// unchanged, the same as emojiSanitize does.
{
const uint8_t* s = (const uint8_t*)text;
int srcLen = (int)strlen(text);
int si = 0, di = 0;
while (si < srcLen && di < CHANNEL_MSG_TEXT_LEN - 1) {
uint8_t b = s[si];
if (b < 0x80) { msg->text[di++] = (char)b; si++; continue; }
int consumed;
uint32_t cp = emojiDecodeUtf8(s + si, srcLen - si, &consumed);
if (cp == 0xFE0F) { si += consumed; continue; } // variation selector
if (cp == 0xFFFD) { si += consumed; continue; } // invalid UTF-8 -- skip
bool isEmoji = false;
for (int e = 0; e < EMOJI_COUNT; e++) {
if (EMOJI_CODEPOINTS[e].cp == cp) {
if (EMOJI_CODEPOINTS[e].cp2 != 0) {
// two-codepoint emoji: only a match when the pair matches
int consumed2;
if (si + consumed < srcLen) {
uint32_t cp2 = emojiDecodeUtf8(s + si + consumed, srcLen - si - consumed, &consumed2);
if (cp2 == EMOJI_CODEPOINTS[e].cp2) {
si += consumed + consumed2;
isEmoji = true;
break;
}
}
continue;
}
si += consumed;
isEmoji = true;
break;
}
}
if (!isEmoji) {
for (int a = 0; a < EMOJI_ALIAS_COUNT; a++) {
if (EMOJI_ALIASES[a].cp == cp) { si += consumed; isEmoji = true; break; }
}
}
if (isEmoji) continue; // dropped
if (di + consumed >= CHANNEL_MSG_TEXT_LEN) break;
for (int k = 0; k < consumed; k++) msg->text[di++] = (char)s[si + k];
si += consumed;
}
msg->text[di] = 0;
}
#else
emojiSanitize(text, msg->text, CHANNEL_MSG_TEXT_LEN);
#endif
if (_msgCount < CHANNEL_MSG_HISTORY_SIZE) {
_msgCount++;
@@ -221,6 +272,23 @@ public:
}
int getMessageCount() const { return _msgCount; }
// Read-only access to stored messages for a given channel, by recency.
// n = 0 returns the newest message on that channel, n = 1 the next newest,
// and so on. Returns nullptr when fewer than n + 1 messages exist.
const ChannelMessage* getChannelMsgByRecency(uint8_t channel_idx, int n) const {
if (n < 0 || _msgCount == 0) return nullptr;
int seen = 0;
for (int i = 0; i < _msgCount; i++) {
int idx = _newestIdx - i;
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
const ChannelMessage& m = _messages[idx];
if (!m.valid || m.channel_idx != channel_idx) continue;
if (seen == n) return &m;
seen++;
}
return nullptr;
}
uint8_t getViewChannelIdx() const { return _viewChannelIdx; }
void setViewChannelIdx(uint8_t idx) {
+28 -15
View File
@@ -478,8 +478,15 @@ public:
char timeBuf[6];
sprintf(timeBuf, "%02d:%02d", hrs, mins);
int clockX = (display.width() - lcd->smallTextWidth(timeBuf)) / 2;
display.setColor(DisplayDriver::LIGHT);
lcd->printSmallFont(clockX, HOME_HDR_Y, timeBuf);
// Long node names: keep the clock centred when there is room,
// otherwise push it right so it starts just past the name. When even
// that would run into the unread count, hide the clock instead.
int nameEndX = lcd->smallTextWidth(filtered_name) + 3;
if (clockX < nameEndX) clockX = nameEndX;
if (clockX + lcd->smallTextWidth(timeBuf) + 3 <= numX) {
display.setColor(DisplayDriver::LIGHT);
lcd->printSmallFont(clockX, HOME_HDR_Y, timeBuf);
}
}
}
#else
@@ -1459,7 +1466,9 @@ public:
"Jul","Aug","Sep","Oct","Nov","Dec"};
sprintf(buf, "%s %d %s", days[tmv.tm_wday], tmv.tm_mday, mons[tmv.tm_mon]);
display.setTextSize(2);
display.setColor(DisplayDriver::GREEN); // light grey on the watch palette
display.drawTextCentered(display.width() / 2, display.height() / 2 + 12, buf);
display.setColor(DisplayDriver::LIGHT); // restore for the battery line
}
// battery % and unread count
@@ -1657,6 +1666,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
tw_picker = new TWatchChannelPicker(_display);
tw_channel = new TWatchChannelScreen(_display);
tw_keyboard = new TWatchKeyboardScreen(_display);
((TWatchChannelPicker*)tw_picker)->setStore((ChannelScreen*)channel_screen);
((TWatchChannelScreen*)tw_channel)->setStore((ChannelScreen*)channel_screen);
#endif
audiobook_screen = nullptr; // Created and assigned from main.cpp if audio hardware present
#ifdef MECK_AUDIO_VARIANT
@@ -1774,9 +1785,6 @@ void UITask::msgRead(int msgcount) {
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount,
const uint8_t* path, int8_t snr, uint8_t scope_idx) {
_msgcount = msgcount;
#ifdef TWATCH_COMPOSE_ENABLED
if (tw_channel) ((TWatchChannelScreen*)tw_channel)->notifyMsg(from_name, text);
#endif
// --- Dedup: suppress retry spam (same sender + text within 60s) ---
uint32_t nameH = simpleHash(from_name);
@@ -1930,6 +1938,10 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
// app is connected (they'll see it there), mark as read immediately
if ((isOnChannelScreen() &&
((ChannelScreen *) channel_screen)->getViewChannelIdx() == channel_idx) ||
#ifdef TWATCH_COMPOSE_ENABLED
(curr == tw_channel &&
((TWatchChannelScreen*)tw_channel)->getChannelIdx() == channel_idx) ||
#endif
hasConnection()) {
((ChannelScreen *) channel_screen)->markChannelRead(channel_idx);
}
@@ -2309,6 +2321,7 @@ if (curr) curr->poll();
if (curr == tw_picker) {
TWatchChannelPicker* picker = (TWatchChannelPicker*)tw_picker;
if (picker->isConfirmed()) {
((TWatchChannelScreen*)tw_channel)->setSelfName(the_mesh.getNodePrefs()->node_name);
((TWatchChannelScreen*)tw_channel)->activate(picker->getSelectedChannelIdx(),
picker->getSelectedChannelName());
setCurrScreen(tw_channel);
@@ -2347,7 +2360,7 @@ if (curr) curr->poll();
the_mesh.sendGroupMessage(ts, ch.channel, the_mesh.getNodeName(),
sendText, strlen(sendText));
showAlert("Sent!", 800);
((TWatchChannelScreen*)tw_channel)->addSentMsg(sendText);
addSentChannelMessage(kb->getChannelIdx(), the_mesh.getNodePrefs()->node_name, sendText);
}
kb->clearOutBuf();
setCurrScreen(tw_channel);
@@ -2541,17 +2554,17 @@ if (curr) curr->poll();
if (millis() > _auto_off) {
_display->turnOff();
}
#endif
#if defined(LILYGO_TWATCH_S3_PLUS)
// Raise-to-wake: a wrist-raise (BMA423 tilt) while the display is off turns
// it back on showing the clock screen and restarts the idle timer.
if (!_display->isOn() && board.tiltFired()) {
_display->turnOn();
setCurrScreen(lock_screen);
_auto_off = millis() + AUTO_OFF_MILLIS;
}
#endif
}
#if defined(LILYGO_TWATCH_S3_PLUS)
// Raise-to-wake: a wrist-raise (BMA423 tilt) while the display is off turns
// it back on showing the clock screen and restarts the idle timer.
if (_display != NULL && !_display->isOn() && board.tiltFired()) {
_display->turnOn();
setCurrScreen(lock_screen);
_auto_off = millis() + AUTO_OFF_MILLIS;
}
#endif
// Auto-lock idle timer — runs regardless of display on/off state
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)