fix: Tier-2 quick mediums — view-only guard, memo counter, updater/close guards

- Send: block Review from a view-only source (no spending key) with a tooltip,
  instead of failing at submit; label the memo counter as bytes and warn in
  colour once the field hits its cap (Sapling memos are byte-limited).
- Shield: show "No shielded (z) address yet — create one on the Receive tab
  first" when the destination combo is empty, instead of an empty dropdown.
- Updater dialogs (xmrig/daemon): suppress the window X during an active
  download/verify/extract so closing can't orphan/block on the worker — the
  in-dialog Cancel is the way to abort.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 13:54:55 -05:00
parent 65bb98cd09
commit b2a8c4487e
4 changed files with 30 additions and 9 deletions

View File

@@ -62,12 +62,14 @@ public:
}
using namespace material;
const float dp = Layout::dpiScale();
const auto p = s_updater->getProgress();
// No window X during an active download/verify/extract — closing would orphan/block on the
// worker; the in-dialog Cancel is the intended way to abort.
const bool active = (p.state == St::Downloading || p.state == St::Verifying || p.state == St::Extracting);
OverlayDialogSpec ov;
ov.title = TR("daemon_update_title"); ov.p_open = &s_open;
ov.title = TR("daemon_update_title"); ov.p_open = active ? nullptr : &s_open;
ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 480.0f; ov.cardBottomViewportRatio = 0.94f;
if (BeginOverlayDialog(ov)) {
const auto p = s_updater->getProgress();
using St = util::DaemonUpdater::State;
switch (p.state) {
case St::Checking: renderChecking(dp, p); break;
case St::UpToDate:

View File

@@ -889,12 +889,19 @@ static void RenderActionButtons(App* app, float width, float vScale,
auto& S = schema::UI();
const auto& state = app->getWalletState();
double total = s_amount + s_fee;
// Block spending from a view-only source (imported viewing key, no spending key) — it would only
// fail at submit. Defaults to spendable if the address isn't in the loaded list yet.
bool sourceSpendable = true;
for (const auto& a : state.addresses) {
if (a.address == s_from_address) { sourceSpendable = a.has_spending_key; break; }
}
bool can_send = app->isConnected() &&
!state.sync.syncing &&
is_valid_address &&
s_amount > 0 &&
s_from_address[0] != '\0' &&
total <= available &&
sourceSpendable &&
!s_sending;
float btnGap = Layout::spacingMd();
@@ -934,6 +941,8 @@ static void RenderActionButtons(App* app, float width, float vScale,
material::Tooltip("%s", TR("send_tooltip_enter_amount"));
else if (total > available)
material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
else if (!sourceSpendable)
material::Tooltip("%s", "View-only address — no spending key, cannot send");
else if (s_sending)
material::Tooltip("%s", TR("send_tooltip_in_progress"));
}
@@ -1439,9 +1448,13 @@ void RenderSendTab(App* app)
ImVec2(colW, memoInputH));
ImGui::PopItemWidth();
// The Sapling memo limit is in BYTES (multi-byte UTF-8 fills it faster than visible chars);
// warn in colour once the field is at its cap so it's clear input has stopped being accepted.
size_t memo_len = strlen(s_memo);
snprintf(buf, sizeof(buf), "%zu / %d", memo_len, (int)S.drawElement("business", "memo-max-length").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);
}
// Divider before action buttons

View File

@@ -129,7 +129,11 @@ void ShieldDialog::render(App* app)
}
ImGui::EndCombo();
}
if (state.z_addresses.empty()) {
material::Type().textColored(material::TypeStyle::Caption, material::Warning(),
"No shielded (z) address yet — create one on the Receive tab first.");
}
ImGui::Spacing();
// Fee

View File

@@ -51,12 +51,14 @@ public:
}
using namespace material;
const float dp = Layout::dpiScale();
const auto p = s_updater->getProgress();
// No window X during an active download/verify/extract — closing would orphan/block on the
// worker; the in-dialog Cancel is the intended way to abort.
const bool active = (p.state == St::Downloading || p.state == St::Verifying || p.state == St::Extracting);
OverlayDialogSpec ov;
ov.title = TR("xmrig_update_title"); ov.p_open = &s_open;
ov.title = TR("xmrig_update_title"); ov.p_open = active ? nullptr : &s_open;
ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 480.0f; ov.cardBottomViewportRatio = 0.94f;
if (BeginOverlayDialog(ov)) {
const auto p = s_updater->getProgress();
using St = util::XmrigUpdater::State;
switch (p.state) {
case St::Checking: renderChecking(dp, p); break;
case St::UpToDate: