diff --git a/src/app_network.cpp b/src/app_network.cpp index df5eaa2..2c93758 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -5062,7 +5062,8 @@ void App::onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::st std::to_string(chat_send_queue_.front().retries) + "); message stays 'Sending'"); return; // leave at front } - chat_service_.resolveOutgoing(op.echoLocalId, chat::ChatDelivery::Failed); // hard failure -> Retry affordance + chat_service_.resolveOutgoing(op.echoLocalId, chat::ChatDelivery::Failed, + util::friendlyRpcError(error).substr(0, 160)); // hard failure -> Retry affordance + reason if (frontMatches) chat_send_queue_.pop_front(); wallet::liteLog("chat: send FAILED: " + error); } diff --git a/src/chat/chat_message.h b/src/chat/chat_message.h index 811dce7..f083584 100644 --- a/src/chat/chat_message.h +++ b/src/chat/chat_message.h @@ -26,7 +26,9 @@ struct ChatMessage { std::string body; // decrypted plaintext (Message) or request text (ContactRequest) std::int64_t timestamp = 0; // tx time in seconds; set by the ingesting caller std::size_t payload_position = 0; // together with txid, the dedup key - ChatDelivery delivery = ChatDelivery::Sent; // outgoing only + ChatDelivery delivery = ChatDelivery::Sent; // outgoing only. Sent = broadcast to the network (not + // necessarily confirmed/delivered yet). + std::string fail_reason; // outgoing Failed only; in-memory, NOT persisted }; } // namespace dragonx::chat diff --git a/src/chat/chat_service.cpp b/src/chat/chat_service.cpp index ba83c16..acdcc69 100644 --- a/src/chat/chat_service.cpp +++ b/src/chat/chat_service.cpp @@ -189,8 +189,8 @@ bool ChatService::recordOutgoingPending(const ChatMessage& message) { return appended; } -void ChatService::resolveOutgoing(const std::string& txid, ChatDelivery delivery) { - const ChatMessage* updated = store_.updateDelivery(txid, delivery); +void ChatService::resolveOutgoing(const std::string& txid, ChatDelivery delivery, const std::string& reason) { + const ChatMessage* updated = store_.updateDelivery(txid, delivery, reason); if (updated && db_) db_->upsert(*updated); // overwrite the Sending row with the final status } diff --git a/src/chat/chat_service.h b/src/chat/chat_service.h index 345c864..dee791d 100644 --- a/src/chat/chat_service.h +++ b/src/chat/chat_service.h @@ -95,7 +95,7 @@ public: // quit), then resolveOutgoing() upserts the final status once the broadcast completes. A stray // persisted Sending (crash mid-broadcast) loads back as Sent. bool recordOutgoingPending(const ChatMessage& message); - void resolveOutgoing(const std::string& txid, ChatDelivery delivery); + void resolveOutgoing(const std::string& txid, ChatDelivery delivery, const std::string& reason = {}); const ChatStore& store() const { return store_; } ChatStore& store() { return store_; } diff --git a/src/chat/chat_store.cpp b/src/chat/chat_store.cpp index 1499a10..8fc044f 100644 --- a/src/chat/chat_store.cpp +++ b/src/chat/chat_store.cpp @@ -61,10 +61,12 @@ std::vector ChatStore::conversation(const std::string& conversation return out; } -const ChatMessage* ChatStore::updateDelivery(const std::string& txid, ChatDelivery delivery) { +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; } diff --git a/src/chat/chat_store.h b/src/chat/chat_store.h index fee532c..3711048 100644 --- a/src/chat/chat_store.h +++ b/src/chat/chat_store.h @@ -40,7 +40,8 @@ public: // 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); + const ChatMessage* updateDelivery(const std::string& txid, ChatDelivery delivery, + const std::string& reason = {}); // Distinct conversation ids, in first-seen order. std::vector conversationIds() const; diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 412ad4d..3feff21 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -2927,8 +2927,10 @@ void RenderSettingsPage(App* app) { Notifications::instance().success(TR("settings_saved")); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings")); - af.next(material::ActionButtonWidth(TR("reset_to_defaults"), ICON_MD_RESTORE)); - if (material::ActionButton("##aboutreset", TR("reset_to_defaults"), ICON_MD_RESTORE, AT::Tertiary)) { + // This only reloads the last-saved values from disk (it doesn't apply factory defaults), so + // label it honestly as "Discard unsaved changes" rather than the misleading "Reset to Defaults". + af.next(material::ActionButtonWidth(TR("discard_changes"), ICON_MD_UNDO)); + if (material::ActionButton("##aboutreset", TR("discard_changes"), ICON_MD_UNDO, AT::Tertiary)) { if (app->settings()) { loadSettingsPageState(app); Notifications::instance().info(TR("settings_reloaded")); } } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings")); diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 2288dc9..b9314b3 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -1679,8 +1679,15 @@ void RenderChatTab(App* app) const char* glyph = sending ? ICON_MD_SCHEDULE : ICON_MD_DONE; const float gs = icfSz * 0.82f; const ImVec2 gsz = icf->CalcTextSizeA(gs, FLT_MAX, 0.0f, glyph); - dl->AddText(icf, gs, ImVec2(bmax.x - gsz.x, bmax.y + 1.0f * dp), - material::WithAlpha(material::OnSurface(), sending ? 95 : 135), glyph); + const ImVec2 gpos(bmax.x - gsz.x, bmax.y + 1.0f * dp); + dl->AddText(icf, gs, gpos, + material::WithAlpha(material::OnSurface(), sending ? 170 : 200), glyph); + // Explain the glyph on hover — a check means "broadcast to the network", not "delivered". + const float hpad = 4.0f * dp; + if (ImGui::IsWindowHovered() && + ImGui::IsMouseHoveringRect(ImVec2(gpos.x - hpad, gpos.y - hpad), + ImVec2(gpos.x + gsz.x + hpad, gpos.y + gsz.y + hpad))) + material::Tooltip("%s", TR(sending ? "chat_status_sending" : "chat_status_sent")); ImGui::Dummy(ImVec2(0.0f, gs + 2.0f * dp)); // reserve the status glyph (no ItemSpacing now) } // Failed send → right-aligned "not sent" + Retry (kept — it's actionable). @@ -1692,6 +1699,12 @@ void RenderChatTab(App* app) ImGui::PushFont(metaFont); ImGui::PushStyleColor(ImGuiCol_Text, material::Error()); ImGui::TextUnformatted(TR("chat_send_failed")); ImGui::PopStyleColor(); ImGui::PopFont(); + if (ImGui::IsItemHovered()) { + std::string tip = m.fail_reason.empty() + ? std::string(TR("chat_failed_generic")) + : std::string(TR("chat_failed_generic")) + "\n" + m.fail_reason; + material::Tooltip("%s", tip.c_str()); + } ImGui::SameLine(); if (ImGui::SmallButton(TR("chat_retry"))) { // A failed contact request must be re-sent as a request (it has no peer key diff --git a/src/ui/windows/explorer_tab.cpp b/src/ui/windows/explorer_tab.cpp index 758c151..021a757 100644 --- a/src/ui/windows/explorer_tab.cpp +++ b/src/ui/windows/explorer_tab.cpp @@ -424,7 +424,15 @@ static void performSearch(App* app, const std::string& query) { try { h = std::stol(query); } catch (...) { return; } int tip = app->getWalletState().sync.blocks; if (h < 1) h = 1; - if (tip > 0 && h > tip) h = tip; + if (tip > 0 && h > tip) { + // Say the height is beyond the tip instead of silently snapping to the top of the list. + char msg[128]; + snprintf(msg, sizeof(msg), TR("explorer_beyond_tip"), + (int)std::min(h, 2147483647L), tip); + s_search_error = msg; + s_search_target_height = -1; // don't re-anchor; leave the list where it is + return; + } s_search_target_height = static_cast(h); s_recent_page = 0; s_search_error.clear(); @@ -939,7 +947,30 @@ static void renderRecentBlocks(App* app, float availWidth) { if (blocks.empty()) { ImGui::SetCursorPosY(Layout::spacingMd()); - ImGui::TextDisabled("%s", fuzzyMode ? TR("explorer_no_results") : TR("loading")); + // Say WHY the list is empty — offline, syncing, or a genuine search miss — instead of an + // endless "Loading" that also shows when the node is disconnected or still catching up. + const auto& st = app->getWalletState(); + auto* erpc = app->rpc(); + char emptyBuf[128]; + const char* msg = TR("loading"); + ImU32 col = OnSurfaceMedium(); + if (fuzzyMode) { + msg = TR("explorer_no_results"); + } else if (!st.connected || !erpc || !erpc->isConnected()) { + msg = TR("explorer_empty_not_connected"); col = Warning(); + } else if (st.sync.syncing) { + const int behind = st.sync.headers - st.sync.blocks; + if (st.sync.tip_known && behind > 0) { + snprintf(emptyBuf, sizeof(emptyBuf), TR("explorer_empty_syncing"), behind); + msg = emptyBuf; + } else { + msg = TR("explorer_empty_syncing_unknown"); + } + col = Warning(); + } + ImGui::PushStyleColor(ImGuiCol_Text, col); + ImGui::TextUnformatted(msg); + ImGui::PopStyleColor(); } else { for (size_t i = 0; i < blocks.size(); i++) { const auto& row = blocks[i]; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index aa20e22..ba1061b 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -233,6 +233,9 @@ void I18n::loadBuiltinEnglish() strings_["chat_you"] = "You"; strings_["chat_contact_request"] = "contact request"; strings_["chat_send_failed"] = "not sent"; + strings_["chat_status_sending"] = "Sending… waiting for your wallet to broadcast it"; + strings_["chat_status_sent"] = "Sent to the network — it arrives after it confirms (about a minute)"; + strings_["chat_failed_generic"] = "Couldn't be sent. Check your connection and funds, then retry."; strings_["chat_new_button"] = "New chat"; strings_["chat_select_hint"] = "Select a conversation to view it."; strings_["chat_waiting_reply"] = "Waiting for this contact to reply — you can message them once they do."; @@ -532,6 +535,7 @@ void I18n::loadBuiltinEnglish() strings_["report_bug"] = "Report Bug"; strings_["save_settings"] = "Save Settings"; strings_["reset_to_defaults"] = "Reset to Defaults"; + strings_["discard_changes"] = "Discard unsaved changes"; // Wallet settings strings_["save_z_transactions"] = "Save shielded tx history"; @@ -1023,7 +1027,7 @@ void I18n::loadBuiltinEnglish() strings_["tt_website"] = "Open the DragonX website"; strings_["tt_report_bug"] = "Report an issue on the project tracker"; strings_["tt_save_settings"] = "Save all settings to disk"; - strings_["tt_reset_settings"] = "Reload settings from disk (undo unsaved changes)"; + strings_["tt_reset_settings"] = "Revert this page to the last saved settings (undo unsaved changes)"; strings_["tt_debug_collapse"] = "Collapse debug logging options"; strings_["tt_debug_expand"] = "Expand debug logging options"; strings_["tt_restart_daemon"] = "Restart the daemon to apply debug logging changes"; @@ -1592,7 +1596,7 @@ void I18n::loadBuiltinEnglish() strings_["settings_not_connected"] = "Not connected to daemon"; strings_["settings_theme_refreshed"] = "Theme list refreshed"; strings_["settings_saved"] = "Settings saved"; - strings_["settings_reloaded"] = "Settings reloaded from disk"; + strings_["settings_reloaded"] = "Unsaved changes discarded"; strings_["settings_ztx_cleared"] = "Z-transaction history cleared"; strings_["settings_ztx_not_found"] = "No history file found"; strings_["tt_open_log_folder"] = "Open the folder containing the debug and crash logs"; @@ -2754,6 +2758,10 @@ void I18n::loadBuiltinEnglish() strings_["explorer_hash_not_found"] = "No block or transaction found for this hash"; strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash"; strings_["explorer_no_results"] = "No matching cached blocks"; + strings_["explorer_beyond_tip"] = "Block %d is beyond the chain tip (%d)"; + strings_["explorer_empty_not_connected"] = "Not connected to the node"; + strings_["explorer_empty_syncing"] = "Syncing, %d blocks behind"; + strings_["explorer_empty_syncing_unknown"] = "Syncing…"; // ---- i18n audit 2026-09: wrap previously-hardcoded UI strings ---- // app_security.cpp — encryption / PIN / lock flow