Message view mode

This commit is contained in:
HarukiToreda 2025-09-22 03:30:16 -04:00
parent d779821f0e
commit abcc166f3a
7 changed files with 345 additions and 44 deletions

View File

@ -4,6 +4,10 @@
#include "SPILock.h" #include "SPILock.h"
#include "SafeFile.h" #include "SafeFile.h"
#include "configuration.h" // for millis() #include "configuration.h" // for millis()
#include "graphics/draw/MessageRenderer.h"
using graphics::MessageRenderer::setThreadMode;
using graphics::MessageRenderer::ThreadMode;
MessageStore::MessageStore(const std::string &label) MessageStore::MessageStore(const std::string &label)
{ {
@ -50,6 +54,13 @@ void MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
} }
addLiveMessage(sm); addLiveMessage(sm);
// === Auto-switch thread view on new message ===
if (sm.type == MessageType::BROADCAST) {
setThreadMode(ThreadMode::CHANNEL, sm.channelIndex);
} else if (sm.type == MessageType::DM_TO_US) {
setThreadMode(ThreadMode::DIRECT, -1, sm.sender);
}
} }
// === Outgoing/manual message === // === Outgoing/manual message ===

View File

@ -1446,7 +1446,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
sm.channelIndex = packet->channel; sm.channelIndex = packet->channel;
sm.text = std::string(reinterpret_cast<const char *>(packet->decoded.payload.bytes)); sm.text = std::string(reinterpret_cast<const char *>(packet->decoded.payload.bytes));
// Distinguish between broadcast vs DM to us // Distinguish between broadcast vs DM to us
if (packet->decoded.dest == NODENUM_BROADCAST) { if (packet->decoded.dest == NODENUM_BROADCAST) {
sm.dest = NODENUM_BROADCAST; sm.dest = NODENUM_BROADCAST;
sm.type = MessageType::BROADCAST; sm.type = MessageType::BROADCAST;
@ -1457,6 +1457,13 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
messageStore.addLiveMessage(sm); // RAM only (flash updated at shutdown) messageStore.addLiveMessage(sm); // RAM only (flash updated at shutdown)
// 🔹 Auto-switch thread view
if (sm.type == MessageType::BROADCAST) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex);
} else if (sm.type == MessageType::DM_TO_US) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.sender);
}
// 🔹 Reset scroll so newest message starts from the top // 🔹 Reset scroll so newest message starts from the top
graphics::MessageRenderer::resetScrollState(); graphics::MessageRenderer::resetScrollState();
} else { } else {
@ -1520,7 +1527,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
sm.channelIndex = packet->channel; sm.channelIndex = packet->channel;
sm.text = std::string(reinterpret_cast<const char *>(packet->decoded.payload.bytes)); sm.text = std::string(reinterpret_cast<const char *>(packet->decoded.payload.bytes));
// Distinguish between broadcast vs DM to us // Distinguish between broadcast vs DM to us
if (packet->to == NODENUM_BROADCAST || packet->decoded.dest == NODENUM_BROADCAST) { if (packet->to == NODENUM_BROADCAST || packet->decoded.dest == NODENUM_BROADCAST) {
sm.dest = NODENUM_BROADCAST; sm.dest = NODENUM_BROADCAST;
sm.type = MessageType::BROADCAST; sm.type = MessageType::BROADCAST;
@ -1531,7 +1538,14 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
messageStore.addLiveMessage(sm); // RAM only (flash updated at shutdown) messageStore.addLiveMessage(sm); // RAM only (flash updated at shutdown)
// 🔹 Reset scroll so newest message starts from the top // Auto-switch thread view
if (sm.type == MessageType::BROADCAST) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex);
} else if (sm.type == MessageType::DM_TO_US) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.sender);
}
// Reset scroll so newest message starts from the top
graphics::MessageRenderer::resetScrollState(); graphics::MessageRenderer::resetScrollState();
} }
} }

View File

