mirror of
https://github.com/meshtastic/firmware.git
synced 2025-10-28 15:22:55 +00:00
more fixes
This commit is contained in:
parent
e38925834d
commit
0b11f93880
@ -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.
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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 we’ve seen
|
// Track channels we’ve 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 we’ve seen
|
// Track DMs we’ve 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, don’t 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user