From 5796664b51a6bfb15d7c4618d22538ec01da13e5 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 13 Jun 2026 09:55:24 -0500 Subject: [PATCH] feat(history): add date and amount sorting to the History tab Add a sort selector next to the type filter with four modes: Newest first (default), Oldest first, Largest amount, Smallest amount. The mode folds into the merged-list memoization cache key (so the list re-sorts only when the mode changes) and the comparator branches on it, keeping txid as a deterministic tiebreak. Changing the sort resets to page 1. Co-Authored-By: Claude Opus 4.8 --- src/ui/windows/transactions_tab.cpp | 50 +++++++++++++++++++++++------ src/util/i18n.cpp | 4 +++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/ui/windows/transactions_tab.cpp b/src/ui/windows/transactions_tab.cpp index 7eac0cd..abcdfc7 100644 --- a/src/ui/windows/transactions_tab.cpp +++ b/src/ui/windows/transactions_tab.cpp @@ -207,6 +207,8 @@ void RenderTransactionsTab(App* app) // Clickable type filter (clicking a card sets the type filter) static int type_filter = 0; + // Sort mode: 0 = newest first, 1 = oldest first, 2 = largest amount, 3 = smallest amount. + static int s_sort_mode = 0; // --- Received card --- { @@ -332,6 +334,13 @@ void RenderTransactionsTab(App* app) const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), TR("mined_filter") }; ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types)); + // Sort selector + ImGui::SameLine(0, filterGap); + ImGui::SetNextItemWidth(comboW); + const char* sorts[] = { TR("sort_date_newest"), TR("sort_date_oldest"), + TR("sort_amount_high"), TR("sort_amount_low") }; + ImGui::Combo("##TxSort", &s_sort_mode, sorts, IM_ARRAYSIZE(sorts)); + ImGui::SameLine(0, filterGap); if (TactileButton(TrId("refresh", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { app->refreshNow(); @@ -386,6 +395,7 @@ void RenderTransactionsTab(App* app) auto mix = [&h](std::uint64_t v) { h = (h ^ v) * 1099511628211ULL; }; mix(state.transactions.size()); mix(static_cast(state.last_tx_update)); + mix(static_cast(s_sort_mode)); // re-sort the cache when the mode changes for (const auto& t : state.transactions) { mix(static_cast(t.confirmations)); mix(static_cast(t.timestamp)); @@ -469,16 +479,36 @@ void RenderTransactionsTab(App* app) } } - // Sort: pending (0-conf) transactions first, then newest-first by timestamp, with txid as - // a final deterministic tiebreak so same-block transactions keep a stable order across - // rebuilds (otherwise equal timestamps reorder every time a new block bumps confirmations). + // Sort per the selected mode. txid is the final deterministic tiebreak so same-block + // transactions keep a stable order across rebuilds (otherwise equal keys reorder every + // time a new block bumps confirmations). + const int sortMode = s_sort_mode; std::sort(display_txns.begin(), display_txns.end(), - [](const DisplayTx& a, const DisplayTx& b) { - bool aPending = (a.confirmations == 0); - bool bPending = (b.confirmations == 0); - if (aPending != bPending) return aPending; - if (a.timestamp != b.timestamp) return a.timestamp > b.timestamp; - return a.txid > b.txid; + [sortMode](const DisplayTx& a, const DisplayTx& b) { + switch (sortMode) { + case 1: // Oldest first + if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp; + return a.txid < b.txid; + case 2: { // Largest amount first + double aa = std::abs(a.amount), ba = std::abs(b.amount); + if (aa != ba) return aa > ba; + if (a.timestamp != b.timestamp) return a.timestamp > b.timestamp; + return a.txid > b.txid; + } + case 3: { // Smallest amount first + double aa = std::abs(a.amount), ba = std::abs(b.amount); + if (aa != ba) return aa < ba; + if (a.timestamp != b.timestamp) return a.timestamp > b.timestamp; + return a.txid > b.txid; + } + default: { // 0: Newest first — pending (0-conf) on top, then by date + bool aPending = (a.confirmations == 0); + bool bPending = (b.confirmations == 0); + if (aPending != bPending) return aPending; + if (a.timestamp != b.timestamp) return a.timestamp > b.timestamp; + return a.txid > b.txid; + } + } }); s_display_cache_key = displayKey; @@ -516,7 +546,7 @@ void RenderTransactionsTab(App* app) int totalPages = std::max(1, (filtered_count + perPage - 1) / perPage); // Reset page when filters change - int filterHash = type_filter * 1000003 + filtered_count * 31 + static_cast(search_str.size()); + int filterHash = type_filter * 1000003 + s_sort_mode * 7919 + filtered_count * 31 + static_cast(search_str.size()); if (filterHash != s_prev_filter_hash) { s_current_page = 0; s_prev_filter_hash = filterHash; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 97d3d57..a8d2e38 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -576,6 +576,10 @@ void I18n::loadBuiltinEnglish() strings_["sent_filter"] = "Sent"; strings_["received_filter"] = "Received"; strings_["mined_filter"] = "Mined"; + strings_["sort_date_newest"] = "Newest first"; + strings_["sort_date_oldest"] = "Oldest first"; + strings_["sort_amount_high"] = "Largest amount"; + strings_["sort_amount_low"] = "Smallest amount"; strings_["export_csv"] = "Export CSV"; strings_["transactions_upper"] = "TRANSACTIONS"; strings_["received_upper"] = "RECEIVED";