Loading chat history did more work than it needed to. Seeding the unread watermark walked every conversation and copied each one's full message list out of the store just to read the last timestamp, and the database reload copied every message into the store one at a time with no reservation. - The watermark now comes from one pass over the stored messages that returns the latest timestamp per conversation, with no per- conversation copy or sort. - The store gained a move-append and a reserve, so the reload pre-sizes its vector and dedup set once and moves each row in instead of copying it. The dedup key is computed before the move. Behaviour is unchanged: the watermark values and the blocked-conversation filter on reload are the same as before. Tests cover the per-conversation latest-timestamp pass and move-append. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
4.2 KiB
C++
120 lines
4.2 KiB
C++
// DragonX Wallet - HushChat in-memory message store (implementation).
|
|
|
|
#include "chat_store.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
std::string ChatStore::messageKey(const std::string& txid, std::size_t payloadPosition) {
|
|
return txid + ":" + std::to_string(payloadPosition);
|
|
}
|
|
|
|
std::string ChatStore::dedupKey(const ChatMessage& message) {
|
|
return messageKey(message.txid, message.payload_position);
|
|
}
|
|
|
|
bool ChatStore::append(const ChatMessage& message) {
|
|
if (!seen_.insert(dedupKey(message)).second) return false;
|
|
messages_.push_back(message);
|
|
++revision_;
|
|
return true;
|
|
}
|
|
|
|
bool ChatStore::append(ChatMessage&& message) {
|
|
if (!seen_.insert(dedupKey(message)).second) return false; // reads txid/payload_position before the move
|
|
messages_.push_back(std::move(message));
|
|
++revision_;
|
|
return true;
|
|
}
|
|
|
|
void ChatStore::reserve(std::size_t n) {
|
|
messages_.reserve(n);
|
|
seen_.reserve(n);
|
|
}
|
|
|
|
std::unordered_map<std::string, std::int64_t> ChatStore::latestTimestampByConversation() const {
|
|
std::unordered_map<std::string, std::int64_t> out;
|
|
for (const auto& message : messages_) {
|
|
auto it = out.find(message.conversation_id);
|
|
if (it == out.end()) out.emplace(message.conversation_id, static_cast<std::int64_t>(message.timestamp));
|
|
else if (static_cast<std::int64_t>(message.timestamp) > it->second) it->second = static_cast<std::int64_t>(message.timestamp);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
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();
|
|
txid_index_.clear(); // drop the History-badge txid copies too (revision_ below invalidates the memo)
|
|
++revision_;
|
|
}
|
|
|
|
} // namespace dragonx::chat
|