From 317d9028a3631e0903c65b124a7d41ac565c7999 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 13 Jun 2026 12:19:47 -0500 Subject: [PATCH] fix(explorer): search re-anchors the block list live instead of opening a modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing in the explorer search ran an exact lookup that popped the block-detail modal. It now updates the recent-blocks LIST as you type: a block height re-anchors the list to that height (offline-friendly, no RPC), and a complete 64-char hash is resolved to its height and the list jumps there (a txid still shows the inline tx view) — all without a modal. Clearing the box returns to the recent (tip) blocks. Row clicks still open the detail modal for an explicit full view. Removed the now-unused fetchBlockDetailByHash. Co-Authored-By: Claude Opus 4.8 --- src/ui/windows/explorer_tab.cpp | 139 ++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 62 deletions(-) diff --git a/src/ui/windows/explorer_tab.cpp b/src/ui/windows/explorer_tab.cpp index 26bf134..baa1d8e 100644 --- a/src/ui/windows/explorer_tab.cpp +++ b/src/ui/windows/explorer_tab.cpp @@ -44,6 +44,9 @@ static std::string s_search_error; static double s_search_change_time = -1.0; // GetTime() of the last edit (debounce for live search) static std::string s_search_last_query; // last query actually run (avoids re-running it) static constexpr double kSearchDebounceSeconds = 0.35; +// When > 0, the recent-blocks list is anchored here (a search result) instead of the chain tip. +// Searching re-anchors the LIST rather than opening the detail modal — that stays for row clicks. +static int s_search_target_height = -1; // A query that performSearch() can resolve to a concrete result: a block height (all digits) or a // complete 64-char block hash / txid. Partial hex is intentionally NOT resolvable, so live search @@ -277,53 +280,6 @@ static void fetchBlockDetail(App* app, int height) { }); } -static void fetchBlockDetailByHash(App* app, const std::string& hash) { - auto* worker = app->worker(); - auto* rpc = app->rpc(); - if (!worker || !rpc) return; - s_detail_loading = true; - s_search_loading = true; - s_search_error.clear(); - worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb { - json blockResult; - json txResult; - std::string blockErr; - bool gotBlock = false; - bool gotTx = false; - // Try as block hash first - try { - rpc::RPCClient::TraceScope trace("Explorer tab / Search"); - blockResult = rpc->call("getblock", {hash}); - gotBlock = true; - } catch (...) {} - // If not a block hash, try as txid - if (!gotBlock) { - try { - rpc::RPCClient::TraceScope trace("Explorer tab / Search"); - txResult = rpc->call("getrawtransaction", {hash, 1}); - gotTx = true; - } catch (...) {} - } - return [blockResult, txResult, gotBlock, gotTx]() { - if (gotBlock) { - applyBlockDetailResult(blockResult, ""); - } else if (gotTx && !txResult.is_null()) { - s_detail_loading = false; - s_search_loading = false; - s_tx_detail = txResult; - s_tx_loading = false; - s_expanded_tx_idx = 0; - s_search_error.clear(); - s_show_detail_modal = false; - } else { - s_detail_loading = false; - s_search_loading = false; - s_search_error = "No block or transaction found for this hash"; - s_show_detail_modal = false; - } - }; - }); -} static bool fetchRecentBlock(App* app, int height) { auto* worker = app->worker(); @@ -405,24 +361,72 @@ static void fetchTxDetail(App* app, const std::string& txid) { }); } -static void performSearch(App* app, const std::string& query) { - if (query.empty()) return; +// Resolve a 64-char hash to a block height and re-anchor the list there (no modal). If the hash is a +// txid rather than a block, fall back to the inline transaction view (also not a modal). +static void navigateToHash(App* app, const std::string& hash) { + auto* worker = app->worker(); + auto* rpc = app->rpc(); + if (!worker || !rpc || !rpc->isConnected()) return; + s_search_loading = true; + s_search_error.clear(); + worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb { + json blockResult, txResult; + bool gotBlock = false, gotTx = false; + try { + rpc::RPCClient::TraceScope trace("Explorer tab / Search"); + blockResult = rpc->call("getblock", {hash}); + gotBlock = true; + } catch (...) {} + if (!gotBlock) { + try { + rpc::RPCClient::TraceScope trace("Explorer tab / Search"); + txResult = rpc->call("getrawtransaction", {hash, 1}); + gotTx = true; + } catch (...) {} + } + return [blockResult, txResult, gotBlock, gotTx]() { + s_search_loading = false; + if (gotBlock) { + int h = blockResult.value("height", -1); + if (h > 0) { s_search_target_height = h; s_recent_page = 0; } + s_show_detail_modal = false; + } else if (gotTx && !txResult.is_null()) { + // A txid: show it inline (existing tx detail view), not a modal. + s_tx_detail = txResult; + s_tx_loading = false; + s_expanded_tx_idx = 0; + s_show_detail_modal = false; + } else { + s_search_error = "No block or transaction found for this hash"; + } + }; + }); +} - // Integer = block height - bool isNumeric = std::all_of(query.begin(), query.end(), ::isdigit); - if (isNumeric && !query.empty()) { - int height = std::stoi(query); - fetchBlockDetail(app, height); +// Search re-anchors the recent-blocks LIST to the queried block instead of popping the detail modal. +static void performSearch(App* app, const std::string& query) { + if (query.empty()) { s_search_target_height = -1; return; } + + // Integer = block height → just point the list at it. + if (std::all_of(query.begin(), query.end(), ::isdigit)) { + long h = 0; + try { h = std::stol(query); } catch (...) { return; } + int tip = app->getWalletState().sync.blocks; + if (h < 1) h = 1; + if (tip > 0 && h > tip) h = tip; + s_search_target_height = static_cast(h); + s_recent_page = 0; + s_search_error.clear(); return; } - // 64-char hex = block hash or txid + // 64-char hex = block hash or txid → resolve to a height and re-anchor (or inline tx). bool isHex64 = query.size() == 64 && std::all_of(query.begin(), query.end(), [](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }); if (isHex64) { - fetchBlockDetailByHash(app, query); + navigateToHash(app, query); return; } @@ -493,20 +497,27 @@ static void renderSearchBar(App* app, float availWidth) { if (edited) { s_search_change_time = ImGui::GetTime(); // (re)start the debounce on each keystroke s_search_error.clear(); // don't show a stale error mid-type + if (query.empty()) { + // Cleared the box → drop the anchor and return to recent (tip) blocks immediately. + s_search_target_height = -1; + s_search_last_query.clear(); + s_search_change_time = -1.0; + } } bool doSearch = enter || clicked; - // Live search: once the user pauses and the query is resolvable, run it without waiting for Enter. + // Live search: once the user pauses and the query is resolvable, re-anchor the list (no modal) + // without waiting for Enter. if (!doSearch && s_search_change_time >= 0.0 && (ImGui::GetTime() - s_search_change_time) >= kSearchDebounceSeconds && isResolvableQuery(query) && query != s_search_last_query) { doSearch = true; } - if (doSearch && app->rpc() && app->rpc()->isConnected() && !query.empty()) { + if (doSearch && !query.empty()) { s_search_change_time = -1.0; // consume the debounce s_search_last_query = query; - performSearch(app, query); + performSearch(app, query); // numeric is offline-friendly; hash lookup checks rpc itself } // Loading spinner @@ -777,12 +788,16 @@ static void renderRecentBlocks(App* app, float availWidth) { int rowsPerPage = std::max(1, (int)std::ceil(rowAreaH / std::max(1.0f, baseRowH))); float rowH = baseRowH; - int maxPage = (state.sync.blocks > 0) ? (state.sync.blocks - 1) / rowsPerPage : 0; + // Anchor the list at a search result when one is active, otherwise at the chain tip. + int anchorTip = (s_search_target_height > 0 && s_search_target_height <= state.sync.blocks) + ? s_search_target_height : state.sync.blocks; + + int maxPage = (anchorTip > 0) ? (anchorTip - 1) / rowsPerPage : 0; s_recent_max_page = maxPage; if (s_recent_page > maxPage) s_recent_page = maxPage; if (s_recent_page < 0) s_recent_page = 0; - int startHeight = state.sync.blocks - s_recent_page * rowsPerPage; + int startHeight = anchorTip - s_recent_page * rowsPerPage; std::vector pageHeights; pageHeights.reserve(rowsPerPage); for (int i = 0; i < rowsPerPage; ++i) { @@ -812,7 +827,7 @@ static void renderRecentBlocks(App* app, float availWidth) { if (currentPageComplete && s_recent_page < maxPage) { std::vector nextPageHeights; nextPageHeights.reserve(rowsPerPage); - int nextStartHeight = state.sync.blocks - (s_recent_page + 1) * rowsPerPage; + int nextStartHeight = anchorTip - (s_recent_page + 1) * rowsPerPage; for (int i = 0; i < rowsPerPage; ++i) { int height = nextStartHeight - i; if (height < 1) break;