fix(history): keep the open detail row across paging and filtering

The inline detail panel was tracked by the filtered list index. The
display list is rebuilt and re-sorted on every data change, sort switch
or filter edit, so a stored index pointed at a different transaction
after the rebuild, and a page reset was triggered by any change in the
row count, bouncing the view back to page 1.

- The open row is now tracked by a stable content key (txid, display
  type, address and the amount's exact bits) so the right row stays open
  across rebuilds.
- The page resets and the open row collapses only when a filter input
  changes (type, sort or the full search text). A new or removed row is
  data and now just clamps the current page.
- The open row is also cleared on page navigation and when the list
  empties, so a wallet switch does not resurrect an old expansion.

Adds pure helpers rowKey/filterInputsChanged to
transactions_tab_helpers.h.

Tests: rowKey distinguishes each field and separates +0.0 from -0.0, and
filterInputsChanged latches each input change but not the first frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 09:24:51 -05:00
co-authored by Claude Opus 4.8
parent a5e4fd5c02
commit c3f981b52e
3 changed files with 83 additions and 13 deletions
+41
View File
@@ -5,6 +5,8 @@
// Pure helpers behind the History tab (no ImGui), so they can be unit-tested.
#include "../../data/wallet_state.h"
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <string>
#include <string_view>
@@ -45,6 +47,45 @@ inline TxidGroups groupByTxid(const std::vector<TransactionInfo>& txs)
return g;
}
// Stable identity of a display row, used to keep an inline detail panel open across list rebuilds.
// txid alone is not unique (a tx can show several rows: multiple receive outputs, unmerged legs), so
// add the display type, address and the amount's exact bits. Two rows identical in all four are the same
// logical payment and expanding both is acceptable.
inline std::string rowKey(const std::string& txid, const std::string& displayType,
const std::string& address, double amount)
{
std::uint64_t bits = 0;
std::memcpy(&bits, &amount, sizeof bits);
char hex[17];
std::snprintf(hex, sizeof hex, "%016llx", static_cast<unsigned long long>(bits));
std::string k;
k.reserve(txid.size() + displayType.size() + address.size() + 20);
k.append(txid).push_back('|');
k.append(displayType).push_back('|');
k.append(address).push_back('|');
k.append(hex);
return k;
}
// The inputs that decide which rows the History list shows and in what order.
struct FilterInputs {
int type = 0;
int sort = 0;
std::string search;
};
// True (and `prev` updated) when type, sort or the FULL search text changed since last call. The caller
// then resets to page 1 and collapses the open row. Starts at the defaults (0, 0, "") so the first frame
// does not count as a change.
inline bool filterInputsChanged(FilterInputs& prev, int type, int sort, const std::string& search)
{
if (prev.type == type && prev.sort == sort && prev.search == search) return false;
prev.type = type;
prev.sort = sort;
prev.search = search;
return true;
}
} // namespace history
} // namespace ui
} // namespace dragonx