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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user