Files
ObsidianDragon/src/chat/chat_store.cpp
T
DanSandClaude Opus 4.8 0a042df8e0 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>
2026-09-01 22:54:51 -05:00

93 lines
3.1 KiB
C++

// DragonX Wallet - HushChat in-memory message store (implementation).
#include "chat_store.h"
#include <algorithm>
namespace dragonx::chat {
std::string ChatStore::dedupKey(const ChatMessage& message) {
return message.txid + ":" + std::to_string(message.payload_position);
}
bool ChatStore::append(const ChatMessage& message) {
if (!seen_.insert(dedupKey(message)).second) return false;
messages_.push_back(message);
++revision_;
return true;
}
std::vector<ChatMessage> ChatStore::conversation(const std::string& conversationId) const {
std::vector<ChatMessage> out;
for (const auto& message : messages_) {
if (message.conversation_id == conversationId) out.push_back(message);
}
// Return chronological (oldest→newest). messages_ is in scan/insertion order, which is NOT
// time-ordered (a full scan harvests txids in std::set/unordered_map order) — that would mis-order the
// rendered thread AND let the reply-target pin (first-seen peer address) latch onto a non-establishing
// message (B2). timestamp is the block/tx time (peer can't set it); tie-break txid + payload_position
// for determinism.
std::stable_sort(out.begin(), out.end(), [](const ChatMessage& a, const ChatMessage& b) {
if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp;
if (a.txid != b.txid) return a.txid < b.txid;
return a.payload_position < b.payload_position;
});
return out;
}
const ChatMessage* ChatStore::updateDelivery(const std::string& txid, ChatDelivery delivery) {
for (auto& message : messages_) {
if (message.txid == txid) {
message.delivery = delivery;
++revision_;
return &message;
}
}
return nullptr;
}
int ChatStore::countUnread(const std::function<bool(const std::string&)>& excluded,
const std::function<std::int64_t(const std::string&)>& seenFor) const {
int unread = 0;
for (const auto& m : messages_) {
if (m.direction != ChatDirection::Incoming) continue;
if (excluded && excluded(m.conversation_id)) continue;
if (m.timestamp > seenFor(m.conversation_id)) ++unread;
}
return unread;
}
std::vector<std::string> ChatStore::conversationIds() const {
std::vector<std::string> ids;
std::unordered_set<std::string> seenIds;
for (const auto& message : messages_) {
if (seenIds.insert(message.conversation_id).second) ids.push_back(message.conversation_id);
}
return ids;
}
std::vector<ChatMessage> ChatStore::eraseConversation(const std::string& conversationId) {
std::vector<ChatMessage> removed;
std::vector<ChatMessage> kept;
kept.reserve(messages_.size());
for (auto& m : messages_) {
if (m.conversation_id == conversationId) {
seen_.erase(dedupKey(m));
removed.push_back(std::move(m));
} else {
kept.push_back(std::move(m));
}
}
messages_.swap(kept);
if (!removed.empty()) ++revision_;
return removed;
}
void ChatStore::clear() {
messages_.clear();
seen_.clear();
++revision_;
}
} // namespace dragonx::chat