feat(chat): per-conversation delete (revive + block); memoize chat/badge render
Adds a per-conversation "delete" with two modes, and removes the per-frame rescans of the chat history that shared these files. Delete conversation (header trash icon → confirm dialog): - Delete (revive-on-new-message): clears local history and tombstones the messages (new chat_deleted table, keyed dedup hashes) so the every-few- seconds memo re-scan can't re-import them; a genuinely NEW message (new txid) revives the thread. - Delete & block: removes history WITHOUT a tombstone and records the cid as blocked (settings); ChatService::ingest drops that conversation's messages — old and future — until unblocked from the "Blocked" manager, which then re-imports the conversation from chain. - Local-only (messages remain on-chain; the peer keeps their copy). deleteConversation() deletes the DB rows FIRST and only then mutates the store, so a failed write can't leave the two diverged. Unit-tested (revive / tombstone-survives-reload / block / unblock) and adversarially reviewed (store/DB divergence, half-open DB, revive-unread). Performance (chat + badge hot paths, from the perf audit): - ChatStore gains revision(); the Chat unread badge is now a single O(N) no-alloc pass cached on it (was O(conversations x messages) copy+sort every frame). The conversation list and open thread are memoized on revision() (+ show-hidden and AddressBook::revision() for peer-name resolution). - AddressBook gains revision() so an in-place contact rename invalidates the chat memo (an edit keeps entries().size() constant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,13 @@ bool Settings::load(const std::string& path)
|
||||
for (const auto& c : j["hidden_chat_cids"])
|
||||
if (c.is_string()) hidden_chat_cids_.push_back(c.get<std::string>());
|
||||
}
|
||||
if (j.contains("blocked_chat_convs") && j["blocked_chat_convs"].is_array()) {
|
||||
blocked_chat_convs_.clear();
|
||||
for (const auto& c : j["blocked_chat_convs"])
|
||||
if (c.is_object() && c.contains("cid") && c["cid"].is_string())
|
||||
blocked_chat_convs_.push_back({c["cid"].get<std::string>(),
|
||||
c.value("name", std::string())});
|
||||
}
|
||||
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
||||
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
|
||||
loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
|
||||
@@ -461,6 +468,13 @@ bool Settings::save(const std::string& path)
|
||||
j["hidden_chat_cids"] = json::array();
|
||||
for (const auto& c : hidden_chat_cids_)
|
||||
j["hidden_chat_cids"].push_back(c);
|
||||
j["blocked_chat_convs"] = json::array();
|
||||
for (const auto& b : blocked_chat_convs_) {
|
||||
json o;
|
||||
o["cid"] = b.cid;
|
||||
o["name"] = b.name;
|
||||
j["blocked_chat_convs"].push_back(o);
|
||||
}
|
||||
j["chat_emoji_color"] = chat_emoji_color_;
|
||||
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
|
||||
j["chat_bubble_style"] = chat_bubble_style_;
|
||||
|
||||
@@ -144,6 +144,26 @@ public:
|
||||
hidden_chat_cids_.end());
|
||||
}
|
||||
|
||||
// Blocked chat conversations (by cid). Unlike hide (reversible, keeps messages), block DELETES the
|
||||
// local history AND suppresses every message for the cid — old and future — until you unblock, at
|
||||
// which point the conversation re-imports from the chain. The last-known peer name is kept so the
|
||||
// "Blocked" list can label the entry (its messages are gone from the local store).
|
||||
struct BlockedChatConv { std::string cid; std::string name; };
|
||||
bool isChatBlocked(const std::string& cid) const {
|
||||
for (const auto& b : blocked_chat_convs_) if (b.cid == cid) return true;
|
||||
return false;
|
||||
}
|
||||
void setChatBlocked(const std::string& cid, const std::string& name, bool blocked) {
|
||||
const bool already = isChatBlocked(cid);
|
||||
if (blocked && !already) blocked_chat_convs_.push_back({cid, name});
|
||||
else if (!blocked && already)
|
||||
blocked_chat_convs_.erase(
|
||||
std::remove_if(blocked_chat_convs_.begin(), blocked_chat_convs_.end(),
|
||||
[&](const BlockedChatConv& b) { return b.cid == cid; }),
|
||||
blocked_chat_convs_.end());
|
||||
}
|
||||
const std::vector<BlockedChatConv>& blockedChatConversations() const { return blocked_chat_convs_; }
|
||||
|
||||
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
|
||||
bool getChatEmojiColor() const { return chat_emoji_color_; }
|
||||
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
|
||||
@@ -559,6 +579,7 @@ private:
|
||||
std::string chat_reply_zaddr_;
|
||||
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
||||
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
|
||||
std::vector<BlockedChatConv> blocked_chat_convs_; // blocked chat conversations (cid + last-known name)
|
||||
// Chat-tab customization (chat settings modal + Settings → Chat & Contacts).
|
||||
bool chat_emoji_color_ = true; // true = color (needs FreeType; falls back to mono if absent), false = monochrome
|
||||
float chat_poll_rate_sec_ = 2.5f; // 0-conf chat fast-scan cadence (full node)
|
||||
|
||||
Reference in New Issue
Block a user