feat(chat): mute conversations (Q10)
Per-conversation mute toggle in the thread header. Muted conversations (tracked by cid in settings, so it persists) are skipped by chatUnreadCount(), so they neither raise the nav-item unread badge nor the new-message toast — the toast now gates on a chatUnreadCount() delta across ingest, which already skips muted cids, so mute is respected for free. "Block" (rejecting a peer's inbound memos) is a larger ingest-filter change and is intentionally left out of this pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<std::string, std::int64_t> 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<std::string, std::int64_t> 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));
|
||||
|
||||
@@ -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<std::string>());
|
||||
}
|
||||
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_;
|
||||
|
||||
@@ -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<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
||||
bool save_ztxs_ = true;
|
||||
bool auto_shield_ = true;
|
||||
bool use_tor_ = false;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user