@ -10,6 +10,7 @@
#include "buzz.h" #include "buzz.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h" #include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h"
@ -349,14 +350,30 @@ void menuHandler::clockMenu()
void menuHandler::messageResponseMenu() void menuHandler::messageResponseMenu()
{ {
enum optionsNumbers { Back = 0, DismissAll = 1, DismissOldest = 2, Preset = 3, Freetext = 4, Aloud = 5, enumEnd = 6 }; enum optionsNumbers { Back = 0, ViewMode, DismissAll, DismissOldest, Preset, Freetext, Aloud, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
optionsArray[options] = "View Mode";
optionsEnumArray[options++] = ViewMode;
optionsArray[options] = "Dismiss All";
optionsEnumArray[options++] = DismissAll;
optionsArray[options] = "Dismiss Oldest";
optionsEnumArray[options++] = DismissOldest;
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
static const char *optionsArray[enumEnd] = {"Back", "Dismiss All", "Dismiss Oldest", "Reply Preset"}; optionsArray[options] = "Reply Preset";
#else #else
static const char *optionsArray[enumEnd] = {"Back", "Dismiss All", "Dismiss Oldest", "Reply via Preset"}; optionsArray[options] = "Reply via Preset";
#endif #endif
static int optionsEnumArray[enumEnd] = {Back, DismissAll, DismissOldest, Preset}; optionsEnumArray[options++] = Preset;
int options = 4;
if (kb_found) { if (kb_found) {
optionsArray[options] = "Reply via Freetext"; optionsArray[options] = "Reply via Freetext";
@ -367,6 +384,7 @@ void menuHandler::messageResponseMenu()
optionsArray[options] = "Read Aloud"; optionsArray[options] = "Read Aloud";
optionsEnumArray[options++] = Aloud; optionsEnumArray[options++] = Aloud;
#endif #endif
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
bannerOptions.message = "Message"; bannerOptions.message = "Message";
@ -377,11 +395,14 @@ void menuHandler::messageResponseMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == DismissAll) { LOG_DEBUG("messageResponseMenu: selected %d", selected);
// Remove all messages if (selected == ViewMode) {
LOG_DEBUG("Switching to message_viewmode_menu");
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
screen->runNow();
} else if (selected == DismissAll) {
messageStore.clearAllMessages(); messageStore.clearAllMessages();
} else if (selected == DismissOldest) { } else if (selected == DismissOldest) {
// Remove only the oldest message
messageStore.dismissOldestMessage(); messageStore.dismissOldestMessage();
} else if (selected == Preset) { } else if (selected == Preset) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
@ -395,19 +416,137 @@ void menuHandler::messageResponseMenu()
} else { } else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
} }
}
#ifdef HAS_I2S #ifdef HAS_I2S
else if (selected == Aloud) { } else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message; const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes); const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
audioThread->readAloud(msg); audioThread->readAloud(msg);
}
#endif #endif
}
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::messageViewModeMenu()
{
// Collect menu entries
static std::vector<std::string> labels;
static std::vector<int> ids;
labels.clear();
ids.clear();
// Back
labels.push_back("Back");
ids.push_back(-1);
// View All
labels.push_back("View All");
ids.push_back(-2);
// --- Add channels with live messages ---
for (int ch = 0; ch < 8; ++ch) {
auto msgs = messageStore.getChannelMessages(ch);
if (!msgs.empty()) {
char buf[20];
snprintf(buf, sizeof(buf), "Channel %d", ch);
labels.push_back(buf);
ids.push_back(100 + ch);
}
}
// --- Add channels from registry ---
for (int ch : graphics::MessageRenderer::getSeenChannels()) {
if (std::find(ids.begin(), ids.end(), 100 + ch) == ids.end()) {
char buf[20];
snprintf(buf, sizeof(buf), "Channel %d", ch);
labels.push_back(buf);
ids.push_back(100 + ch);
}
}
// --- Add DMs from live store ---
auto dms = messageStore.getDirectMessages();
std::vector<uint32_t> uniqueSenders;
for (auto &m : dms) {
if (std::find(uniqueSenders.begin(), uniqueSenders.end(), m.sender) == uniqueSenders.end()) {
uniqueSenders.push_back(m.sender);
}
}
// --- Add DMs from registry ---
for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) {
if (std::find(uniqueSenders.begin(), uniqueSenders.end(), peer) == uniqueSenders.end()) {
uniqueSenders.push_back(peer);
}
}
std::sort(uniqueSenders.begin(), uniqueSenders.end());
for (auto sender : uniqueSenders) {
auto node = nodeDB->getMeshNode(sender);
std::string name;
if (node && node->has_user) {
name = sanitizeString(node->user.long_name).substr(0, 15);
} else {
char buf[20];
snprintf(buf, sizeof(buf), "Node %08X", sender);
name = buf;
}
labels.push_back("DM: " + name);
ids.push_back(sender);
}
// --- Determine active ID ---
int activeId = -2;
auto mode = graphics::MessageRenderer::getThreadMode();
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
activeId = -2;
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
activeId = 100 + graphics::MessageRenderer::getThreadChannel();
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
activeId = (int)graphics::MessageRenderer::getThreadPeer();
}
// Prepare arrays for banner
static std::vector<const char *> options;
static std::vector<int> optionIds;
options.clear();
optionIds.clear();
int initialIndex = 0;
for (size_t i = 0; i < labels.size(); i++) {
options.push_back(labels[i].c_str());
optionIds.push_back(ids[i]);
if (ids[i] == activeId) {
initialIndex = i;
}
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Select View Mode";
bannerOptions.optionsArrayPtr = options.data();
bannerOptions.optionsEnumPtr = optionIds.data();
bannerOptions.optionsCount = options.size();
bannerOptions.InitialSelected = initialIndex;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == -1) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
} else if (selected == -2) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
} else if (selected >= 100) {
int ch = selected - 100;
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch);
} else {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, selected);
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::homeBaseMenu() void menuHandler::homeBaseMenu()
{ {
enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Sleep, enumEnd };
@ -1579,6 +1718,12 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case throttle_message: case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break; break;
case message_response_menu:
messageResponseMenu();
break;
case message_viewmode_menu:
messageViewModeMenu();
break;
} }
menuQueue = menu_none; menuQueue = menu_none;
} }

