mirror of
https://github.com/meshtastic/firmware.git
synced 2025-10-28 07:13:25 +00:00
more fixes
This commit is contained in:
parent
e38925834d
commit
0b11f93880
@ -8,6 +8,8 @@
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
|
||||
#include <algorithm> // 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<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)
|
||||
@ -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<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) {
|
||||
// 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<size_t>(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<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const
|
||||
{
|
||||
@ -352,6 +384,19 @@ std::deque<StoredMessage> MessageStore::getDirectMessages() const
|
||||
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
|
||||
// Only same-boot boot-relative messages are healed.
|
||||
// Persisted boot-relative messages from old boots stay ??? forever.
|
||||
|
||||
@ -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 };
|
||||
@ -39,7 +43,7 @@ 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
|
||||
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<StoredMessage> &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();
|
||||
|
||||
@ -47,25 +47,56 @@ 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<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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
i += utf8CharLen(static_cast<uint8_t>(line[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Baseline alignment
|
||||
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)
|
||||
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<uint32_t> 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<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)
|
||||
{
|
||||
// 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<StoredMessage> 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<std::string> wrapped = generateLines(display, "", m.text, textWidth);
|
||||
std::vector<std::string> 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<std::string> generateLines(OLEDDisplay *display, const char *headerS
|
||||
|
||||
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;
|
||||
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 (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) {
|
||||
// Last line has emote → preserve its height + padding
|
||||
lineHeight = std::max(lineHeight, FONT_HEIGHT_SMALL) + 4;
|
||||
// Emote line: add overshoot + bottom padding
|
||||
int overshoot = std::max(0, tallestEmote - baseHeight);
|
||||
lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW;
|
||||
} else {
|
||||
// Plain last line → full spacing only
|
||||
lineHeight = FONT_HEIGHT_SMALL;
|
||||
// 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;
|
||||
}
|
||||
} else if (!hasEmote) {
|
||||
// Plain body line, tighter spacing
|
||||
lineHeight -= 4;
|
||||
if (lineHeight < 8)
|
||||
lineHeight = 8; // safe minimum
|
||||
} else {
|
||||
// Line has emotes, don’t compress
|
||||
lineHeight += 4; // add breathing room
|
||||
}
|
||||
|
||||
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) {
|
||||
// Always focus into the correct conversation thread when a message arrives
|
||||
setThreadFor(sm, packet);
|
||||
}
|
||||
|
||||
// Reset scroll for a clean start
|
||||
resetScrollState();
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
// 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
|
||||
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
|
||||
|
||||
@ -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<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
|
||||
{
|
||||
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<std::pair<bool, String>> 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<std::pair<bool, String>> 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<std::vector<std::pair<bool, String>>> lines;
|
||||
std::vector<std::pair<bool, String>> 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<int>(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<std::pair<bool, String>> 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<std::pair<bool, String>> 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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user