fix(ui): prevent stuck spinners and double-submits in dialogs
Address a cluster of low-severity robustness nits where an async callback could be skipped (leaving a spinner spinning forever) or a synchronous action could be re-fired mid-flight: - app_network: add catch(...) fallbacks around the importPrivateKey and submitZSendMany worker lambdas so a non-std throw can't escape the worker and skip the main-thread callback (stuck "Importing…"/"Sending…"). - export_transactions: re-entrancy guard disabling Export while a write is in flight. - bootstrap_download: disable Cancel once clicked so the request is visibly acknowledged and can't be re-fired before the worker stops. - network_tab: give clear "already in list" feedback on a duplicate server (keep the inputs, clear only the stale invalid-URL error) and disable/relabel Refresh while a probe is in flight. - key_export: offer Retry from the error branch (falls back to Reveal) and guard against an empty/malformed address before dispatching. - receive_tab: only enter the "generating…" state when a dispatch is actually possible (guards a stuck spinner when disconnected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -290,27 +290,34 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), material::LoadingDots());
|
||||
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
|
||||
} else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
s_generating_address = true;
|
||||
if (s_addr_type_filter != 2) {
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_address = false;
|
||||
if (addr.empty())
|
||||
Notifications::instance().error(TR("failed_create_shielded"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success(TR("new_shielded_created"));
|
||||
}
|
||||
});
|
||||
// createNewAddress can early-return without ever invoking the callback (e.g. connection
|
||||
// dropped between this button being enabled and the click landing), which would leave
|
||||
// s_generating_address stuck. Only enter the "generating…" state when a dispatch is possible.
|
||||
if (!app->isConnected()) {
|
||||
s_generating_address = false;
|
||||
} else {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
s_generating_address = false;
|
||||
if (addr.empty())
|
||||
Notifications::instance().error(TR("failed_create_transparent"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success(TR("new_transparent_created"));
|
||||
}
|
||||
});
|
||||
s_generating_address = true;
|
||||
if (s_addr_type_filter != 2) {
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_address = false;
|
||||
if (addr.empty())
|
||||
Notifications::instance().error(TR("failed_create_shielded"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success(TR("new_shielded_created"));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
s_generating_address = false;
|
||||
if (addr.empty())
|
||||
Notifications::instance().error(TR("failed_create_transparent"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success(TR("new_transparent_created"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
@@ -601,6 +608,7 @@ void RenderReceiveTab(App* app)
|
||||
if (s_request_usd_mode && usd_price > 0) {
|
||||
ImGui::PushItemWidth(amtInputW);
|
||||
if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) {
|
||||
if (s_request_usd_amount < 0) s_request_usd_amount = 0;
|
||||
s_request_amount = s_request_usd_amount / usd_price;
|
||||
}
|
||||
{
|
||||
@@ -617,6 +625,7 @@ void RenderReceiveTab(App* app)
|
||||
} else {
|
||||
ImGui::PushItemWidth(amtInputW);
|
||||
if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) {
|
||||
if (s_request_amount < 0) s_request_amount = 0;
|
||||
if (usd_price > 0)
|
||||
s_request_usd_amount = s_request_amount * usd_price;
|
||||
}
|
||||
@@ -763,9 +772,14 @@ void RenderReceiveTab(App* app)
|
||||
ImVec2(addrColW, memoInputH));
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
// The Sapling memo limit is in BYTES and matches the enforced buffer size
|
||||
// (InputTextMultiline caps at sizeof(s_request_memo)); count against that, not the
|
||||
// old display-only 256 value, and warn in colour once the field is at its cap.
|
||||
size_t memo_len = strlen(s_request_memo);
|
||||
snprintf(buf, sizeof(buf), "%zu / %d", memo_len, (int)S.drawElement("tabs.receive", "memo-max-display-chars").size);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
|
||||
bool memoAtCap = memo_len + 1 >= memoMax;
|
||||
snprintf(buf, sizeof(buf), "%zu / %zu bytes", memo_len, memoMax);
|
||||
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// URI preview
|
||||
|
||||
Reference in New Issue
Block a user