View File

@ -42,7 +42,9 @@ class menuHandler
key_verification_final_prompt, key_verification_final_prompt,
trace_route_menu, trace_route_menu,
throttle_message, throttle_message,
FrameToggles FrameToggles,
message_response_menu,
message_viewmode_menu // <-- View Mode menu entry
}; };
static screenMenus menuQueue; static screenMenus menuQueue;
@ -57,6 +59,7 @@ class menuHandler
static void TwelveHourPicker(); static void TwelveHourPicker();
static void ClockFacePicker(); static void ClockFacePicker();
static void messageResponseMenu(); static void messageResponseMenu();
static void messageViewModeMenu(); // <-- prototype already here
static void homeBaseMenu(); static void homeBaseMenu();
static void textMessageBaseMenu(); static void textMessageBaseMenu();
static void systemBaseMenu(); static void systemBaseMenu();
@ -94,4 +97,4 @@ class menuHandler
}; };
} // namespace graphics } // namespace graphics
#endif #endif

View File

@ -94,13 +94,13 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
} }
} }
// === Step 2: Baseline alignment === // Step 2: Baseline alignment
int lineHeight = std::max(fontHeight, maxIconHeight); int lineHeight = std::max(fontHeight, maxIconHeight);
int baselineOffset = (lineHeight - fontHeight) / 2; int baselineOffset = (lineHeight - fontHeight) / 2;
int fontY = y + baselineOffset; int fontY = y + baselineOffset;
int fontMidline = fontY + fontHeight / 2; int fontMidline = fontY + fontHeight / 2;
// === Step 3: Render line in segments === // Step 3: Render line in segments
size_t i = 0; size_t i = 0;
bool inBold = false; bool inBold = false;
@ -172,7 +172,7 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
} }
} }
// === Scroll state (file scope so we can reset on new message) === // Scroll state (file scope so we can reset on new message)
float scrollY = 0.0f; float scrollY = 0.0f;
uint32_t lastTime = 0; uint32_t lastTime = 0;
uint32_t scrollStartDelay = 0; uint32_t scrollStartDelay = 0;
@ -181,7 +181,7 @@ bool waitingToReset = false;
bool scrollStarted = false; bool scrollStarted = false;
static bool didReset = false; // <-- add here static bool didReset = false; // <-- add here
// === Reset scroll state when new messages arrive === // Reset scroll state when new messages arrive
void resetScrollState() void resetScrollState()
{ {
scrollY = 0.0f; scrollY = 0.0f;
@ -192,6 +192,60 @@ void resetScrollState()
didReset = false; // <-- now valid didReset = false; // <-- now valid
} }
// --- Current thread state ---
static ThreadMode currentMode = ThreadMode::ALL;
static int currentChannel = -1;
static uint32_t currentPeer = 0;
// --- Registry of seen threads for manual toggle ---
static std::vector<int> seenChannels;
static std::vector<uint32_t> seenPeers;
// Setter so other code can switch threads
void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */)
{
currentMode = mode;
currentChannel = channel;
currentPeer = peer;
didReset = false; // force reset when mode changes
// Track channels weve seen
if (mode == ThreadMode::CHANNEL && channel >= 0) {
if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end())
seenChannels.push_back(channel);
}
// Track DMs weve seen
if (mode == ThreadMode::DIRECT && peer != 0) {
if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end())
seenPeers.push_back(peer);
}
}
ThreadMode getThreadMode()
{
return currentMode;
}
int getThreadChannel()
{
return currentChannel;
}
uint32_t getThreadPeer()
{
return currentPeer;
}
// === Accessors for menuHandler ===
const std::vector<int> &getSeenChannels()
{
return seenChannels;
}
const std::vector<uint32_t> &getSeenPeers()
{
return seenPeers;
}
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
@ -203,8 +257,26 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// Clear the unread message indicator when viewing the message // Clear the unread message indicator when viewing the message
hasUnreadMessage = false; hasUnreadMessage = false;
// === Use live RAM buffer directly (boot handles flash load) === // Filter messages based on thread mode
const auto &msgs = messageStore.getMessages(); std::deque<StoredMessage> filtered;
for (const auto &m : messageStore.getMessages()) {
bool include = false;
switch (currentMode) {
case ThreadMode::ALL:
include = true;
break;
case ThreadMode::CHANNEL:
if (m.type == MessageType::BROADCAST && (int)m.channelIndex == currentChannel)
include = true;
break;
case ThreadMode::DIRECT:
if (m.type == MessageType::DM_TO_US && m.sender == currentPeer)
include = true;
break;
}
if (include)
filtered.push_back(m);
}
display->clear(); display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
@ -222,10 +294,30 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const int textWidth = SCREEN_WIDTH; const int textWidth = SCREEN_WIDTH;
#endif #endif
// === Set Title // Title string depending on mode
static char titleBuf[32];
const char *titleStr = "Messages"; const char *titleStr = "Messages";
switch (currentMode) {
case ThreadMode::ALL:
titleStr = "Messages";
break;
case ThreadMode::CHANNEL:
snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel);
titleStr = titleBuf;
break;
case ThreadMode::DIRECT: {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
if (node && node->has_user) {
snprintf(titleBuf, sizeof(titleBuf), "DM: %s", node->user.short_name);
} else {
snprintf(titleBuf, sizeof(titleBuf), "DM: %08x", currentPeer);
}
titleStr = titleBuf;
break;
}
}
if (msgs.empty()) { if (filtered.empty()) {
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);
didReset = false; didReset = false;
const char *messageString = "No messages"; const char *messageString = "No messages";
@ -238,12 +330,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
return; return;
} }
// === Build lines for all messages (newest first) === // Build lines for filtered messages (newest first)
std::vector<std::string> allLines; std::vector<std::string> allLines;
std::vector<bool> isMine; // track alignment std::vector<bool> isMine; // track alignment
std::vector<bool> isHeader; // track header lines std::vector<bool> isHeader; // track header lines
for (auto it = msgs.rbegin(); it != msgs.rend(); ++it) { for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) {
const auto &m = *it; const auto &m = *it;
// --- Build header line for this message --- // --- Build header line for this message ---
@ -268,21 +360,20 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
sender = "Me"; sender = "Me";
} }
// === Channel / destination labeling === // Channel / destination labeling
char chanType[32]; char chanType[32] = "";
if (m.dest == NODENUM_BROADCAST) { if (currentMode == ThreadMode::ALL) {
// Broadcast to a channel if (m.dest == NODENUM_BROADCAST) {
snprintf(chanType, sizeof(chanType), "(Ch%d)", m.channelIndex + 1); snprintf(chanType, sizeof(chanType), "(Ch%d)", m.channelIndex);
} else { } else {
// Direct message (always to us if it shows up) snprintf(chanType, sizeof(chanType), "(DM)");
snprintf(chanType, sizeof(chanType), "(DM)"); }
} }
// else: leave empty for thread views
// === Calculate how long ago === // Calculate how long ago
uint32_t nowSecs = millis() / 1000; uint32_t nowSecs = millis() / 1000;
uint32_t seconds = (nowSecs > m.timestamp) ? (nowSecs - m.timestamp) : 0; uint32_t seconds = (nowSecs > m.timestamp) ? (nowSecs - m.timestamp) : 0;
// Fallback if timestamp looks bogus (0 or way too large)
bool invalidTime = (m.timestamp == 0 || seconds > 315360000); // >10 years bool invalidTime = (m.timestamp == 0 || seconds > 315360000); // >10 years
char timeBuf[16]; char timeBuf[16];
@ -298,11 +389,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
snprintf(timeBuf, sizeof(timeBuf), "%ud ago", seconds / 86400); snprintf(timeBuf, sizeof(timeBuf), "%ud ago", seconds / 86400);
} }
// Final header line
char headerStr[96]; char headerStr[96];
if (mine) { if (mine) {
snprintf(headerStr, sizeof(headerStr), "me %s %s", timeBuf, chanType); snprintf(headerStr, sizeof(headerStr), "me %s %s", timeBuf, chanType);
} else { } else {
snprintf(headerStr, sizeof(headerStr), "%s from %s %s", timeBuf, sender, chanType); snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, sender, chanType);
} }
// Push header line // Push header line
@ -319,11 +411,11 @@ 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);
// === Scrolling logic (unchanged) === // Scrolling logic (unchanged)
uint32_t now = millis(); uint32_t now = millis();
int totalHeight = 0; int totalHeight = 0;
for (size_t i = 0; i < cachedHeights.size(); ++i) for (size_t i = 0; i < cachedHeights.size(); ++i)
@ -363,7 +455,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int scrollOffset = static_cast<int>(scrollY); int scrollOffset = static_cast<int>(scrollY);
int yOffset = -scrollOffset + getTextPositions(display)[1]; int yOffset = -scrollOffset + getTextPositions(display)[1];
// === Render visible lines === // Render visible lines
for (size_t i = 0; i < cachedLines.size(); ++i) { for (size_t i = 0; i < cachedLines.size(); ++i) {
int lineY = yOffset; int lineY = yOffset;
for (size_t j = 0; j < i; ++j) for (size_t j = 0; j < i; ++j)
@ -393,6 +485,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
} }
} }
} }
// Draw screen title header last // Draw screen title header last
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);
} }

