more fixes

This commit is contained in:
HarukiToreda 2025-10-12 22:36:15 -04:00
parent e38925834d
commit 0b11f93880
5 changed files with 282 additions and 196 deletions

View File

@ -8,6 +8,8 @@
#include "gps/RTC.h" #include "gps/RTC.h"
#include "graphics/draw/MessageRenderer.h" #include "graphics/draw/MessageRenderer.h"
#include <algorithm> // std::min, std::find_if
using graphics::MessageRenderer::setThreadMode; using graphics::MessageRenderer::setThreadMode;
using graphics::MessageRenderer::ThreadMode; using graphics::MessageRenderer::ThreadMode;
@ -15,7 +17,8 @@ using graphics::MessageRenderer::ThreadMode;
static inline size_t getMessageSize(const StoredMessage &m) static inline size_t getMessageSize(const StoredMessage &m)
{ {
// serialized size = fixed 16 bytes + text length (capped at MAX_MESSAGE_SIZE) // serialized size = fixed 16 bytes + text length (capped at MAX_MESSAGE_SIZE)
return 16 + std::min<size_t>(MAX_MESSAGE_SIZE, strnlen(m.text, MAX_MESSAGE_SIZE)); // NOTE: Using std::string length here (fast path we had originally)
return 16 + std::min(static_cast<size_t>(MAX_MESSAGE_SIZE), m.text.size());
} }
static inline void assignTimestamp(StoredMessage &sm) static inline void assignTimestamp(StoredMessage &sm)
@ -52,20 +55,34 @@ void MessageStore::addLiveMessage(StoredMessage &&msg)
if (liveMessages.size() >= MAX_MESSAGES_SAVED) { if (liveMessages.size() >= MAX_MESSAGES_SAVED) {
liveMessages.pop_front(); // keep only most recent N liveMessages.pop_front(); // keep only most recent N
} }
// Use emplace_back with std::move to avoid extra copy
liveMessages.emplace_back(std::move(msg)); liveMessages.emplace_back(std::move(msg));
} }
void MessageStore::addLiveMessage(const StoredMessage &msg)
{
if (liveMessages.size() >= MAX_MESSAGES_SAVED) {
liveMessages.pop_front(); // keep only most recent N
}
liveMessages.push_back(msg);
}
// Persistence queue (used only on shutdown/reboot) // Persistence queue (used only on shutdown/reboot)
void MessageStore::addMessage(StoredMessage &&msg) void MessageStore::addMessage(StoredMessage &&msg)
{ {
if (messages.size() >= MAX_MESSAGES_SAVED) { if (messages.size() >= MAX_MESSAGES_SAVED) {
messages.pop_front(); messages.pop_front();
} }
// Use emplace_back with std::move to avoid extra copy
messages.emplace_back(std::move(msg)); messages.emplace_back(std::move(msg));
} }
void MessageStore::addMessage(const StoredMessage &msg)
{
if (messages.size() >= MAX_MESSAGES_SAVED) {
messages.pop_front();
}
messages.push_back(msg);
}
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
{ {
StoredMessage sm; StoredMessage sm;
@ -73,8 +90,9 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
assignTimestamp(sm); assignTimestamp(sm);
sm.channelIndex = packet.channel; sm.channelIndex = packet.channel;
strncpy(sm.text, reinterpret_cast<const char *>(packet.decoded.payload.bytes), MAX_MESSAGE_SIZE - 1);
sm.text[MAX_MESSAGE_SIZE - 1] = '\0'; // Text from packet → std::string (fast, no extra copies)
sm.text = std::string(reinterpret_cast<const char *>(packet.decoded.payload.bytes));
if (packet.from == 0) { if (packet.from == 0) {
// Phone-originated (outgoing) // Phone-originated (outgoing)
@ -107,7 +125,7 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
sm.ackStatus = AckStatus::ACKED; sm.ackStatus = AckStatus::ACKED;
} }
addLiveMessage(std::move(sm)); addLiveMessage(sm);
// Return reference to the most recently stored message // Return reference to the most recently stored message
return liveMessages.back(); return liveMessages.back();
@ -123,8 +141,7 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st
sm.sender = sender; sm.sender = sender;
sm.channelIndex = channelIndex; sm.channelIndex = channelIndex;
strncpy(sm.text, text.c_str(), MAX_MESSAGE_SIZE - 1); sm.text = text;
sm.text[MAX_MESSAGE_SIZE - 1] = '\0';
// Default manual adds to broadcast // Default manual adds to broadcast
sm.dest = NODENUM_BROADCAST; sm.dest = NODENUM_BROADCAST;
@ -133,7 +150,7 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st
// Outgoing messages start as NONE until ACK/NACK arrives // Outgoing messages start as NONE until ACK/NACK arrives
sm.ackStatus = AckStatus::NONE; sm.ackStatus = AckStatus::NONE;
addLiveMessage(std::move(sm)); addLiveMessage(sm);
} }
#if ENABLE_MESSAGE_PERSISTENCE #if ENABLE_MESSAGE_PERSISTENCE
@ -160,7 +177,10 @@ void MessageStore::saveToFlash()
f.write((uint8_t *)&m.sender, sizeof(m.sender)); f.write((uint8_t *)&m.sender, sizeof(m.sender));
f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex));
f.write((uint8_t *)&m.dest, sizeof(m.dest)); f.write((uint8_t *)&m.dest, sizeof(m.dest));
f.write((uint8_t *)m.text, strnlen(m.text, MAX_MESSAGE_SIZE));
const size_t toWrite = std::min(static_cast<size_t>(MAX_MESSAGE_SIZE), m.text.size());
if (toWrite)
f.write((const uint8_t *)m.text.data(), toWrite);
f.write('\0'); // null terminator f.write('\0'); // null terminator
uint8_t bootFlag = m.isBootRelative ? 1 : 0; uint8_t bootFlag = m.isBootRelative ? 1 : 0;
@ -203,8 +223,20 @@ void MessageStore::loadFromFlash()
f.readBytes((char *)&m.sender, sizeof(m.sender)); f.readBytes((char *)&m.sender, sizeof(m.sender));
f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex));
f.readBytes((char *)&m.dest, sizeof(m.dest)); f.readBytes((char *)&m.dest, sizeof(m.dest));
f.readBytes(m.text, MAX_MESSAGE_SIZE - 1);
m.text[MAX_MESSAGE_SIZE - 1] = '\0'; // Fast text read: read until NUL or cap
m.text.clear();
m.text.reserve(64); // small reserve to avoid initial reallocs
char c;
size_t readCount = 0;
while (readCount < MAX_MESSAGE_SIZE) {
if (f.readBytes(&c, 1) <= 0)
break;
if (c == '\0')
break;
m.text.push_back(c);
++readCount;
}
// Try to read boot-relative flag (new format) // Try to read boot-relative flag (new format)
uint8_t bootFlag = 0; uint8_t bootFlag = 0;
@ -295,6 +327,16 @@ void MessageStore::dismissOldestMessage()
saveToFlash(); saveToFlash();
} }
// Dismiss newest message (RAM + persisted queue)
void MessageStore::dismissNewestMessage()
{
eraseIf(
liveMessages, [](StoredMessage &) { return true; }, true);
eraseIf(
messages, [](StoredMessage &) { return true; }, true);
saveToFlash();
}
// Dismiss oldest message in a specific channel // Dismiss oldest message in a specific channel
void MessageStore::dismissOldestMessageInChannel(uint8_t channel) void MessageStore::dismissOldestMessageInChannel(uint8_t channel)
{ {
@ -319,16 +361,6 @@ void MessageStore::dismissOldestMessageWithPeer(uint32_t peer)
saveToFlash(); saveToFlash();
} }
// Dismiss newest message (RAM + persisted queue)
void MessageStore::dismissNewestMessage()
{
eraseIf(
liveMessages, [](StoredMessage &) { return true; }, true);
eraseIf(
messages, [](StoredMessage &) { return true; }, true);
saveToFlash();
}
// Helper filters for future use // Helper filters for future use
std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const
{ {
@ -352,6 +384,19 @@ std::deque<StoredMessage> MessageStore::getDirectMessages() const
return result; return result;
} }
std::deque<StoredMessage> MessageStore::getConversationWith(uint32_t peer) const
{
std::deque<StoredMessage> result;
for (const auto &m : liveMessages) {
if (m.type == MessageType::DM_TO_US) {
uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
if (other == peer)
result.push_back(m);
}
}
return result;
}
// Upgrade boot-relative timestamps once RTC is valid // Upgrade boot-relative timestamps once RTC is valid
// Only same-boot boot-relative messages are healed. // Only same-boot boot-relative messages are healed.
// Persisted boot-relative messages from old boots stay ??? forever. // Persisted boot-relative messages from old boots stay ??? forever.

