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 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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<size_t>(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<size_t>(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<size_t>(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)
|
||||
|
||||
Reference in New Issue
Block a user