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:
2026-07-05 15:08:43 -05:00
parent 1dbbd44759
commit 41f5548593
6 changed files with 86 additions and 30 deletions

View File

@@ -160,7 +160,12 @@ void RenderLiteNetworkTab(App* app)
applyAndRebuild();
}
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 110.0f * dp);
if (TactileButton(TR("lite_net_refresh"), ImVec2(100.0f * dp, 0))) startProbe(st);
// While a probe is in flight, disable + relabel Refresh so there's clear in-progress feedback.
const bool probing = s_probe.busy();
ImGui::BeginDisabled(probing);
if (TactileButton(probing ? TR("lite_net_checking") : TR("lite_net_refresh"), ImVec2(100.0f * dp, 0)))
startProbe(st);
ImGui::EndDisabled();
if (randomMode)
Type().textColored(TypeStyle::Caption, Primary(), TR("lite_net_random_active"));
ImGui::Spacing();
@@ -191,7 +196,13 @@ void RenderLiteNetworkTab(App* app)
auto servers = st->getLiteServers();
bool exists = false;
for (const auto& s : servers) if (s.url == url) { exists = true; break; }
if (!exists) {
if (exists) {
// Duplicate: keep the inputs (so it doesn't look like the add succeeded); only
// clear any stale invalid-URL error since this URL is valid. The user-facing
// "already in list" message needs a shared i18n key added centrally, so no new
// text is set here.
s_addError.clear();
} else {
config::Settings::LiteServerPreference p;
p.url = url;
p.label = !addLabel.empty() ? addLabel : url;
@@ -200,8 +211,8 @@ void RenderLiteNetworkTab(App* app)
st->setLiteServers(servers);
st->save();
startProbe(st);
s_addUrl[0] = '\0'; s_addLabel[0] = '\0'; s_addError.clear();
}
s_addUrl[0] = '\0'; s_addLabel[0] = '\0'; s_addError.clear();
}
}
if (!s_addError.empty())