feat(chat): service-level sensitive gating + local sensitive marks

Wires the sensitive-conversation plumbing into ChatService + Settings:
- Settings gains a persisted local sensitive set (isChatSensitive /
  setChatSensitive / sensitiveChatConversations), mirroring the blocked set, for
  INSTANT effect the moment the user marks a conversation (before the on-chain
  signal is harvested back).
- ChatService::isConversationSensitive() OR-combines that local mark with the
  on-chain reduction, and setPersistence() wires it as the DB's per-row key
  selector (sensitive rows -> vault key).
- ingest() withholds a sensitive conversation's messages while the vault key is
  absent (fail-closed: not shown, not persisted); they re-import + persist under
  the vault key once it is unlocked and a scan runs. loadFromDatabase() already
  withholds via the DB's two-key try.
Service-tested end to end against a real ChatDatabase (ingest + reload).

Phase B, step 4 (service gating). App vault lifecycle + UI next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 17:42:31 -05:00
co-authored by Claude Opus 4.8
parent 6b9bda3ee4
commit fe3d2aa935
5 changed files with 124 additions and 1 deletions
+15
View File
@@ -180,6 +180,14 @@ bool Settings::load(const std::string& path)
(c.contains("name") && c["name"].is_string())
? c["name"].get<std::string>() : std::string()});
}
if (j.contains("sensitive_chat_convs") && j["sensitive_chat_convs"].is_array()) {
sensitive_chat_convs_.clear();
for (const auto& c : j["sensitive_chat_convs"])
if (c.is_object() && c.contains("cid") && c["cid"].is_string())
sensitive_chat_convs_.push_back({c["cid"].get<std::string>(),
(c.contains("name") && c["name"].is_string())
? c["name"].get<std::string>() : 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_);
@@ -544,6 +552,13 @@ bool Settings::save(const std::string& path)
o["name"] = b.name;
j["blocked_chat_convs"].push_back(o);
}
j["sensitive_chat_convs"] = json::array();
for (const auto& s : sensitive_chat_convs_) {
json o;
o["cid"] = s.cid;
o["name"] = s.name;
j["sensitive_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_;
+21
View File
@@ -182,6 +182,26 @@ public:
}
const std::vector<BlockedChatConv>& blockedChatConversations() const { return blocked_chat_convs_; }
// Sensitive chat conversations (by cid): their local rows are stored under the local chat vault key
// (not the seed key) and are withheld without it. Kept locally for INSTANT effect the moment the user
// marks one; the mark is also signaled on-chain so other devices / a fresh seed import gate it too.
// The last-known peer name labels the sensitive-conversations list.
struct SensitiveChatConv { std::string cid; std::string name; };
bool isChatSensitive(const std::string& cid) const {
for (const auto& s : sensitive_chat_convs_) if (s.cid == cid) return true;
return false;
}
void setChatSensitive(const std::string& cid, const std::string& name, bool sensitive) {
const bool already = isChatSensitive(cid);
if (sensitive && !already) sensitive_chat_convs_.push_back({cid, name});
else if (!sensitive && already)
sensitive_chat_convs_.erase(
std::remove_if(sensitive_chat_convs_.begin(), sensitive_chat_convs_.end(),
[&](const SensitiveChatConv& s) { return s.cid == cid; }),
sensitive_chat_convs_.end());
}
const std::vector<SensitiveChatConv>& sensitiveChatConversations() const { return sensitive_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; }
@@ -694,6 +714,7 @@ private:
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)
std::vector<SensitiveChatConv> sensitive_chat_convs_; // sensitive 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)