Reply in thread feature

This commit is contained in:
HarukiToreda 2025-09-23 03:42:32 -04:00
parent 3780290581
commit ea7638b4ec
5 changed files with 128 additions and 77 deletions

View File

@ -65,11 +65,14 @@ void MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
addLiveMessage(sm); addLiveMessage(sm);
// === Auto-switch thread view on new message === // === Auto-switch ONLY if we're currently in ALL-mode ===
if (sm.type == MessageType::BROADCAST) { using graphics::MessageRenderer::getThreadMode;
setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); if (getThreadMode() == ThreadMode::ALL) {
} else if (sm.type == MessageType::DM_TO_US) { if (sm.type == MessageType::BROADCAST) {
setThreadMode(ThreadMode::DIRECT, -1, sm.sender); setThreadMode(ThreadMode::CHANNEL, sm.channelIndex);
} else if (sm.type == MessageType::DM_TO_US) {
setThreadMode(ThreadMode::DIRECT, -1, sm.sender);
}
} }
} }

View File

@ -396,6 +396,13 @@ void menuHandler::messageResponseMenu()
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
LOG_DEBUG("messageResponseMenu: selected %d", selected); LOG_DEBUG("messageResponseMenu: selected %d", selected);
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
if (selected == ViewMode) { if (selected == ViewMode) {
LOG_DEBUG("Switching to message_viewmode_menu"); LOG_DEBUG("Switching to message_viewmode_menu");
menuHandler::menuQueue = menuHandler::message_viewmode_menu; menuHandler::menuQueue = menuHandler::message_viewmode_menu;
@ -404,17 +411,33 @@ void menuHandler::messageResponseMenu()
messageStore.clearAllMessages(); messageStore.clearAllMessages();
} else if (selected == DismissOldest) { } else if (selected == DismissOldest) {
messageStore.dismissOldestMessage(); messageStore.dismissOldestMessage();
} else if (selected == Preset) { } else if (selected == Preset || selected == Freetext) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); LOG_DEBUG("Replying to CHANNEL %d", ch);
if (selected == Preset)
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
else
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
LOG_DEBUG("Replying to DIRECT peer=0x%08x", peer);
if (selected == Preset)
cannedMessageModule->LaunchWithDestination(peer);
else
cannedMessageModule->LaunchFreetextWithDestination(peer);
} else { } else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); LOG_DEBUG("Fallback reply using last rx_text_message");
} if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
} else if (selected == Freetext) { if (selected == Preset)
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); else
} else { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST,
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); devicestate.rx_text_message.channel);
} else {
if (selected == Preset)
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
else
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
}
} }
#ifdef HAS_I2S #ifdef HAS_I2S
} else if (selected == Aloud) { } else if (selected == Aloud) {
@ -429,97 +452,103 @@ void menuHandler::messageResponseMenu()
void menuHandler::messageViewModeMenu() void menuHandler::messageViewModeMenu()
{ {
// Collect menu entries auto encodeChannelId = [](int ch) -> int { return 100 + ch; };
auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; };
static std::vector<std::string> labels; static std::vector<std::string> labels;
static std::vector<int> ids; static std::vector<int> ids;
static std::vector<uint32_t> idToPeer; // DM lookup
labels.clear(); labels.clear();
ids.clear(); ids.clear();
idToPeer.clear();
// Back
labels.push_back("Back"); labels.push_back("Back");
ids.push_back(-1); ids.push_back(-1);
// View All
labels.push_back("View All"); labels.push_back("View All");
ids.push_back(-2); ids.push_back(-2);
// Add channels with live messages // Channels with messages
for (int ch = 0; ch < 8; ++ch) { for (int ch = 0; ch < 8; ++ch) {
auto msgs = messageStore.getChannelMessages(ch); auto msgs = messageStore.getChannelMessages((uint8_t)ch);
if (!msgs.empty()) { if (!msgs.empty()) {
char buf[40]; char buf[40];
const char *cname = channels.getName(ch); const char *cname = channels.getName(ch);
if (cname && cname[0]) { snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
snprintf(buf, sizeof(buf), "#%s", cname);
} else {
snprintf(buf, sizeof(buf), "#Ch%d", ch);
}
labels.push_back(buf); labels.push_back(buf);
ids.push_back(100 + ch); ids.push_back(encodeChannelId(ch));
LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch));
} }
} }
// Add channels from registry // Registry channels
for (int ch : graphics::MessageRenderer::getSeenChannels()) { for (int ch : graphics::MessageRenderer::getSeenChannels()) {
if (std::find(ids.begin(), ids.end(), 100 + ch) == ids.end()) { if (ch < 0 || ch >= 8)
continue;
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
if (msgs.empty())
continue;
int enc = encodeChannelId(ch);
if (std::find(ids.begin(), ids.end(), enc) == ids.end()) {
char buf[40]; char buf[40];
const char *cname = channels.getName(ch); const char *cname = channels.getName(ch);
if (cname && cname[0]) { snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
snprintf(buf, sizeof(buf), "#%s", cname);
} else {
snprintf(buf, sizeof(buf), "#Ch%d", ch);
}
labels.push_back(buf); labels.push_back(buf);
ids.push_back(100 + ch); ids.push_back(enc);
LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc);
} }
} }
// Add DMs from live store // Gather unique peers
auto dms = messageStore.getDirectMessages(); auto dms = messageStore.getDirectMessages();
std::vector<uint32_t> uniquePeers; std::vector<uint32_t> uniquePeers;
for (auto &m : dms) { for (auto &m : dms) {
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) { if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer); uniquePeers.push_back(peer);
}
} }
// Add DMs from registry
for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) { for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) {
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) { if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer); uniquePeers.push_back(peer);
}
} }
std::sort(uniquePeers.begin(), uniquePeers.end()); std::sort(uniquePeers.begin(), uniquePeers.end());
for (auto peer : uniquePeers) { // Encode peers
for (size_t i = 0; i < uniquePeers.size(); ++i) {
uint32_t peer = uniquePeers[i];
auto node = nodeDB->getMeshNode(peer); auto node = nodeDB->getMeshNode(peer);
std::string name; std::string name;
if (node && node->has_user) { if (node && node->has_user)
name = sanitizeString(node->user.long_name).substr(0, 15); name = sanitizeString(node->user.long_name).substr(0, 15);
} else { else {
char buf[20]; char buf[20];
snprintf(buf, sizeof(buf), "Node %08X", peer); snprintf(buf, sizeof(buf), "Node %08X", peer);
name = buf; name = buf;
} }
labels.push_back("DM: " + name); labels.push_back("DM: " + name);
ids.push_back(peer); int encPeer = 1000 + (int)idToPeer.size();
ids.push_back(encPeer);
idToPeer.push_back(peer);
LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer);
} }
// Determine active ID // Active ID
int activeId = -2; int activeId = -2;
auto mode = graphics::MessageRenderer::getThreadMode(); auto mode = graphics::MessageRenderer::getThreadMode();
if (mode == graphics::MessageRenderer::ThreadMode::ALL) { if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL)
activeId = -2; activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel());
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
activeId = 100 + graphics::MessageRenderer::getThreadChannel(); uint32_t cur = graphics::MessageRenderer::getThreadPeer();
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { for (size_t i = 0; i < idToPeer.size(); ++i)
activeId = (int)graphics::MessageRenderer::getThreadPeer(); if (idToPeer[i] == cur) {
activeId = 1000 + (int)i;
break;
}
} }
// Prepare arrays for banner LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId);
// Build banner
static std::vector<const char *> options; static std::vector<const char *> options;
static std::vector<int> optionIds; static std::vector<int> optionIds;
options.clear(); options.clear();
@ -529,9 +558,8 @@ void menuHandler::messageViewModeMenu()
for (size_t i = 0; i < labels.size(); i++) { for (size_t i = 0; i < labels.size(); i++) {
options.push_back(labels[i].c_str()); options.push_back(labels[i].c_str());
optionIds.push_back(ids[i]); optionIds.push_back(ids[i]);
if (ids[i] == activeId) { if (ids[i] == activeId)
initialIndex = i; initialIndex = (int)i;
}
} }
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
@ -541,20 +569,24 @@ void menuHandler::messageViewModeMenu()
bannerOptions.optionsCount = options.size(); bannerOptions.optionsCount = options.size();
bannerOptions.InitialSelected = initialIndex; bannerOptions.InitialSelected = initialIndex;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [=](int selected) -> void {
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
if (selected == -1) { if (selected == -1) {
menuHandler::menuQueue = menuHandler::message_response_menu; menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow(); screen->runNow();
} else if (selected == -2) { } else if (selected == -2) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
} else if (selected >= 100) { } else if (isChannelSel(selected)) {
int ch = selected - 100; int ch = selected - 100;
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch); graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch);
} else { } else if (selected >= 1000) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, selected); int idx = selected - 1000;
if (idx >= 0 && (size_t)idx < idToPeer.size()) {
uint32_t peer = idToPeer[idx];
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer);
}
} }
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }

