Three next-batch UX-audit fixes:
- Chat delivery: the sending/sent glyph has higher contrast and a tooltip
("Sent to the network — arrives after it confirms", so a check isn't mistaken
for "delivered"); a failed send now carries the real reason (ChatMessage::
fail_reason, set from onChatBroadcastResult via friendlyRpcError, in-memory
only) in the "not sent" hover tooltip.
- Explorer: a block height beyond the tip says so ("Block N is beyond the chain
tip (M)") instead of silently snapping to the top; the empty block list shows
Not connected / Syncing (N behind) / Loading instead of an endless "Loading".
- Settings: the misleading "Reset to Defaults" button (which only reloads the
last-saved values) is relabeled "Discard unsaved changes" with accurate copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
4.4 KiB
C++
122 lines
4.4 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,
|
|
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<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
|