From 3f127f9b62d56c635096d6f8069f66c6405c4770 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 18 Oct 2026 22:50:30 -0500 Subject: [PATCH] feat(request,balance): Request-Payment create-address empty-state + tx empty states - Request-Payment no longer dead-ends with no shielded address: it shows a hint and a "Create a shielded address" button (auto-selecting the new address), and labels address entries with their contact/wallet names. - The balance "Recent transactions" empty area uses a proper empty-state and now distinguishes "still loading / syncing" from "no transactions yet" instead of a dim one-liner that also showed mid-sync. Co-Authored-By: Claude Opus 4.8 --- src/ui/windows/balance_components.cpp | 22 ++++++- src/ui/windows/request_payment_dialog.cpp | 79 +++++++++++++++++------ 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index 06f6781..948d8a3 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -1057,7 +1057,27 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float int count = std::min({4, maxRows, (int)txs.size()}); // show only the most recent (state.transactions is newest-first) if (count == 0) { - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet")); + const bool roomForIcon = ImGui::GetContentRegionAvail().y >= 100.0f * dp; + if (!app->isConnected()) { + // Disconnected: not "empty" and not "loading" — keep a plain caption. + Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), + TR(app->isLiteBuild() ? "lite_no_wallet" : "not_connected")); + } else { + const std::string txLoadingText = app->transactionRefreshProgressText(); + // In lite, transactionRefreshProgressText() is empty and last_tx_update is stamped differently, + // so treat only an active sync as "loading" there. + const bool loadingOrSyncing = !txLoadingText.empty() || + (state.sync.syncing && !state.sync.isSynced()) || + (!app->isLiteBuild() && state.last_tx_update == 0); + if (loadingOrSyncing) { + char lbuf[192]; + const char* base = !txLoadingText.empty() ? txLoadingText.c_str() : TR("loading_transactions"); + snprintf(lbuf, sizeof(lbuf), "%s%s", base, material::LoadingDots()); + material::DrawEmptyState(roomForIcon ? ICON_MD_HOURGLASS_EMPTY : nullptr, lbuf); + } else { + material::DrawEmptyState(roomForIcon ? ICON_MD_RECEIPT_LONG : nullptr, TR("no_transactions_yet")); + } + } } else { ImDrawList* dl = ImGui::GetWindowDrawList(); ImFont* capFont = Type().caption(); diff --git a/src/ui/windows/request_payment_dialog.cpp b/src/ui/windows/request_payment_dialog.cpp index c21e301..2d4ccb5 100644 --- a/src/ui/windows/request_payment_dialog.cpp +++ b/src/ui/windows/request_payment_dialog.cpp @@ -32,6 +32,7 @@ static uintptr_t s_qr_texture = 0; static int s_qr_width = 0; static int s_qr_height = 0; static bool s_uri_dirty = true; +static bool s_creating_address = false; // true while a Create-shielded-address RPC is in flight // Helper to build payment URI static std::string buildPaymentUri() @@ -48,7 +49,8 @@ void RequestPaymentDialog::show(const std::string& address) s_label[0] = '\0'; s_payment_uri.clear(); s_uri_dirty = true; - + s_creating_address = false; + // Only a shielded address can be paid (DragonX is a private chain): never prefill a mining address. if (!address.empty() && !util::isTransparentAddress(address)) { strncpy(s_address, address.c_str(), sizeof(s_address) - 1); @@ -67,6 +69,7 @@ void RequestPaymentDialog::show(const std::string& address) void RequestPaymentDialog::hide() { s_open = false; + s_creating_address = false; s_payment_uri.clear(); if (s_qr_texture != 0) { FreeQRTexture(s_qr_texture); @@ -100,17 +103,54 @@ void RequestPaymentDialog::render(App* app) ImGui::Spacing(); // Address selection - ImGui::Text("%s", TR("request_receive_address")); - - std::string addr_display = s_address[0] ? s_address : TR("request_select_address"); - if (addr_display.length() > static_cast(zAddrLbl.truncate)) { - addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate); - } - - ImGui::SetNextItemWidth(-1); - if (ImGui::BeginCombo("##Address", addr_display.c_str())) { - // Z-addresses - if (!state.z_addresses.empty()) { + const bool noShielded = state.z_addresses.empty(); + if (noShielded) { + // Nothing to request to yet — offer to create a shielded address instead of dead-ending. + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium())); + ImGui::TextWrapped("%s", TR("request_no_shielded")); + ImGui::PopStyleColor(); + ImGui::Spacing(); + if (s_creating_address) { + ImGui::BeginDisabled(); + material::TactileButton(TR("generating"), ImVec2(0, 0), S.resolveFont(actionBtn.font)); + ImGui::EndDisabled(); + } else { + ImGui::BeginDisabled(!app->isConnected()); + if (material::TactileButton(TR("request_create_shielded"), ImVec2(0, 0), S.resolveFont(actionBtn.font))) { + if (app->isConnected()) { + s_creating_address = true; + app->createNewZAddress([](const std::string& addr) { + s_creating_address = false; + if (addr.empty()) { + Notifications::instance().error(TR("failed_create_shielded")); + } else { + Notifications::instance().success(TR("new_shielded_created")); + // Pre-select the new address if the dialog is still open and none chosen. + if (s_open && s_address[0] == '\0') { + strncpy(s_address, addr.c_str(), sizeof(s_address) - 1); + s_address[sizeof(s_address) - 1] = '\0'; + s_uri_dirty = true; + } + } + }); + } + } + ImGui::EndDisabled(); + } + } else { + ImGui::Text("%s", TR("request_receive_address")); + + std::string addr_display = s_address[0] ? s_address : std::string(TR("request_select_address")); + if (addr_display.length() > static_cast(zAddrLbl.truncate)) { + addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate); + } + if (s_address[0]) { + std::string l = app->getAddressLabel(s_address); + if (!l.empty()) addr_display = l + " " + addr_display; + } + + ImGui::SetNextItemWidth(-1); + if (ImGui::BeginCombo("##Address", addr_display.c_str())) { ImGui::TextDisabled("%s", TR("request_shielded_addrs")); for (size_t i = 0; i < state.z_addresses.size(); i++) { const auto& addr = state.z_addresses[i]; @@ -118,19 +158,20 @@ void RequestPaymentDialog::render(App* app) if (label.length() > static_cast(zAddrLbl.truncate)) { label = label.substr(0, zAddrFrontLbl.truncate) + "..." + label.substr(label.length() - zAddrBackLbl.truncate); } - - if (ImGui::Selectable(label.c_str(), addr.address == s_address)) { + std::string clbl = app->getAddressLabel(addr.address); + std::string shown = (clbl.empty() ? label : (clbl + " " + label)) + "##" + addr.address; + + if (ImGui::Selectable(shown.c_str(), addr.address == s_address)) { strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1); s_uri_dirty = true; } } + // No mining (transparent) addresses: they only receive mining rewards, so a request to one + // could never be paid. + ImGui::EndCombo(); } - - // No mining (transparent) addresses: they only receive mining rewards, so a request to one - // could never be paid. - ImGui::EndCombo(); } - + ImGui::Spacing(); // Amount (optional)