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 "SafeFile.h"
#include "configuration.h" // for millis()
#include "graphics/draw/MessageRenderer.h"
using graphics::MessageRenderer::setThreadMode;
using graphics::MessageRenderer::ThreadMode;
MessageStore::MessageStore(const std::string &label)
{
@ -50,6 +54,13 @@ void MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
}
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 ===

View File

@ -1446,7 +1446,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
sm.channelIndex = packet->channel;
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) {
sm.dest = NODENUM_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)
// 🔹 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();
} else {
@ -1520,7 +1527,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
sm.channelIndex = packet->channel;
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) {
sm.dest = NODENUM_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)
// 🔹 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();
}
}

View File

@ -10,6 +10,7 @@
#include "buzz.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
@ -349,14 +350,30 @@ void menuHandler::clockMenu()
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)
static const char *optionsArray[enumEnd] = {"Back", "Dismiss All", "Dismiss Oldest", "Reply Preset"};
optionsArray[options] = "Reply Preset";
#else
static const char *optionsArray[enumEnd] = {"Back", "Dismiss All", "Dismiss Oldest", "Reply via Preset"};
optionsArray[options] = "Reply via Preset";
#endif
static int optionsEnumArray[enumEnd] = {Back, DismissAll, DismissOldest, Preset};
int options = 4;
optionsEnumArray[options++] = Preset;
if (kb_found) {
optionsArray[options] = "Reply via Freetext";
@ -367,6 +384,7 @@ void menuHandler::messageResponseMenu()
optionsArray[options] = "Read Aloud";
optionsEnumArray[options++] = Aloud;
#endif
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Message";
@ -377,11 +395,14 @@ void menuHandler::messageResponseMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == DismissAll) {
// Remove all messages
LOG_DEBUG("messageResponseMenu: selected %d", selected);
if (selected == ViewMode) {
LOG_DEBUG("Switching to message_viewmode_menu");
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
screen->runNow();
} else if (selected == DismissAll) {
messageStore.clearAllMessages();
} else if (selected == DismissOldest) {
// Remove only the oldest message
messageStore.dismissOldestMessage();
} else if (selected == Preset) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
@ -395,19 +416,137 @@ void menuHandler::messageResponseMenu()
} else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
}
}
#ifdef HAS_I2S
else if (selected == Aloud) {
} else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
audioThread->readAloud(msg);
}
#endif
}
};
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()
{
enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Sleep, enumEnd };
@ -1579,6 +1718,12 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;
case message_response_menu:
messageResponseMenu();
break;
case message_viewmode_menu:
messageViewModeMenu();
break;
}
menuQueue = menu_none;
}

View File

@ -42,7 +42,9 @@ class menuHandler
key_verification_final_prompt,
trace_route_menu,
throttle_message,
FrameToggles
FrameToggles,
message_response_menu,
message_viewmode_menu // <-- View Mode menu entry
};
static screenMenus menuQueue;
@ -57,6 +59,7 @@ class menuHandler
static void TwelveHourPicker();
static void ClockFacePicker();
static void messageResponseMenu();
static void messageViewModeMenu(); // <-- prototype already here
static void homeBaseMenu();
static void textMessageBaseMenu();
static void systemBaseMenu();
@ -94,4 +97,4 @@ class menuHandler
};
} // 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 baselineOffset = (lineHeight - fontHeight) / 2;
int fontY = y + baselineOffset;
int fontMidline = fontY + fontHeight / 2;
// === Step 3: Render line in segments ===
// Step 3: Render line in segments
size_t i = 0;
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;
uint32_t lastTime = 0;
uint32_t scrollStartDelay = 0;
@ -181,7 +181,7 @@ bool waitingToReset = false;
bool scrollStarted = false;
static bool didReset = false; // <-- add here
// === Reset scroll state when new messages arrive ===
// Reset scroll state when new messages arrive
void resetScrollState()
{
scrollY = 0.0f;
@ -192,6 +192,60 @@ void resetScrollState()
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)
{
@ -203,8 +257,26 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// Clear the unread message indicator when viewing the message
hasUnreadMessage = false;
// === Use live RAM buffer directly (boot handles flash load) ===
const auto &msgs = messageStore.getMessages();
// Filter messages based on thread mode
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->setTextAlignment(TEXT_ALIGN_LEFT);
@ -222,10 +294,30 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const int textWidth = SCREEN_WIDTH;
#endif
// === Set Title
// Title string depending on mode
static char titleBuf[32];
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);
didReset = false;
const char *messageString = "No messages";
@ -238,12 +330,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
return;
}
// === Build lines for all messages (newest first) ===
// Build lines for filtered messages (newest first)
std::vector<std::string> allLines;
std::vector<bool> isMine; // track alignment
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;
// --- Build header line for this message ---
@ -268,21 +360,20 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
sender = "Me";
}
// === Channel / destination labeling ===
char chanType[32];
if (m.dest == NODENUM_BROADCAST) {
// Broadcast to a channel
snprintf(chanType, sizeof(chanType), "(Ch%d)", m.channelIndex + 1);
} else {
// Direct message (always to us if it shows up)
snprintf(chanType, sizeof(chanType), "(DM)");
// Channel / destination labeling
char chanType[32] = "";
if (currentMode == ThreadMode::ALL) {
if (m.dest == NODENUM_BROADCAST) {
snprintf(chanType, sizeof(chanType), "(Ch%d)", m.channelIndex);
} else {
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 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
char timeBuf[16];
@ -298,11 +389,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
snprintf(timeBuf, sizeof(timeBuf), "%ud ago", seconds / 86400);
}
// Final header line
char headerStr[96];
if (mine) {
snprintf(headerStr, sizeof(headerStr), "me %s %s", timeBuf, chanType);
} 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
@ -319,11 +411,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
}
}
// === Cache lines and heights ===
// Cache lines and heights
cachedLines = allLines;
cachedHeights = calculateLineHeights(cachedLines, emotes);
// === Scrolling logic (unchanged) ===
// Scrolling logic (unchanged)
uint32_t now = millis();
int totalHeight = 0;
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 yOffset = -scrollOffset + getTextPositions(display)[1];
// === Render visible lines ===
// Render visible lines
for (size_t i = 0; i < cachedLines.size(); ++i) {
int lineY = yOffset;
for (size_t j = 0; j < i; ++j)
@ -393,6 +485,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
}
}
}
// Draw screen title header last
graphics::drawCommonHeader(display, x, y, titleStr);
}

View File

@ -10,6 +10,25 @@ namespace graphics
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
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 "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/NotificationRenderer.h"
#include "graphics/emotes.h"
#include "graphics/images.h"
@ -953,7 +954,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
p->to = dest;
p->channel = channel;
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->incoming = dest;
@ -977,10 +978,25 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
sm.sender = nodeDB->getNodeNum();
sm.channelIndex = channel;
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);
// 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();
}