A z_sendmany submit set off a burst of wallet scans before anything had changed. Ops now re-read the wallet when they finish, by who started them: the user's sends and dialog ops get a re-read plus one forced balance once none of their ops is pending; client auto-shield re-reads unforced; chat messages and splits refresh only the chat note view. A pending send no longer forces scans, and chat txids no longer enter the tracked-send set. Chat ingest skips stored messages before decrypting and caches undecryptable ones per identity. The inbox scan runs when getwalletinfo's txcount changed (a cheap probe, at most 2% duty) or a cost-scaled safety net is due; the note-buffer scan runs on need. Both are admitted by the scan budget, on a steady clock. The chat payer comes from a per-address view of verified notes, with debits re-applied across in-flight scans; splits are sized to one payer and only when they can refill the buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.5 KiB
C++
79 lines
3.5 KiB
C++
#pragma once
|
|
|
|
// DragonX Wallet - HushChat in-memory message store: the fast read model / dedup view. Durable
|
|
// persistence lives in ChatDatabase; ChatService rehydrates this store from it on unlock.
|
|
|
|
#include "chat_message.h"
|
|
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
// Threads messages by conversation_id (cid) and deduplicates by (txid, payload_position) so
|
|
// re-scanning the chain never double-inserts. Not thread-safe — drive from the main thread.
|
|
class ChatStore {
|
|
public:
|
|
// Returns true if newly inserted, false if a duplicate was ignored.
|
|
bool append(const ChatMessage& message);
|
|
|
|
// The dedup key of a message: "<txid>:<payload_position>".
|
|
static std::string messageKey(const std::string& txid, std::size_t payloadPosition);
|
|
// A message with this (txid, position) is in the store (append would refuse it).
|
|
bool contains(const std::string& txid, std::size_t payloadPosition) const {
|
|
return seen_.count(messageKey(txid, payloadPosition)) != 0;
|
|
}
|
|
|
|
// Messages in a conversation, in insertion order.
|
|
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
|
|
|
|
// Update an outgoing echo's delivery status by its local txid. Returns a pointer to the updated
|
|
// message (for the caller to persist), or nullptr if no message has that txid.
|
|
const ChatMessage* updateDelivery(const std::string& txid, ChatDelivery delivery);
|
|
|
|
// Distinct conversation ids, in first-seen order.
|
|
std::vector<std::string> conversationIds() const;
|
|
|
|
// The set of on-chain txids that carried a chat message (sent or received, messages + contact
|
|
// requests) — used by the History tab to badge / filter chat transactions. O(messages), no copies.
|
|
std::unordered_set<std::string> chatTxids() const {
|
|
std::unordered_set<std::string> out;
|
|
out.reserve(messages_.size());
|
|
for (const auto& m : messages_)
|
|
if (!m.txid.empty()) out.insert(m.txid);
|
|
return out;
|
|
}
|
|
|
|
// Remove every message in a conversation from the in-memory view and return the removed messages
|
|
// (so the caller can delete/tombstone their persisted rows). Re-appends are prevented by the ingest
|
|
// guard (blocked-cid predicate / DB tombstone), not here.
|
|
std::vector<ChatMessage> eraseConversation(const std::string& conversationId);
|
|
|
|
std::size_t size() const { return messages_.size(); }
|
|
bool empty() const { return messages_.empty(); }
|
|
void clear();
|
|
|
|
// Monotonic counter bumped on every mutation (append / updateDelivery change / eraseConversation /
|
|
// clear). Callers memoize expensive per-frame reads (conversation-list build, unread count) against it
|
|
// so they only rebuild when the store actually changed.
|
|
std::uint64_t revision() const { return revision_; }
|
|
|
|
// Count unread incoming messages in a SINGLE pass over the store: an incoming message counts when its
|
|
// cid is not `excluded` and its timestamp is newer than `seenFor(cid)`. Avoids the per-conversation
|
|
// copy+sort that conversation() does — the count doesn't need ordering.
|
|
int countUnread(const std::function<bool(const std::string&)>& excluded,
|
|
const std::function<std::int64_t(const std::string&)>& seenFor) const;
|
|
|
|
private:
|
|
static std::string dedupKey(const ChatMessage& message);
|
|
|
|
std::vector<ChatMessage> messages_;
|
|
std::unordered_set<std::string> seen_;
|
|
std::uint64_t revision_ = 0;
|
|
};
|
|
|
|
} // namespace dragonx::chat
|