fix(explorer): search re-anchors the block list live instead of opening a modal

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 12:19:47 -05:00
parent 09ab8d52c5
commit 317d9028a3

View File

@@ -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<int>(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<int> 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<int> 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;