View File

@ -21,7 +21,11 @@
#ifndef MAX_MESSAGES_SAVED #ifndef MAX_MESSAGES_SAVED
#define MAX_MESSAGES_SAVED 20 #define MAX_MESSAGES_SAVED 20
#endif #endif
constexpr size_t MAX_MESSAGE_SIZE = 220; // safe bound for text payload
// Keep a cap for serialized payloads
#ifndef MAX_MESSAGE_SIZE
#define MAX_MESSAGE_SIZE 220
#endif
// Explicit message classification // Explicit message classification
enum class MessageType : uint8_t { BROADCAST = 0, DM_TO_US = 1 }; enum class MessageType : uint8_t { BROADCAST = 0, DM_TO_US = 1 };
@ -39,7 +43,7 @@ struct StoredMessage {
uint32_t timestamp; // When message was created (secs since boot/RTC) uint32_t timestamp; // When message was created (secs since boot/RTC)
uint32_t sender; // NodeNum of sender uint32_t sender; // NodeNum of sender
uint8_t channelIndex; // Channel index used uint8_t channelIndex; // Channel index used
char text[MAX_MESSAGE_SIZE]; // UTF-8 text payload std::string text; // UTF-8 text payload (dynamic now, no fixed size)
// Destination node. // Destination node.
// 0xffffffff (NODENUM_BROADCAST) means broadcast, // 0xffffffff (NODENUM_BROADCAST) means broadcast,
@ -71,10 +75,12 @@ class MessageStore
// Live RAM methods (always current, used by UI and runtime) // Live RAM methods (always current, used by UI and runtime)
void addLiveMessage(StoredMessage &&msg); void addLiveMessage(StoredMessage &&msg);
void addLiveMessage(const StoredMessage &msg); // convenience overload
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; } const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
// Persistence methods (used only on boot/shutdown) // Persistence methods (used only on boot/shutdown)
void addMessage(StoredMessage &&msg); void addMessage(StoredMessage &&msg);
void addMessage(const StoredMessage &msg); // convenience overload
const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only
void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text);
void saveToFlash(); void saveToFlash();

