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;
|
int unread = 0;
|
||||||
const auto& store = chat_service_.store();
|
const auto& store = chat_service_.store();
|
||||||
for (const auto& cid : store.conversationIds()) {
|
for (const auto& cid : store.conversationIds()) {
|
||||||
|
if (settings_ && settings_->isChatMuted(cid)) continue; // muted conversations don't badge (Q10)
|
||||||
std::int64_t seen = 0;
|
std::int64_t seen = 0;
|
||||||
const auto it = chat_seen_watermark_.find(cid);
|
const auto it = chat_seen_watermark_.find(cid);
|
||||||
if (it != chat_seen_watermark_.end()) seen = it->second;
|
if (it != chat_seen_watermark_.end()) seen = it->second;
|
||||||
@@ -1688,9 +1689,12 @@ void App::refreshTransactionData()
|
|||||||
!result.hushChatMetadata.empty()) {
|
!result.hushChatMetadata.empty()) {
|
||||||
std::unordered_map<std::string, std::int64_t> chatTxTimes;
|
std::unordered_map<std::string, std::int64_t> chatTxTimes;
|
||||||
for (const auto& tx : result.transactions) chatTxTimes[tx.txid] = tx.timestamp;
|
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));
|
const int newMsgs = chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr));
|
||||||
if (newMsgs > 0 && current_page_ != ui::NavPage::Chat)
|
// Toast only when a non-muted conversation gained an unread message — chatUnreadCount()
|
||||||
ui::Notifications::instance().info(TR("chat_new_message_toast")); // Q4: alert when not on the Chat tab
|
// 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(
|
NetworkRefreshService::applyTransactionRefreshResult(
|
||||||
state_, cacheUpdate, std::move(result), std::time(nullptr));
|
state_, cacheUpdate, std::move(result), std::time(nullptr));
|
||||||
@@ -1748,9 +1752,12 @@ void App::refreshRecentTransactionData()
|
|||||||
!result.hushChatMetadata.empty()) {
|
!result.hushChatMetadata.empty()) {
|
||||||
std::unordered_map<std::string, std::int64_t> chatTxTimes;
|
std::unordered_map<std::string, std::int64_t> chatTxTimes;
|
||||||
for (const auto& tx : result.transactions) chatTxTimes[tx.txid] = tx.timestamp;
|
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));
|
const int newMsgs = chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr));
|
||||||
if (newMsgs > 0 && current_page_ != ui::NavPage::Chat)
|
// Toast only when a non-muted conversation gained an unread message — chatUnreadCount()
|
||||||
ui::Notifications::instance().info(TR("chat_new_message_toast")); // Q4: alert when not on the Chat tab
|
// 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(
|
NetworkRefreshService::applyTransactionRefreshResult(
|
||||||
state_, cacheUpdate, std::move(result), std::time(nullptr));
|
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, "language", language_);
|
||||||
loadScalar(j, "skin_id", skin_id_);
|
loadScalar(j, "skin_id", skin_id_);
|
||||||
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
|
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_enabled", acrylic_enabled_);
|
||||||
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
||||||
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
||||||
@@ -422,6 +427,9 @@ bool Settings::save(const std::string& path)
|
|||||||
j["language"] = language_;
|
j["language"] = language_;
|
||||||
j["skin_id"] = skin_id_;
|
j["skin_id"] = skin_id_;
|
||||||
j["chat_reply_zaddr"] = chat_reply_zaddr_;
|
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_enabled"] = acrylic_enabled_;
|
||||||
j["acrylic_quality"] = acrylic_quality_;
|
j["acrylic_quality"] = acrylic_quality_;
|
||||||
j["blur_multiplier"] = blur_multiplier_;
|
j["blur_multiplier"] = blur_multiplier_;
|
||||||
|
|||||||
@@ -118,6 +118,18 @@ public:
|
|||||||
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
|
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
|
||||||
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
|
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
|
// Privacy
|
||||||
bool getSaveZtxs() const { return save_ztxs_; }
|
bool getSaveZtxs() const { return save_ztxs_; }
|
||||||
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
||||||
@@ -473,6 +485,7 @@ private:
|
|||||||
std::string theme_ = "dragonx";
|
std::string theme_ = "dragonx";
|
||||||
std::string skin_id_ = "dragonx";
|
std::string skin_id_ = "dragonx";
|
||||||
std::string chat_reply_zaddr_;
|
std::string chat_reply_zaddr_;
|
||||||
|
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
||||||
bool save_ztxs_ = true;
|
bool save_ztxs_ = true;
|
||||||
bool auto_shield_ = true;
|
bool auto_shield_ = true;
|
||||||
bool use_tor_ = false;
|
bool use_tor_ = false;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "../../chat/chat_service.h"
|
#include "../../chat/chat_service.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/platform.h" // getConfigDir + writeFileAtomically — conversation export (Q11)
|
#include "../../util/platform.h" // getConfigDir + writeFileAtomically — conversation export (Q11)
|
||||||
|
#include "../../config/settings.h" // per-conversation mute (Q10)
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../material/color_theme.h" // WithAlpha
|
#include "../material/color_theme.h" // WithAlpha
|
||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
@@ -431,6 +432,13 @@ void RenderChatTab(App* app)
|
|||||||
else
|
else
|
||||||
Notifications::instance().error(TR("chat_export_failed"));
|
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();
|
ImGui::Separator();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user