// DragonX Wallet - HushChat in-memory message store (implementation). #include "chat_store.h" #include 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 ChatStore::latestTimestampByConversation() const { std::unordered_map out; for (const auto& message : messages_) { auto it = out.find(message.conversation_id); if (it == out.end()) out.emplace(message.conversation_id, static_cast(message.timestamp)); else if (static_cast(message.timestamp) > it->second) it->second = static_cast(message.timestamp); } return out; } std::vector ChatStore::conversation(const std::string& conversationId) const { std::vector 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, const std::string& reason) { for (auto& message : messages_) { if (message.txid == txid) { message.delivery = delivery; message.fail_reason = (delivery == ChatDelivery::Failed) ? reason : std::string(); ++revision_; return &message; } } return nullptr; } int ChatStore::countUnread(const std::function& excluded, const std::function& 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 ChatStore::conversationIds() const { std::vector ids; std::unordered_set seenIds; for (const auto& message : messages_) { if (seenIds.insert(message.conversation_id).second) ids.push_back(message.conversation_id); } return ids; } std::vector ChatStore::eraseConversation(const std::string& conversationId) { std::vector removed; std::vector 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