View File

@ -47,25 +47,56 @@ inline size_t utf8CharLen(uint8_t c)
return 1; return 1;
} }
// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels
std::string normalizeEmoji(const std::string &s)
{
std::string out;
for (size_t i = 0; i < s.size();) {
uint8_t c = static_cast<uint8_t>(s[i]);
size_t len = utf8CharLen(c);
if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) {
i += 3;
continue;
}
// Skip skin tone modifiers
if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F &&
((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) {
i += 4;
continue;
}
out.append(s, i, len);
i += len;
}
return out;
}
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
{ {
std::string renderLine = normalizeEmoji(line);
int cursorX = x; int cursorX = x;
const int fontHeight = FONT_HEIGHT_SMALL; const int fontHeight = FONT_HEIGHT_SMALL;
// Step 1: Find tallest emote in the line // Step 1: Find tallest emote in the line
int maxIconHeight = fontHeight; int maxIconHeight = fontHeight;
for (size_t i = 0; i < line.length();) { for (size_t i = 0; i < line.length();) {
bool matched = false;
for (int e = 0; e < emoteCount; ++e) { for (int e = 0; e < emoteCount; ++e) {
size_t emojiLen = strlen(emotes[e].label); size_t emojiLen = strlen(emotes[e].label);
if (line.compare(i, emojiLen, emotes[e].label) == 0) { if (line.compare(i, emojiLen, emotes[e].label) == 0) {
if (emotes[e].height > maxIconHeight) if (emotes[e].height > maxIconHeight)
maxIconHeight = emotes[e].height; maxIconHeight = emotes[e].height;
i += emojiLen; i += emojiLen;
matched = true;
break; break;
} }
} }
if (!matched) {
i += utf8CharLen(static_cast<uint8_t>(line[i])); i += utf8CharLen(static_cast<uint8_t>(line[i]));
} }
}
// Step 2: Baseline alignment // Step 2: Baseline alignment
int lineHeight = std::max(fontHeight, maxIconHeight); int lineHeight = std::max(fontHeight, maxIconHeight);
@ -122,11 +153,12 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
// Render the emote (if found) // Render the emote (if found)
if (matchedEmote && i == nextEmotePos) { if (matchedEmote && i == nextEmotePos) {
// Center vertically — padding handled in calculateLineHeights // Vertically center emote relative to font baseline (not just midline)
int iconY = fontMidline - matchedEmote->height / 2; int iconY = fontY + (fontHeight - matchedEmote->height) / 2;
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
cursorX += matchedEmote->width + 1; cursorX += matchedEmote->width + 1;
i += emojiLen; i += emojiLen;
continue;
} else { } else {
// No more emotes — render the rest of the line // No more emotes — render the rest of the line
std::string remaining = line.substr(i); std::string remaining = line.substr(i);
@ -139,7 +171,6 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
#else #else
cursorX += display->getStringWidth(remaining.c_str()); cursorX += display->getStringWidth(remaining.c_str());
#endif #endif
break; break;
} }
} }
@ -177,7 +208,6 @@ static std::vector<uint32_t> seenPeers;
// Public helper so menus / store can clear stale registries // Public helper so menus / store can clear stale registries
void clearThreadRegistries() void clearThreadRegistries()
{ {
LOG_DEBUG("[MessageRenderer] Clearing thread registries (seenChannels/seenPeers)");
seenChannels.clear(); seenChannels.clear();
seenPeers.clear(); seenPeers.clear();
} }
@ -185,7 +215,6 @@ void clearThreadRegistries()
// Setter so other code can switch threads // Setter so other code can switch threads
void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */) void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */)
{ {
LOG_DEBUG("[MessageRenderer] setThreadMode(mode=%d, ch=%d, peer=0x%08x)", (int)mode, channel, (unsigned int)peer);
currentMode = mode; currentMode = mode;
currentChannel = channel; currentChannel = channel;
currentPeer = peer; currentPeer = peer;
@ -194,7 +223,6 @@ void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0
// Track channels weve seen // Track channels weve seen
if (mode == ThreadMode::CHANNEL && channel >= 0) { if (mode == ThreadMode::CHANNEL && channel >= 0) {
if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) {
LOG_DEBUG("[MessageRenderer] Track seen channel: %d", channel);
seenChannels.push_back(channel); seenChannels.push_back(channel);
} }
} }
@ -202,7 +230,6 @@ void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0
// Track DMs weve seen // Track DMs weve seen
if (mode == ThreadMode::DIRECT && peer != 0) { if (mode == ThreadMode::DIRECT && peer != 0) {
if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) { if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) {
LOG_DEBUG("[MessageRenderer] Track seen peer: 0x%08x", (unsigned int)peer);
seenPeers.push_back(peer); seenPeers.push_back(peer);
} }
} }
@ -272,6 +299,36 @@ void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8)
display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4);
} }
int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount)
{
std::string normalized = normalizeEmoji(line);
int totalWidth = 0;
size_t i = 0;
while (i < normalized.length()) {
bool matched = false;
for (int e = 0; e < emoteCount; ++e) {
size_t emojiLen = strlen(emotes[e].label);
if (normalized.compare(i, emojiLen, emotes[e].label) == 0) {
totalWidth += emotes[e].width + 1; // +1 spacing
i += emojiLen;
matched = true;
break;
}
}
if (!matched) {
size_t charLen = utf8CharLen(static_cast<uint8_t>(normalized[i]));
#if defined(OLED_UA) || defined(OLED_RU)
totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true);
#else
totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str());
#endif
i += charLen;
}
}
return totalWidth;
}
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
// Ensure any boot-relative timestamps are upgraded if RTC is valid // Ensure any boot-relative timestamps are upgraded if RTC is valid
@ -287,7 +344,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// Filter messages based on thread mode // Filter messages based on thread mode
std::deque<StoredMessage> filtered; std::deque<StoredMessage> filtered;
for (const auto &m : messageStore.getMessages()) { for (const auto &m : messageStore.getLiveMessages()) {
bool include = false; bool include = false;
switch (currentMode) { switch (currentMode) {
case ThreadMode::ALL: case ThreadMode::ALL:
@ -460,7 +517,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
ackForLine.push_back(m.ackStatus); ackForLine.push_back(m.ackStatus);
// Split message text into wrapped lines // Split message text into wrapped lines
std::vector<std::string> wrapped = generateLines(display, "", m.text, textWidth); std::vector<std::string> wrapped = generateLines(display, "", m.text.c_str(), textWidth);
for (auto &ln : wrapped) { for (auto &ln : wrapped) {
allLines.push_back(ln); allLines.push_back(ln);
isMine.push_back(mine); isMine.push_back(mine);
@ -471,7 +528,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// Cache lines and heights // Cache lines and heights
cachedLines = allLines; cachedLines = allLines;
cachedHeights = calculateLineHeights(cachedLines, emotes); cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
// Scrolling logic (unchanged) // Scrolling logic (unchanged)
int totalHeight = 0; int totalHeight = 0;
@ -559,9 +616,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
} else { } else {
// Render message line // Render message line
if (isMine[i]) { if (isMine[i]) {
display->setTextAlignment(TEXT_ALIGN_RIGHT); // Calculate actual rendered width including emotes
drawStringWithEmotes(display, SCREEN_WIDTH, lineY, cachedLines[i], emotes, numEmotes); int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
display->setTextAlignment(TEXT_ALIGN_LEFT); int rightX = SCREEN_WIDTH - renderedWidth - 2; // -2 for slight padding from the edge
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
} else { } else {
drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes); drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes);
} }
@ -623,54 +681,71 @@ std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerS
return lines; return lines;
} }
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes,
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes) const std::vector<bool> &isHeaderVec)
{ {
// Tunables for layout control
constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line
constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn)
constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines
constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header
constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above)
constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line)
std::vector<int> rowHeights; std::vector<int> rowHeights;
rowHeights.reserve(lines.size());
for (size_t idx = 0; idx < lines.size(); ++idx) { for (size_t idx = 0; idx < lines.size(); ++idx) {
const auto &_line = lines[idx]; const auto &line = lines[idx];
int lineHeight = FONT_HEIGHT_SMALL; const int baseHeight = FONT_HEIGHT_SMALL;
// Detect if THIS line or NEXT line contains an emote
bool hasEmote = false; bool hasEmote = false;
bool isHeader = false; int tallestEmote = baseHeight;
// Detect emotes in this line
for (int i = 0; i < numEmotes; ++i) { for (int i = 0; i < numEmotes; ++i) {
const Emote &e = emotes[i]; if (line.find(emotes[i].label) != std::string::npos) {
if (_line.find(e.label) != std::string::npos) {
lineHeight = std::max(lineHeight, e.height);
hasEmote = true; hasEmote = true;
tallestEmote = std::max(tallestEmote, emotes[i].height);
} }
} }
// Detect header lines (start of a message, or time stamps like "5m ago") bool nextHasEmote = false;
if (idx == 0 || _line.find("ago") != std::string::npos || _line.rfind("me ", 0) == 0) { if (idx + 1 < lines.size()) {
isHeader = true; for (int i = 0; i < numEmotes; ++i) {
if (lines[idx + 1].find(emotes[i].label) != std::string::npos) {
nextHasEmote = true;
break;
}
}
} }
// Look ahead to see if next line is a header → this is the last line of a message int lineHeight = baseHeight;
bool beforeHeader =
(idx + 1 < lines.size() && (lines[idx + 1].find("ago") != std::string::npos || lines[idx + 1].rfind("me ", 0) == 0)); if (isHeaderVec[idx]) {
// Header line spacing
lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP;
} else {
// Base spacing for normal lines
int desiredBody = baseHeight + BODY_LINE_LEADING;
if (isHeader) {
// Headers always keep full line height
lineHeight = FONT_HEIGHT_SMALL;
} else if (beforeHeader) {
if (hasEmote) { if (hasEmote) {
// Last line has emote → preserve its height + padding // Emote line: add overshoot + bottom padding
lineHeight = std::max(lineHeight, FONT_HEIGHT_SMALL) + 4; int overshoot = std::max(0, tallestEmote - baseHeight);
lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW;
} else { } else {
// Plain last line → full spacing only // Regular line: no emote → standard spacing
lineHeight = FONT_HEIGHT_SMALL; lineHeight = desiredBody;
// If next line has an emote → add top padding *here*
if (nextHasEmote) {
lineHeight += EMOTE_PADDING_ABOVE;
}
}
// Add block gap if next is a header
if (idx + 1 < lines.size() && isHeaderVec[idx + 1]) {
lineHeight += MESSAGE_BLOCK_GAP;
} }
} else if (!hasEmote) {
// Plain body line, tighter spacing
lineHeight -= 4;
if (lineHeight < 8)
lineHeight = 8; // safe minimum
} else {
// Line has emotes, dont compress
lineHeight += 4; // add breathing room
} }
rowHeights.push_back(lineHeight); rowHeights.push_back(lineHeight);
@ -793,10 +868,10 @@ void handleNewMessage(const StoredMessage &sm, const meshtastic_MeshPacket &pack
screen->showSimpleBanner(banner, inThread ? 1000 : 3000); screen->showSimpleBanner(banner, inThread ? 1000 : 3000);
} }
// No setFrames() here anymore // Always focus into the correct conversation thread when a message arrives
if (packet.from == 0) {
setThreadFor(sm, packet); setThreadFor(sm, packet);
}
// Reset scroll for a clean start
resetScrollState(); resetScrollState();
} }

