mirror of
https://github.com/meshtastic/firmware.git
synced 2025-10-27 23:12:39 +00:00
Message view mode
This commit is contained in:
parent
d779821f0e
commit
abcc166f3a
@ -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 ===
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 we’ve seen
|
||||
if (mode == ThreadMode::CHANNEL && channel >= 0) {
|
||||
if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end())
|
||||
seenChannels.push_back(channel);
|
||||
}
|
||||
|
||||
// Track DMs we’ve 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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user