Chat sends move 0 value, so the network fee is structurally load-bearing (it's the only thing that forces a real shielded input; 0-value + 0-fee builds a degenerate, unrelayable tx). Three gaps addressed: 1. Fee floor — broadcastChatMemos now uses max(getDefaultFee(), kChatMinFeeDrgx), so a 0 / too-low global default-fee setting can't silently break chat. 2. Real delivery status — the echo was marked Sent on SUBMIT regardless of the on-chain outcome (the z_sendmany callback was empty), so failures were invisible and the Retry affordance never fired for async failures. Add a third ChatDelivery::Sending state (appended so persisted 0=Sent stays valid); record the echo in-memory as Sending, and resolve it to Sent/Failed from the z_sendmany completion callback — persisting only the final status (so a restart never shows a stuck spinner; a stray persisted Sending loads as Sent). A subtle "sending…" label shows while in flight. 3. Pay-from-funded + pre-check — z_sendmany spends from one z-address, and the identity reply address may be unfunded while funds sit elsewhere. chatPayFromZaddr picks a spendable z-address that can cover the fee (preferring the identity address); the memo still advertises the identity address as reply-to, so paying from a different note is transport-transparent. If nothing can cover the fee, a clear "need a small shielded balance" toast replaces the cryptic failure. Full node only for the callback path; lite resolves optimistically on queue. 8-language strings + CJK subset (+1 glyph 賄). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.1 KiB
C++
62 lines
2.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);
|
|
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;
|
|
return &message;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void ChatStore::clear() {
|
|
messages_.clear();
|
|
seen_.clear();
|
|
}
|
|
|
|
} // namespace dragonx::chat
|