From f3e399e0ffebb1c2badb9ca59e56e216fe4ba445 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 11:23:01 -0700 Subject: [PATCH] feat: show unread message count on chat tabs Each non-General chat tab now shows an unread count in parentheses (e.g. "Whispers (3)") when messages arrive while that tab is inactive. The counter clears when the tab is selected. The General tab is excluded since it shows all messages anyway. --- include/ui/game_screen.hpp | 2 ++ src/ui/game_screen.cpp | 39 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 947f4b4a..cd102fa9 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -70,6 +70,8 @@ private: uint64_t typeMask; // bitmask of ChatType values to show (64-bit: types go up to 84) }; std::vector chatTabs_; + std::vector chatTabUnread_; // unread message count per tab (0 = none) + size_t chatTabSeenCount_ = 0; // how many history messages have been processed void initChatTabs(); bool shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 7a3bba73..b54eb321 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -228,6 +228,9 @@ void GameScreen::initChatTabs() { (1ULL << static_cast(game::ChatType::GUILD_ACHIEVEMENT))}); // Trade/LFG tab: channel messages chatTabs_.push_back({"Trade/LFG", (1ULL << static_cast(game::ChatType::CHANNEL))}); + // Reset unread counts to match new tab list + chatTabUnread_.assign(chatTabs_.size(), 0); + chatTabSeenCount_ = 0; } bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const { @@ -1107,11 +1110,43 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { chatWindowPos_ = ImGui::GetWindowPos(); } + // Update unread counts: scan any new messages since last frame + { + const auto& history = gameHandler.getChatHistory(); + // Ensure unread array is sized correctly (guards against late init) + if (chatTabUnread_.size() != chatTabs_.size()) + chatTabUnread_.assign(chatTabs_.size(), 0); + // If history shrank (e.g. cleared), reset + if (chatTabSeenCount_ > history.size()) chatTabSeenCount_ = 0; + for (size_t mi = chatTabSeenCount_; mi < history.size(); ++mi) { + const auto& msg = history[mi]; + // For each non-General (non-0) tab that isn't currently active, check visibility + for (int ti = 1; ti < static_cast(chatTabs_.size()); ++ti) { + if (ti == activeChatTab_) continue; + if (shouldShowMessage(msg, ti)) { + chatTabUnread_[ti]++; + } + } + } + chatTabSeenCount_ = history.size(); + } + // Chat tabs if (ImGui::BeginTabBar("ChatTabs")) { for (int i = 0; i < static_cast(chatTabs_.size()); ++i) { - if (ImGui::BeginTabItem(chatTabs_[i].name.c_str())) { - activeChatTab_ = i; + // Build label with unread count suffix for non-General tabs + std::string tabLabel = chatTabs_[i].name; + if (i > 0 && i < static_cast(chatTabUnread_.size()) && chatTabUnread_[i] > 0) { + tabLabel += " (" + std::to_string(chatTabUnread_[i]) + ")"; + } + // Use ImGuiTabItemFlags_NoPushId so label changes don't break tab identity + if (ImGui::BeginTabItem(tabLabel.c_str())) { + if (activeChatTab_ != i) { + activeChatTab_ = i; + // Clear unread count when tab becomes active + if (i < static_cast(chatTabUnread_.size())) + chatTabUnread_[i] = 0; + } ImGui::EndTabItem(); } }