diff --git a/src/app_network.cpp b/src/app_network.cpp index a7c9393..4e2028b 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1233,6 +1233,7 @@ int App::chatUnreadCount() const int unread = 0; const auto& store = chat_service_.store(); for (const auto& cid : store.conversationIds()) { + if (settings_ && settings_->isChatMuted(cid)) continue; // muted conversations don't badge (Q10) std::int64_t seen = 0; const auto it = chat_seen_watermark_.find(cid); if (it != chat_seen_watermark_.end()) seen = it->second; @@ -1688,9 +1689,12 @@ void App::refreshTransactionData() !result.hushChatMetadata.empty()) { std::unordered_map chatTxTimes; for (const auto& tx : result.transactions) chatTxTimes[tx.txid] = tx.timestamp; + const int unreadBefore = chatUnreadCount(); const int newMsgs = chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr)); - if (newMsgs > 0 && current_page_ != ui::NavPage::Chat) - ui::Notifications::instance().info(TR("chat_new_message_toast")); // Q4: alert when not on the Chat tab + // Toast only when a non-muted conversation gained an unread message — chatUnreadCount() + // already skips muted cids, so a delta cleanly respects mute (Q10 + Q4). + if (newMsgs > 0 && current_page_ != ui::NavPage::Chat && chatUnreadCount() > unreadBefore) + ui::Notifications::instance().info(TR("chat_new_message_toast")); } NetworkRefreshService::applyTransactionRefreshResult( state_, cacheUpdate, std::move(result), std::time(nullptr)); @@ -1748,9 +1752,12 @@ void App::refreshRecentTransactionData() !result.hushChatMetadata.empty()) { std::unordered_map chatTxTimes; for (const auto& tx : result.transactions) chatTxTimes[tx.txid] = tx.timestamp; + const int unreadBefore = chatUnreadCount(); const int newMsgs = chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr)); - if (newMsgs > 0 && current_page_ != ui::NavPage::Chat) - ui::Notifications::instance().info(TR("chat_new_message_toast")); // Q4: alert when not on the Chat tab + // Toast only when a non-muted conversation gained an unread message — chatUnreadCount() + // already skips muted cids, so a delta cleanly respects mute (Q10 + Q4). + if (newMsgs > 0 && current_page_ != ui::NavPage::Chat && chatUnreadCount() > unreadBefore) + ui::Notifications::instance().info(TR("chat_new_message_toast")); } NetworkRefreshService::applyTransactionRefreshResult( state_, cacheUpdate, std::move(result), std::time(nullptr)); diff --git a/src/config/settings.cpp b/src/config/settings.cpp index fcd1b57..6e82421 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -147,6 +147,11 @@ bool Settings::load(const std::string& path) loadScalar(j, "language", language_); loadScalar(j, "skin_id", skin_id_); loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_); + if (j.contains("muted_chat_cids") && j["muted_chat_cids"].is_array()) { + muted_chat_cids_.clear(); + for (const auto& c : j["muted_chat_cids"]) + if (c.is_string()) muted_chat_cids_.push_back(c.get()); + } loadScalar(j, "acrylic_enabled", acrylic_enabled_); loadScalar(j, "acrylic_quality", acrylic_quality_); loadScalar(j, "blur_multiplier", blur_multiplier_); @@ -422,6 +427,9 @@ bool Settings::save(const std::string& path) j["language"] = language_; j["skin_id"] = skin_id_; j["chat_reply_zaddr"] = chat_reply_zaddr_; + j["muted_chat_cids"] = json::array(); + for (const auto& c : muted_chat_cids_) + j["muted_chat_cids"].push_back(c); j["acrylic_enabled"] = acrylic_enabled_; j["acrylic_quality"] = acrylic_quality_; j["blur_multiplier"] = blur_multiplier_; diff --git a/src/config/settings.h b/src/config/settings.h index 67c32b8..d698df7 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -118,6 +118,18 @@ public: std::string getChatReplyZaddr() const { return chat_reply_zaddr_; } void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; } + // Muted chat conversations (by cid) — muted conversations don't badge or raise a toast (Q10). + bool isChatMuted(const std::string& cid) const { + return std::find(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid) != muted_chat_cids_.end(); + } + void setChatMuted(const std::string& cid, bool muted) { + const bool already = isChatMuted(cid); + if (muted && !already) muted_chat_cids_.push_back(cid); + else if (!muted && already) + muted_chat_cids_.erase(std::remove(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid), + muted_chat_cids_.end()); + } + // Privacy bool getSaveZtxs() const { return save_ztxs_; } void setSaveZtxs(bool save) { save_ztxs_ = save; } @@ -473,6 +485,7 @@ private: std::string theme_ = "dragonx"; std::string skin_id_ = "dragonx"; std::string chat_reply_zaddr_; + std::vector muted_chat_cids_; // muted chat conversations by cid (Q10) bool save_ztxs_ = true; bool auto_shield_ = true; bool use_tor_ = false; diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index a4def1b..61ccb1e 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -11,6 +11,7 @@ #include "../../chat/chat_service.h" #include "../../util/i18n.h" #include "../../util/platform.h" // getConfigDir + writeFileAtomically — conversation export (Q11) +#include "../../config/settings.h" // per-conversation mute (Q10) #include "../material/colors.h" #include "../material/color_theme.h" // WithAlpha #include "../material/type.h" @@ -431,6 +432,13 @@ void RenderChatTab(App* app) else Notifications::instance().error(TR("chat_export_failed")); } + // Mute toggle — muted conversations don't badge or toast (Q10). Persisted to settings. + ImGui::SameLine(); + const bool muted = app->settings() && app->settings()->isChatMuted(sel->cid); + if (ImGui::SmallButton(muted ? TR("chat_unmute") : TR("chat_mute")) && app->settings()) { + app->settings()->setChatMuted(sel->cid, !muted); + app->settings()->save(); + } } ImGui::Separator();