From 4a8d07edce64a25e93865ccbc89837ad8ebf42b3 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 17 Oct 2026 16:48:33 -0500 Subject: [PATCH] =?UTF-8?q?feat(balance):=20show=20a=20"catching=20up=20?= =?UTF-8?q?=E2=80=94=20funds=20are=20safe"=20strip=20while=20syncing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A syncing node skips the balance RPC, so the balance card paints a bare 0.00000000 / $-.-- that reads as lost funds after an update (a documented support trap). Add one shared notice at the top of the Balance tab — above every layout — shown only for the normal catching-up case (connected, network tip known, still verifying, not a rescan/witness rebuild). Offline, no-peers, a busy node and rescans stay the node-status banner's job, so the two never stack. Co-Authored-By: Claude Opus 4.8 --- src/ui/windows/balance_tab.cpp | 33 +++++++++++++++++++++++++++++++++ src/util/i18n.cpp | 1 + 2 files changed, 34 insertions(+) diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index 18c7eae..2b17fcd 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -205,6 +205,39 @@ void RenderBalanceTab(App* app) } } + // Shared "still catching up" strip, above whichever layout renders, so a syncing node's 0 / stale + // balance is never mistaken for lost funds (the documented "my coins vanished after an update" + // trap). Scoped to the NORMAL catching-up case — connected, a network tip is known, still verifying + // blocks, and not in a rescan / witness rebuild. Offline, no-peers, a busy node and rescans are + // already surfaced by the node-status banner, so this never stacks a second banner on them. + { + namespace m = material; + const auto& st = app->state(); + if (st.connected && st.sync.tip_known && st.sync.syncing && + !st.sync.building_witnesses && !st.sync.rescanning) { + ImFont* ico = m::Type().iconSmall(); + ImFont* txt = m::Type().body2(); + const float padX = ui::Layout::spacingMd(); + const float padY = ui::Layout::spacingSm(); + const float availW = ImGui::GetContentRegionAvail().x; + char line[192]; + snprintf(line, sizeof(line), TR("balance_catchup_notice"), + static_cast(st.sync.verification_progress) * 100.0f); + const float iconW = ico->LegacySize + padX * 0.5f; + const float wrapW = std::max(1.0f, availW - padX * 2.0f - iconW); + ImVec2 tsz = txt->CalcTextSizeA(txt->LegacySize, FLT_MAX, wrapW, line); + const float h = std::max(ico->LegacySize, tsz.y) + padY * 2.0f; + ImVec2 p0 = ImGui::GetCursorScreenPos(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddRectFilled(p0, ImVec2(p0.x + availW, p0.y + h), + m::WithAlphaF(m::Warning(), 0.13f), ui::Layout::glassRounding()); + dl->AddText(ico, ico->LegacySize, ImVec2(p0.x + padX, p0.y + padY), m::Warning(), ICON_MD_SYNC); + dl->AddText(txt, txt->LegacySize, ImVec2(p0.x + padX + iconW, p0.y + padY), + m::OnSurfaceMedium(), line, nullptr, wrapW); + ImGui::Dummy(ImVec2(availW, h + ui::Layout::spacingSm())); + } + } + // Dispatch by string ID for (const auto& entry : s_layoutDispatch) { if (layoutId == entry.id) { diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 556504d..d3075f0 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1237,6 +1237,7 @@ void I18n::loadBuiltinEnglish() strings_["balance_shielded_fmt"] = "Shielded: %.8f"; strings_["total_balance_label"] = "Total Balance"; strings_["balance_syncing_pct"] = "Syncing %.1f%%"; + strings_["balance_catchup_notice"] = "Catching up to the network (%.0f%%) — your balance will finish updating when sync completes. Your funds are safe."; strings_["balance_mining_rate"] = "Mining %s"; strings_["balance_layout_switched"] = "Layout: %s"; strings_["byte_count_fmt"] = "%zu / %zu bytes";