feat(chat): mark outgoing messages that fail to send
Outgoing echoes were recorded optimistically regardless of whether the broadcast was actually submitted. Now broadcastChatMemos / broadcastChatMemosLite return whether the send was submitted (false on immediate failures: not connected, no spendable z-address, or a lite send already in progress), and the echo records a ChatDelivery status (Sent / Failed). The Chat tab shows a "not sent" marker in the error color on failed messages. The status persists in the DB (backward- compatible: old rows deserialize as Sent). (A later async failure — an opid that fails after submission — still surfaces via the existing send-progress notification; only the submit gate is reflected here.) Also removes the now-unused chat_readonly_note string (the composer replaced the read-only footer). Test: delivery status round-trips through the DB. Verified: Linux + Windows build with chat ON, ctest 100%, hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2534,23 +2534,22 @@ std::string App::generateChatLocalId(const char* prefix, int numBytes) const
|
||||
// LIVE-VERIFY (cannot be proven from source): that dragonxd returns the memo under `memoStr` verbatim
|
||||
// on receive, that recipient-array order maps to note position (the receive pairing needs the header
|
||||
// at a lower position), that a 0-value memo-only tx relays, and full SDXL<->DragonX interop.
|
||||
void App::broadcastChatMemos(const chat::OutgoingChatMemos& memos)
|
||||
bool App::broadcastChatMemos(const chat::OutgoingChatMemos& memos)
|
||||
{
|
||||
if (memos.recipientZaddr.empty()) return;
|
||||
if (memos.recipientZaddr.empty()) return false;
|
||||
|
||||
if (lite_wallet_) {
|
||||
broadcastChatMemosLite(memos);
|
||||
return;
|
||||
return broadcastChatMemosLite(memos);
|
||||
}
|
||||
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
ui::Notifications::instance().error("Not connected — chat message not sent.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const std::string from = chatReplyZaddr(); // spendable + advertised as the reply-to address
|
||||
if (from.empty()) {
|
||||
ui::Notifications::instance().error("No spendable z-address to send chat from.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Full-node: each memo needs the daemon's "utf8:" prefix (raw JSON/hex is otherwise rejected).
|
||||
@@ -2569,15 +2568,16 @@ void App::broadcastChatMemos(const chat::OutgoingChatMemos& memos)
|
||||
// SINGLE-recipient tx from the scalar to/amount/memo and would drop the second (payload) output.
|
||||
submitZSendMany(from, memos.recipientZaddr, 0.0, fee, /*memo*/"", recipients,
|
||||
"HushChat / broadcast", /*markFeeGapRetry*/ true, /*callback*/{});
|
||||
return true; // submitted (async build/broadcast; a later failure surfaces via the opid poller)
|
||||
}
|
||||
|
||||
// Lite variant: two 0-value recipients to the same z-address, RAW memos (the backend does
|
||||
// Memo::from_str directly — no "utf8:" prefix). The backend accepts duplicate addresses + 0-value
|
||||
// outputs for exactly this HushChat pattern. Fire-and-forget: the result surfaces via the lite
|
||||
// broadcast log; the message is already echoed locally.
|
||||
void App::broadcastChatMemosLite(const chat::OutgoingChatMemos& memos)
|
||||
bool App::broadcastChatMemosLite(const chat::OutgoingChatMemos& memos)
|
||||
{
|
||||
if (!lite_wallet_) return;
|
||||
if (!lite_wallet_) return false;
|
||||
const auto outputs = chat::chatSendOutputs(memos, /*utf8Prefix=*/false);
|
||||
wallet::LiteSendRequest req;
|
||||
for (const auto& out : outputs) {
|
||||
@@ -2589,7 +2589,9 @@ void App::broadcastChatMemosLite(const chat::OutgoingChatMemos& memos)
|
||||
}
|
||||
if (!lite_wallet_->sendTransaction(req)) {
|
||||
ui::Notifications::instance().error("A send is already in progress, or no wallet is open.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void App::sendChatMessage(const std::string& conversationId, const std::string& text)
|
||||
@@ -2620,7 +2622,7 @@ void App::sendChatMessage(const std::string& conversationId, const std::string&
|
||||
ui::Notifications::instance().error("Could not compose the message (too long?).");
|
||||
return;
|
||||
}
|
||||
broadcastChatMemos(memos);
|
||||
const bool submitted = broadcastChatMemos(memos);
|
||||
|
||||
chat::ChatMessage echo;
|
||||
echo.direction = chat::ChatDirection::Outgoing;
|
||||
@@ -2632,6 +2634,7 @@ void App::sendChatMessage(const std::string& conversationId, const std::string&
|
||||
echo.timestamp = std::time(nullptr);
|
||||
echo.txid = generateChatLocalId("out:", 8);
|
||||
echo.payload_position = 0;
|
||||
echo.delivery = submitted ? chat::ChatDelivery::Sent : chat::ChatDelivery::Failed;
|
||||
chat_service_.recordOutgoing(echo);
|
||||
}
|
||||
|
||||
@@ -2652,7 +2655,7 @@ void App::startChatConversation(const std::string& peerZaddr, const std::string&
|
||||
ui::Notifications::instance().error("Could not compose the contact request (invalid address / text?).");
|
||||
return;
|
||||
}
|
||||
broadcastChatMemos(memos);
|
||||
const bool submitted = broadcastChatMemos(memos);
|
||||
|
||||
chat::ChatMessage echo;
|
||||
echo.direction = chat::ChatDirection::Outgoing;
|
||||
@@ -2663,8 +2666,9 @@ void App::startChatConversation(const std::string& peerZaddr, const std::string&
|
||||
echo.timestamp = std::time(nullptr);
|
||||
echo.txid = generateChatLocalId("out:", 8);
|
||||
echo.payload_position = 0;
|
||||
echo.delivery = submitted ? chat::ChatDelivery::Sent : chat::ChatDelivery::Failed;
|
||||
chat_service_.recordOutgoing(echo);
|
||||
ui::Notifications::instance().success("Contact request queued.");
|
||||
if (submitted) ui::Notifications::instance().success("Contact request queued.");
|
||||
}
|
||||
|
||||
// HushChat (lite variant): the full-node harvest works off z_viewtransaction, but the lite wallet's
|
||||
|
||||
Reference in New Issue
Block a user