feat(history): show "Loading older history (N%)" during the initial bulk load

History streams in over many refresh cycles (the incremental shielded scan
walks every z-address), so the first batch appears long before the list is
complete — with no indication more is still coming. The existing loading banner
deliberately goes quiet once any rows are on screen.

Track whether the first full shielded scan has finished
(initial_history_scan_complete_) and, until it has, surface a progress percentage
(fraction of z-addresses scanned) in transactionRefreshProgressText() — which the
History tab already renders as its pulsing loading indicator. Goes quiet once the
first scan completes; routine per-block re-scans don't re-trigger it. Reset on a
full history invalidation (rescan / session reset) so it shows again on reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 09:05:37 -05:00
parent d8c055c125
commit 2b70ee5cd8
4 changed files with 21 additions and 0 deletions

View File

@@ -3704,6 +3704,20 @@ std::string App::transactionRefreshProgressText() const
return buf; return buf;
} }
// Initial bulk history load: the incremental shielded scan streams older transactions in over
// many refresh cycles, so the first batch can appear long before the history is complete. Keep
// the user informed (with a %) even after rows are on screen, so they know it's still loading.
// Progress = fraction of z-addresses scanned at least once (monotonic). Goes quiet for good once
// the first full scan completes; routine per-block re-scans don't re-trigger it.
if (!initial_history_scan_complete_ && !state_.z_addresses.empty()) {
std::size_t total = state_.z_addresses.size();
std::size_t scanned = std::min(shielded_history_scan_heights_.size(), total);
int pct = total > 0 ? static_cast<int>((scanned * 100) / total) : 0;
if (pct > 99) pct = 99; // 100% is the "complete" state, which suppresses the banner
snprintf(buf, sizeof(buf), TR("tx_loading_history_progress"), pct);
return buf;
}
// Once history is on screen, stay quiet for routine background refreshes. The incremental // Once history is on screen, stay quiet for routine background refreshes. The incremental
// shielded-receive scan re-dirties transactions_dirty_ on EVERY new block (each block moves the // shielded-receive scan re-dirties transactions_dirty_ on EVERY new block (each block moves the
// "scanned at tip" bar, invalidating prior per-address scans), so for a wallet with more // "scanned at tip" bar, invalidating prior per-address scans), so for a wallet with more

View File

@@ -575,6 +575,10 @@ private:
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
std::size_t shielded_history_scan_cursor_ = 0; std::size_t shielded_history_scan_cursor_ = 0;
bool shielded_history_scan_pending_ = false; bool shielded_history_scan_pending_ = false;
// False until the first full shielded-history scan finishes. Drives the History tab's
// "Loading older history…" progress so the user knows transactions are still streaming in
// after the first batch appears; goes quiet for the routine per-block re-scans afterward.
bool initial_history_scan_complete_ = false;
std::unordered_map<std::string, int> shielded_history_scan_heights_; std::unordered_map<std::string, int> shielded_history_scan_heights_;
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for // P4b: z_viewtransaction result cache — avoids re-calling the RPC for

View File

@@ -975,6 +975,7 @@ void App::invalidateShieldedHistoryScanProgress(bool persistCache)
{ {
shielded_history_scan_cursor_ = 0; shielded_history_scan_cursor_ = 0;
shielded_history_scan_pending_ = false; shielded_history_scan_pending_ = false;
initial_history_scan_complete_ = false; // a full re-scan is coming — show load progress again
shielded_history_scan_heights_.clear(); shielded_history_scan_heights_.clear();
if (persistCache) storeTransactionHistoryCacheIfAvailable(); if (persistCache) storeTransactionHistoryCacheIfAvailable();
} }
@@ -1353,6 +1354,7 @@ void App::refreshTransactionData()
storeTransactionHistoryCacheIfAvailable(); storeTransactionHistoryCacheIfAvailable();
shielded_history_scan_cursor_ = nextShieldedScanStartIndex; shielded_history_scan_cursor_ = nextShieldedScanStartIndex;
shielded_history_scan_pending_ = !shieldedScanComplete; shielded_history_scan_pending_ = !shieldedScanComplete;
if (shieldedScanComplete) initial_history_scan_complete_ = true;
transactions_dirty_ = !shieldedScanComplete; transactions_dirty_ = !shieldedScanComplete;
maybeFinishTransactionSendProgress(); maybeFinishTransactionSendProgress();
}; };

View File

@@ -568,6 +568,7 @@ void I18n::loadBuiltinEnglish()
strings_["tx_loading_scanning_shielded"] = "Scanning shielded history (%d addresses)"; strings_["tx_loading_scanning_shielded"] = "Scanning shielded history (%d addresses)";
strings_["tx_loading_refreshing_cached"] = "Refreshing wallet history (%d cached)"; strings_["tx_loading_refreshing_cached"] = "Refreshing wallet history (%d cached)";
strings_["tx_loading_fetching_transparent"] = "Fetching transparent history"; strings_["tx_loading_fetching_transparent"] = "Fetching transparent history";
strings_["tx_loading_history_progress"] = "Loading older history (%d%%)";
strings_["no_matching"] = "No matching transactions"; strings_["no_matching"] = "No matching transactions";
strings_["transaction_id"] = "TRANSACTION ID"; strings_["transaction_id"] = "TRANSACTION ID";
strings_["search_placeholder"] = "Search..."; strings_["search_placeholder"] = "Search...";