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:
+27
-10
@@ -1458,18 +1458,26 @@ void App::processWalletSwitchRevert()
|
||||
int App::chatUnreadCount() const
|
||||
{
|
||||
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return 0;
|
||||
int unread = 0;
|
||||
// This is called every frame from render() to size the Chat nav badge. Recompute only when the store
|
||||
// actually changed (new/removed messages bump revision()) or after a short interval (to pick up
|
||||
// mute/hide/seen changes, which don't bump the store). Previously it ran a full O(conversations x
|
||||
// messages) scan that COPIED and stable_sorted every conversation's messages, every frame.
|
||||
const auto& store = chat_service_.store();
|
||||
for (const auto& cid : store.conversationIds()) {
|
||||
if (settings_ && settings_->isChatMuted(cid)) continue; // muted conversations don't badge (Q10)
|
||||
if (settings_ && settings_->isChatHidden(cid)) continue; // hidden conversations don't badge
|
||||
std::int64_t seen = 0;
|
||||
const auto it = chat_seen_watermark_.find(cid);
|
||||
if (it != chat_seen_watermark_.end()) seen = it->second;
|
||||
for (const auto& m : store.conversation(cid))
|
||||
if (m.direction == chat::ChatDirection::Incoming && m.timestamp > seen) ++unread;
|
||||
const std::uint64_t rev = store.revision();
|
||||
const double now = ImGui::GetTime();
|
||||
if (rev != chat_unread_rev_ || now - chat_unread_computed_at_ > 0.25) {
|
||||
chat_unread_cached_ = store.countUnread(
|
||||
[this](const std::string& cid) { // excluded from the badge
|
||||
return settings_ && (settings_->isChatMuted(cid) || settings_->isChatHidden(cid));
|
||||
},
|
||||
[this](const std::string& cid) -> std::int64_t { // seen watermark
|
||||
const auto it = chat_seen_watermark_.find(cid);
|
||||
return it != chat_seen_watermark_.end() ? it->second : 0;
|
||||
});
|
||||
chat_unread_rev_ = rev;
|
||||
chat_unread_computed_at_ = now;
|
||||
}
|
||||
return unread;
|
||||
return chat_unread_cached_;
|
||||
}
|
||||
|
||||
void App::markChatConversationSeen(const std::string& cid, std::int64_t latestTs)
|
||||
@@ -1477,6 +1485,11 @@ void App::markChatConversationSeen(const std::string& cid, std::int64_t latestTs
|
||||
if (latestTs > 0) chat_seen_watermark_[cid] = latestTs;
|
||||
}
|
||||
|
||||
void App::forgetChatConversationSeen(const std::string& cid)
|
||||
{
|
||||
chat_seen_watermark_.erase(cid);
|
||||
}
|
||||
|
||||
void App::wipePendingTransactionHistoryCachePassphrase()
|
||||
{
|
||||
if (!pending_transaction_history_cache_passphrase_.empty()) {
|
||||
@@ -3041,6 +3054,10 @@ void App::provisionChatIdentityFromSecret(std::string secret)
|
||||
// Persistence: unlock the seed-derived chat DB with the SAME secret and rehydrate the store
|
||||
// with prior messages (decrypted at rest under a key only this seed can derive).
|
||||
chat_service_.setPersistence(&chat_db_);
|
||||
// A blocked conversation's messages are dropped at ingest (old + future) until it is unblocked.
|
||||
chat_service_.setBlockedPredicate([this](const std::string& cid) {
|
||||
return settings_ && settings_->isChatBlocked(cid);
|
||||
});
|
||||
if (chat_db_.unlockWithSecret(trimmed)) {
|
||||
chat_service_.loadFromDatabase();
|
||||
// Baseline unread: treat everything already in the store at load as read, so only messages
|
||||
|
||||
Reference in New Issue
Block a user