diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index 7a945df3f..322e13fe6 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -8,6 +8,8 @@ #include "gps/RTC.h" #include "graphics/draw/MessageRenderer.h" +#include // std::min, std::find_if + using graphics::MessageRenderer::setThreadMode; using graphics::MessageRenderer::ThreadMode; @@ -15,7 +17,8 @@ using graphics::MessageRenderer::ThreadMode; static inline size_t getMessageSize(const StoredMessage &m) { // serialized size = fixed 16 bytes + text length (capped at MAX_MESSAGE_SIZE) - return 16 + std::min(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(MAX_MESSAGE_SIZE), m.text.size()); } static inline void assignTimestamp(StoredMessage &sm) @@ -52,20 +55,34 @@ void MessageStore::addLiveMessage(StoredMessage &&msg) if (liveMessages.size() >= MAX_MESSAGES_SAVED) { liveMessages.pop_front(); // keep only most recent N } - // Use emplace_back with std::move to avoid extra copy 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) void MessageStore::addMessage(StoredMessage &&msg) { if (messages.size() >= MAX_MESSAGES_SAVED) { messages.pop_front(); } - // Use emplace_back with std::move to avoid extra copy 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) { StoredMessage sm; @@ -73,8 +90,9 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa assignTimestamp(sm); sm.channelIndex = packet.channel; - strncpy(sm.text, reinterpret_cast(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(packet.decoded.payload.bytes)); if (packet.from == 0) { // Phone-originated (outgoing) @@ -107,7 +125,7 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa sm.ackStatus = AckStatus::ACKED; } - addLiveMessage(std::move(sm)); + addLiveMessage(sm); // Return reference to the most recently stored message return liveMessages.back(); @@ -123,8 +141,7 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st sm.sender = sender; sm.channelIndex = channelIndex; - strncpy(sm.text, text.c_str(), MAX_MESSAGE_SIZE - 1); - sm.text[MAX_MESSAGE_SIZE - 1] = '\0'; + sm.text = text; // Default manual adds to 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 sm.ackStatus = AckStatus::NONE; - addLiveMessage(std::move(sm)); + addLiveMessage(sm); } #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.channelIndex, sizeof(m.channelIndex)); 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(MAX_MESSAGE_SIZE), m.text.size()); + if (toWrite) + f.write((const uint8_t *)m.text.data(), toWrite); f.write('\0'); // null terminator 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.channelIndex, sizeof(m.channelIndex)); 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) uint8_t bootFlag = 0; @@ -295,6 +327,16 @@ void MessageStore::dismissOldestMessage() 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 void MessageStore::dismissOldestMessageInChannel(uint8_t channel) { @@ -319,16 +361,6 @@ void MessageStore::dismissOldestMessageWithPeer(uint32_t peer) 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 std::deque MessageStore::getChannelMessages(uint8_t channel) const { @@ -352,6 +384,19 @@ std::deque MessageStore::getDirectMessages() const return result; } +std::deque MessageStore::getConversationWith(uint32_t peer) const +{ + std::deque 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 // Only same-boot boot-relative messages are healed. // Persisted boot-relative messages from old boots stay ??? forever. @@ -384,4 +429,4 @@ void MessageStore::upgradeBootRelativeTimestamps() // Global definition MessageStore messageStore("default"); -#endif +#endif \ No newline at end of file diff --git a/src/MessageStore.h b/src/MessageStore.h index 8b45daef9..d20c2596a 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -21,7 +21,11 @@ #ifndef MAX_MESSAGES_SAVED #define MAX_MESSAGES_SAVED 20 #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 enum class MessageType : uint8_t { BROADCAST = 0, DM_TO_US = 1 }; @@ -36,10 +40,10 @@ enum class AckStatus : uint8_t { }; struct StoredMessage { - uint32_t timestamp; // When message was created (secs since boot/RTC) - uint32_t sender; // NodeNum of sender - uint8_t channelIndex; // Channel index used - char text[MAX_MESSAGE_SIZE]; // UTF-8 text payload + uint32_t timestamp; // When message was created (secs since boot/RTC) + uint32_t sender; // NodeNum of sender + uint8_t channelIndex; // Channel index used + std::string text; // UTF-8 text payload (dynamic now, no fixed size) // Destination node. // 0xffffffff (NODENUM_BROADCAST) means broadcast, @@ -71,10 +75,12 @@ class MessageStore // Live RAM methods (always current, used by UI and runtime) void addLiveMessage(StoredMessage &&msg); + void addLiveMessage(const StoredMessage &msg); // convenience overload const std::deque &getLiveMessages() const { return liveMessages; } // Persistence methods (used only on boot/shutdown) void addMessage(StoredMessage &&msg); + void addMessage(const StoredMessage &msg); // convenience overload const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); void saveToFlash(); diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 7905d92c1..7ddd72e72 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -47,24 +47,55 @@ inline size_t utf8CharLen(uint8_t c) 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(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) { + std::string renderLine = normalizeEmoji(line); int cursorX = x; const int fontHeight = FONT_HEIGHT_SMALL; // Step 1: Find tallest emote in the line int maxIconHeight = fontHeight; for (size_t i = 0; i < line.length();) { + bool matched = false; for (int e = 0; e < emoteCount; ++e) { size_t emojiLen = strlen(emotes[e].label); if (line.compare(i, emojiLen, emotes[e].label) == 0) { if (emotes[e].height > maxIconHeight) maxIconHeight = emotes[e].height; i += emojiLen; + matched = true; break; } } - i += utf8CharLen(static_cast(line[i])); + if (!matched) { + i += utf8CharLen(static_cast(line[i])); + } } // Step 2: Baseline alignment @@ -122,11 +153,12 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string // Render the emote (if found) if (matchedEmote && i == nextEmotePos) { - // Center vertically — padding handled in calculateLineHeights - int iconY = fontMidline - matchedEmote->height / 2; + // Vertically center emote relative to font baseline (not just midline) + int iconY = fontY + (fontHeight - matchedEmote->height) / 2; display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); cursorX += matchedEmote->width + 1; i += emojiLen; + continue; } else { // No more emotes — render the rest of the line std::string remaining = line.substr(i); @@ -139,7 +171,6 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string #else cursorX += display->getStringWidth(remaining.c_str()); #endif - break; } } @@ -177,7 +208,6 @@ static std::vector seenPeers; // Public helper so menus / store can clear stale registries void clearThreadRegistries() { - LOG_DEBUG("[MessageRenderer] Clearing thread registries (seenChannels/seenPeers)"); seenChannels.clear(); seenPeers.clear(); } @@ -185,7 +215,6 @@ void clearThreadRegistries() // Setter so other code can switch threads 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; currentChannel = channel; currentPeer = peer; @@ -194,7 +223,6 @@ void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 // Track channels we’ve seen if (mode == ThreadMode::CHANNEL && channel >= 0) { if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { - LOG_DEBUG("[MessageRenderer] Track seen channel: %d", channel); seenChannels.push_back(channel); } } @@ -202,7 +230,6 @@ void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 // Track DMs we’ve seen if (mode == ThreadMode::DIRECT && peer != 0) { 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); } } @@ -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); } +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(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) { // 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 std::deque filtered; - for (const auto &m : messageStore.getMessages()) { + for (const auto &m : messageStore.getLiveMessages()) { bool include = false; switch (currentMode) { case ThreadMode::ALL: @@ -460,7 +517,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 ackForLine.push_back(m.ackStatus); // Split message text into wrapped lines - std::vector wrapped = generateLines(display, "", m.text, textWidth); + std::vector wrapped = generateLines(display, "", m.text.c_str(), textWidth); for (auto &ln : wrapped) { allLines.push_back(ln); isMine.push_back(mine); @@ -471,7 +528,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // Cache lines and heights cachedLines = allLines; - cachedHeights = calculateLineHeights(cachedLines, emotes); + cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); // Scrolling logic (unchanged) int totalHeight = 0; @@ -559,9 +616,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } else { // Render message line if (isMine[i]) { - display->setTextAlignment(TEXT_ALIGN_RIGHT); - drawStringWithEmotes(display, SCREEN_WIDTH, lineY, cachedLines[i], emotes, numEmotes); - display->setTextAlignment(TEXT_ALIGN_LEFT); + // Calculate actual rendered width including emotes + int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + int rightX = SCREEN_WIDTH - renderedWidth - 2; // -2 for slight padding from the edge + drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); } else { drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes); } @@ -623,54 +681,71 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS return lines; } - -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes) +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, + const std::vector &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 rowHeights; + rowHeights.reserve(lines.size()); for (size_t idx = 0; idx < lines.size(); ++idx) { - const auto &_line = lines[idx]; - int lineHeight = FONT_HEIGHT_SMALL; + const auto &line = lines[idx]; + const int baseHeight = FONT_HEIGHT_SMALL; + + // Detect if THIS line or NEXT line contains an emote bool hasEmote = false; - bool isHeader = false; - - // Detect emotes in this line + int tallestEmote = baseHeight; for (int i = 0; i < numEmotes; ++i) { - const Emote &e = emotes[i]; - if (_line.find(e.label) != std::string::npos) { - lineHeight = std::max(lineHeight, e.height); + if (line.find(emotes[i].label) != std::string::npos) { hasEmote = true; + tallestEmote = std::max(tallestEmote, emotes[i].height); } } - // Detect header lines (start of a message, or time stamps like "5m ago") - if (idx == 0 || _line.find("ago") != std::string::npos || _line.rfind("me ", 0) == 0) { - isHeader = true; + bool nextHasEmote = false; + if (idx + 1 < lines.size()) { + 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 - bool beforeHeader = - (idx + 1 < lines.size() && (lines[idx + 1].find("ago") != std::string::npos || lines[idx + 1].rfind("me ", 0) == 0)); + int lineHeight = baseHeight; - if (isHeader) { - // Headers always keep full line height - lineHeight = FONT_HEIGHT_SMALL; - } else if (beforeHeader) { - if (hasEmote) { - // Last line has emote → preserve its height + padding - lineHeight = std::max(lineHeight, FONT_HEIGHT_SMALL) + 4; - } else { - // Plain last line → full spacing only - lineHeight = FONT_HEIGHT_SMALL; - } - } else if (!hasEmote) { - // Plain body line, tighter spacing - lineHeight -= 4; - if (lineHeight < 8) - lineHeight = 8; // safe minimum + if (isHeaderVec[idx]) { + // Header line spacing + lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; } else { - // Line has emotes, don’t compress - lineHeight += 4; // add breathing room + // Base spacing for normal lines + int desiredBody = baseHeight + BODY_LINE_LEADING; + + if (hasEmote) { + // Emote line: add overshoot + bottom padding + int overshoot = std::max(0, tallestEmote - baseHeight); + lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW; + } else { + // Regular line: no emote → standard spacing + 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; + } } rowHeights.push_back(lineHeight); @@ -793,10 +868,10 @@ void handleNewMessage(const StoredMessage &sm, const meshtastic_MeshPacket &pack screen->showSimpleBanner(banner, inThread ? 1000 : 3000); } - // No setFrames() here anymore - if (packet.from == 0) { - setThreadFor(sm, packet); - } + // Always focus into the correct conversation thread when a message arrives + setThreadFor(sm, packet); + + // Reset scroll for a clean start resetScrollState(); } diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h index b56cccbec..ab1edc4ae 100644 --- a/src/graphics/draw/MessageRenderer.h +++ b/src/graphics/draw/MessageRenderer.h @@ -44,7 +44,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); // Function to calculate heights for each line -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes); +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, + const std::vector &isHeaderVec); // Function to render the message content void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index fb77c9662..c0ee60545 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -44,6 +44,71 @@ extern MessageStore messageStore; // Remove Canned message screen if no action is taken for some milliseconds #define INACTIVATE_AFTER_MS 20000 +// Tokenize a message string into emote/text segments +static std::vector> tokenizeMessageWithEmotes(const char *msg) +{ + std::vector> 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 { extern int bannerSignalBars; @@ -997,7 +1062,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha sm.sender = nodeDB->getNodeNum(); // us 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'; // Classify broadcast vs DM @@ -1841,51 +1906,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); // Tokenize input into (isEmote, token) pairs - std::vector> tokens; const char *msg = msgWithCursor.c_str(); - 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; - } - } - } + std::vector> tokens = tokenizeMessageWithEmotes(msg); - // 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>> lines; std::vector> currentLine; int lineWidth = 0; @@ -1910,7 +1934,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } else { // Text: split by words and wrap inside word if needed String text = token.second; - pos = 0; + int pos = 0; while (pos < static_cast(text.length())) { // Find next space (or end) int spacePos = text.indexOf(' ', pos); @@ -1956,18 +1980,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int nextX = x; for (const auto &token : line) { if (token.first) { - const graphics::Emote *emote = nullptr; - 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, yLine + emoteYOffset, emote->width, emote->height, emote->bitmap); - nextX += emote->width + 2; - } + // Emote rendering centralized in helper + renderEmote(display, nextX, yLine, rowHeight, token.second); } else { display->drawString(nextX, yLine, token.second); nextX += display->getStringWidth(token.second); @@ -2033,51 +2047,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st bool _highlight = (msgIdx == currentMessageIndex); // Multi-emote tokenization - std::vector> tokens; // (isEmote, token) - 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 + std::vector> tokens = tokenizeMessageWithEmotes(msg); // Vertically center based on rowHeight int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; @@ -2098,19 +2068,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Draw all tokens left to right for (const auto &token : tokens) { if (token.first) { - // Emote - const graphics::Emote *emote = nullptr; - 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; - } + // Emote rendering centralized in helper + renderEmote(display, nextX, lineY, rowHeight, token.second); } else { // Text display->drawString(nextX, lineY + textYOffset, token.second);