View File

@ -199,9 +199,18 @@ static uint32_t currentPeer = 0;
static std::vector<int> seenChannels; static std::vector<int> seenChannels;
static std::vector<uint32_t> seenPeers; static std::vector<uint32_t> seenPeers;
// Public helper so menus / store can clear stale registries
void clearThreadRegistries()
{
LOG_DEBUG("[MessageRenderer] Clearing thread registries (seenChannels/seenPeers)");
seenChannels.clear();
seenPeers.clear();
}
// 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;
@ -209,14 +218,18 @@ void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0
// Track channels weve seen // Track channels weve 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);
}
} }
// Track DMs weve seen // Track DMs weve 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);
}
} }
} }

View File

@ -29,6 +29,8 @@ uint32_t getThreadPeer();
const std::vector<int> &getSeenChannels(); const std::vector<int> &getSeenChannels();
const std::vector<uint32_t> &getSeenPeers(); const std::vector<uint32_t> &getSeenPeers();
void clearThreadRegistries();
// 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);
@ -49,4 +51,4 @@ void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &
void resetScrollState(); void resetScrollState();
} // namespace MessageRenderer } // namespace MessageRenderer
} // namespace graphics } // namespace graphics

View File

@ -75,18 +75,16 @@ CannedMessageModule::CannedMessageModule()
void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel)
{ {
// Use the requested destination, unless it's "broadcast" and we have a previous node/channel // 🚫 Do NOT override explicit broadcast replies
if (newDest == NODENUM_BROADCAST && lastDestSet) { // Only reuse lastDest in LaunchRepeatDestination()
newDest = lastDest;
newChannel = lastChannel;
}
dest = newDest; dest = newDest;
channel = newChannel; channel = newChannel;
lastDest = dest; lastDest = dest;
lastChannel = channel; lastChannel = channel;
lastDestSet = true; lastDestSet = true;
// Rest of function unchanged...
// Upon activation, highlight "[Select Destination]" // Upon activation, highlight "[Select Destination]"
int selectDestination = 0; int selectDestination = 0;
for (int i = 0; i < messagesCount; ++i) { for (int i = 0; i < messagesCount; ++i) {
@ -103,6 +101,8 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan
UIFrameEvent e; UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e); notifyObservers(&e);
LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel);
} }
void CannedMessageModule::LaunchRepeatDestination() void CannedMessageModule::LaunchRepeatDestination()
@ -116,13 +116,12 @@ void CannedMessageModule::LaunchRepeatDestination()
void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel)
{ {
// Use the requested destination, unless it's "broadcast" and we have a previous node/channel // 🚫 Do NOT override explicit broadcast replies
if (newDest == NODENUM_BROADCAST && lastDestSet) { // Only reuse lastDest in LaunchRepeatDestination()
newDest = lastDest;
newChannel = lastChannel;
}
dest = newDest; dest = newDest;
channel = newChannel; channel = newChannel;
lastDest = dest; lastDest = dest;
lastChannel = channel; lastChannel = channel;
lastDestSet = true; lastDestSet = true;
@ -132,6 +131,8 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t
UIFrameEvent e; UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e); notifyObservers(&e);
LOG_DEBUG("[CannedMessage] LaunchFreetextWithDestination dest=0x%08x ch=%d", dest, channel);
} }
static bool returnToCannedList = false; static bool returnToCannedList = false;