View File

@ -10,6 +10,25 @@ namespace graphics
namespace MessageRenderer namespace MessageRenderer
{ {
// === Thread filter modes ===
enum class ThreadMode { ALL, CHANNEL, DIRECT };
// Setter for switching thread mode
void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0);
// Getter for current mode
ThreadMode getThreadMode();
// Getter for current channel (valid if mode == CHANNEL)
int getThreadChannel();
// Getter for current peer (valid if mode == DIRECT)
uint32_t getThreadPeer();
// --- Registry accessors for menuHandler ---
const std::vector<int> &getSeenChannels();
const std::vector<uint32_t> &getSeenPeers();
// Text and emote rendering // Text and emote rendering
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);

View File

@ -14,6 +14,7 @@
#include "detect/ScanI2C.h" #include "detect/ScanI2C.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/NotificationRenderer.h" #include "graphics/draw/NotificationRenderer.h"
#include "graphics/emotes.h" #include "graphics/emotes.h"
#include "graphics/images.h" #include "graphics/images.h"
@ -953,7 +954,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
p->to = dest; p->to = dest;
p->channel = channel; p->channel = channel;
p->want_ack = true; p->want_ack = true;
p->decoded.dest = dest; // <-- Mirror picker: NODENUM_BROADCAST or node->num p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num
this->lastSentNode = dest; this->lastSentNode = dest;
this->incoming = dest; this->incoming = dest;
@ -977,10 +978,25 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
sm.sender = nodeDB->getNodeNum(); sm.sender = nodeDB->getNodeNum();
sm.channelIndex = channel; sm.channelIndex = channel;
sm.text = std::string(message); sm.text = std::string(message);
sm.dest = dest; // ✅ Will be NODENUM_BROADCAST or node->num
// Classify broadcast vs DM
if (dest == NODENUM_BROADCAST) {
sm.dest = NODENUM_BROADCAST;
sm.type = MessageType::BROADCAST;
} else {
sm.dest = dest;
sm.type = MessageType::DM_TO_US;
}
messageStore.addLiveMessage(sm); messageStore.addLiveMessage(sm);
// Auto-switch thread view on outgoing message
if (sm.type == MessageType::BROADCAST) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex);
} else {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest);
}
playComboTune(); playComboTune();
} }