View File

@ -44,7 +44,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
// Function to calculate heights for each line // Function to calculate heights for each line
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes); std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes,
const std::vector<bool> &isHeaderVec);
// Function to render the message content // Function to render the message content
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x, void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,

View File

@ -44,6 +44,71 @@ extern MessageStore messageStore;
// Remove Canned message screen if no action is taken for some milliseconds // Remove Canned message screen if no action is taken for some milliseconds
#define INACTIVATE_AFTER_MS 20000 #define INACTIVATE_AFTER_MS 20000
// Tokenize a message string into emote/text segments
static std::vector<std::pair<bool, String>> tokenizeMessageWithEmotes(const char *msg)
{
std::vector<std::pair<bool, String>> tokens;
int msgLen = strlen(msg);
int pos = 0;
while (pos < msgLen) {
const graphics::Emote *foundEmote = nullptr;
int foundLen = 0;
for (int j = 0; j < graphics::numEmotes; j++) {
const char *label = graphics::emotes[j].label;
int labelLen = strlen(label);
if (labelLen == 0)
continue;
if (strncmp(msg + pos, label, labelLen) == 0) {
if (!foundEmote || labelLen > foundLen) {
foundEmote = &graphics::emotes[j];
foundLen = labelLen;
}
}
}
if (foundEmote) {
tokens.emplace_back(true, String(foundEmote->label));
pos += foundLen;
} else {
// Find next emote
int nextEmote = msgLen;
for (int j = 0; j < graphics::numEmotes; j++) {
const char *label = graphics::emotes[j].label;
if (!label || !*label)
continue;
const char *found = strstr(msg + pos, label);
if (found && (found - msg) < nextEmote) {
nextEmote = found - msg;
}
}
int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos);
if (textLen > 0) {
tokens.emplace_back(false, String(msg + pos).substring(0, textLen));
pos += textLen;
} else {
break;
}
}
}
return tokens;
}
// Render a single emote token centered vertically on a row
static void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label)
{
const graphics::Emote *emote = nullptr;
for (int j = 0; j < graphics::numEmotes; j++) {
if (label == graphics::emotes[j].label) {
emote = &graphics::emotes[j];
break;
}
}
if (emote) {
int emoteYOffset = (rowHeight - emote->height) / 2; // vertically center the emote
display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap);
nextX += emote->width + 2; // spacing between tokens
}
}
namespace graphics namespace graphics
{ {
extern int bannerSignalBars; extern int bannerSignalBars;
@ -997,7 +1062,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
sm.sender = nodeDB->getNodeNum(); // us sm.sender = nodeDB->getNodeNum(); // us
sm.channelIndex = channel; sm.channelIndex = channel;
strncpy(sm.text, message, MAX_MESSAGE_SIZE - 1); sm.text = std::string(message).substr(0, MAX_MESSAGE_SIZE - 1);
sm.text[MAX_MESSAGE_SIZE - 1] = '\0'; sm.text[MAX_MESSAGE_SIZE - 1] = '\0';
// Classify broadcast vs DM // Classify broadcast vs DM
@ -1841,51 +1906,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor);
// Tokenize input into (isEmote, token) pairs // Tokenize input into (isEmote, token) pairs
std::vector<std::pair<bool, String>> tokens;
const char *msg = msgWithCursor.c_str(); const char *msg = msgWithCursor.c_str();
int msgLen = strlen(msg); std::vector<std::pair<bool, String>> tokens = tokenizeMessageWithEmotes(msg);
int pos = 0;
while (pos < msgLen) {
const graphics::Emote *foundEmote = nullptr;
int foundLen = 0;
for (int j = 0; j < graphics::numEmotes; j++) {
const char *label = graphics::emotes[j].label;
int labelLen = strlen(label);
if (labelLen == 0)
continue;
if (strncmp(msg + pos, label, labelLen) == 0) {
if (!foundEmote || labelLen > foundLen) {
foundEmote = &graphics::emotes[j];
foundLen = labelLen;
}
}
}
if (foundEmote) {
tokens.emplace_back(true, String(foundEmote->label));
pos += foundLen;
} else {
// Find next emote
int nextEmote = msgLen;
for (int j = 0; j < graphics::numEmotes; j++) {
const char *label = graphics::emotes[j].label;
if (!label || !*label)
continue;
const char *found = strstr(msg + pos, label);
if (found && (found - msg) < nextEmote) {
nextEmote = found - msg;
}
}
int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos);
if (textLen > 0) {
tokens.emplace_back(false, String(msg + pos).substring(0, textLen));
pos += textLen;
} else {
break;
}
}
}
// Advanced word-wrapping (emotes + text, split by word, wrap by char if needed) // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed)
std::vector<std::vector<std::pair<bool, String>>> lines; std::vector<std::vector<std::pair<bool, String>>> lines;
std::vector<std::pair<bool, String>> currentLine; std::vector<std::pair<bool, String>> currentLine;
int lineWidth = 0; int lineWidth = 0;
@ -1910,7 +1934,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
} else { } else {
// Text: split by words and wrap inside word if needed // Text: split by words and wrap inside word if needed
String text = token.second; String text = token.second;
pos = 0; int pos = 0;
while (pos < static_cast<int>(text.length())) { while (pos < static_cast<int>(text.length())) {
// Find next space (or end) // Find next space (or end)
int spacePos = text.indexOf(' ', pos); int spacePos = text.indexOf(' ', pos);
@ -1956,18 +1980,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
int nextX = x; int nextX = x;
for (const auto &token : line) { for (const auto &token : line) {
if (token.first) { if (token.first) {
const graphics::Emote *emote = nullptr; // Emote rendering centralized in helper
for (int j = 0; j < graphics::numEmotes; j++) { renderEmote(display, nextX, yLine, rowHeight, token.second);
if (token.second == graphics::emotes[j].label) {
emote = &graphics::emotes[j];
break;
}
}
if (emote) {
int emoteYOffset = (rowHeight - emote->height) / 2;
display->drawXbm(nextX, yLine + emoteYOffset, emote->width, emote->height, emote->bitmap);
nextX += emote->width + 2;
}
} else { } else {
display->drawString(nextX, yLine, token.second); display->drawString(nextX, yLine, token.second);
nextX += display->getStringWidth(token.second); nextX += display->getStringWidth(token.second);
@ -2033,51 +2047,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
bool _highlight = (msgIdx == currentMessageIndex); bool _highlight = (msgIdx == currentMessageIndex);
// Multi-emote tokenization // Multi-emote tokenization
std::vector<std::pair<bool, String>> tokens; // (isEmote, token) std::vector<std::pair<bool, String>> tokens = tokenizeMessageWithEmotes(msg);
int pos = 0;
int msgLen = strlen(msg);
while (pos < msgLen) {
const graphics::Emote *foundEmote = nullptr;
int foundLen = 0;
// Look for any emote label at this pos (prefer longest match)
for (int j = 0; j < graphics::numEmotes; j++) {
const char *label = graphics::emotes[j].label;
int labelLen = strlen(label);
if (labelLen == 0)
continue;
if (strncmp(msg + pos, label, labelLen) == 0) {
if (!foundEmote || labelLen > foundLen) {
foundEmote = &graphics::emotes[j];
foundLen = labelLen;
}
}
}
if (foundEmote) {
tokens.emplace_back(true, String(foundEmote->label));
pos += foundLen;
} else {
// Find next emote
int nextEmote = msgLen;
for (int j = 0; j < graphics::numEmotes; j++) {
const char *label = graphics::emotes[j].label;
if (label[0] == 0)
continue;
const char *found = strstr(msg + pos, label);
if (found && (found - msg) < nextEmote) {
nextEmote = found - msg;
}
}
int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos);
if (textLen > 0) {
tokens.emplace_back(false, String(msg + pos).substring(0, textLen));
pos += textLen;
} else {
break;
}
}
}
// End multi-emote tokenization
// Vertically center based on rowHeight // Vertically center based on rowHeight
int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2;
@ -2098,19 +2068,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
// Draw all tokens left to right // Draw all tokens left to right
for (const auto &token : tokens) { for (const auto &token : tokens) {
if (token.first) { if (token.first) {
// Emote // Emote rendering centralized in helper
const graphics::Emote *emote = nullptr; renderEmote(display, nextX, lineY, rowHeight, token.second);
for (int j = 0; j < graphics::numEmotes; j++) {
if (token.second == graphics::emotes[j].label) {
emote = &graphics::emotes[j];
break;
}
}
if (emote) {
int emoteYOffset = (rowHeight - emote->height) / 2;
display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap);
nextX += emote->width + 2;
}
} else { } else {
// Text // Text
display->drawString(nextX, lineY + textYOffset, token.second); display->drawString(nextX, lineY + textYOffset, token.second);