feat(history): view the transactions involving an address

Adds a "View transactions" action that opens the History tab pre-filtered
to a chosen address. SetTransactionsAddressFilter() stores a pending filter
that RenderTransactionsTab consumes on its next frame — pre-filling the
search box and resetting the type filter to All — then the caller navigates
to NavPage::History. This reuses the existing "prefill + navigate" pattern
(cf. SetSendFromAddress) and all of History's rendering, detail and export.

Triggers:
- Contacts: a context-menu item and a 4th per-row action icon (copy /
  view-transactions / edit / delete; the trailing width is widened to fit).
- Balance address list: a per-address context-menu item (own z/t addresses).
- Receive tab: the recent-received rows now carry their address into the
  filter instead of jumping to History unfiltered.

The History search also now matches from_address, so filtering by one of your
own addresses catches sends FROM it, not only receives INTO it.

Scoping is honest, not exhaustive: transparent is well covered, but shielded
counterparties are private and the transparent sender of received funds is
never recorded on-chain, so this shows transactions *involving* the address.
On Lite, from_address is thin, so own-address send filtering is weaker.

New i18n keys view_transactions / tt_view_transactions translated into all
eight locales (no new glyphs). Builds + test suites pass on both variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 10:13:15 -05:00
co-authored by Claude Opus 4.8
parent 5fcab14e4e
commit 2282f332a6
14 changed files with 78 additions and 9 deletions
+19 -9
View File
@@ -17,6 +17,8 @@
#include "../../util/texture_loader.h"
#include "../../util/platform.h"
#include "image_picker.h"
#include "transactions_tab.h"
#include "../sidebar.h"
#include "../layout.h"
#include "imgui.h"
#include <filesystem>
@@ -530,6 +532,11 @@ void RenderContactsTab(App* app)
s_show_edit_dialog = true;
s_focus_edit_field = true;
};
auto doViewTransactions = [&]() {
if (!selValid()) return;
SetTransactionsAddressFilter(book.entries()[s_selected_index].address);
app->setCurrentPage(ui::NavPage::History);
};
// Add/edit form — a modal popup layered over the tab, with a live contact preview and an avatar
// picker (default Z/T badge, a Material icon, or a custom image via the in-app image picker).
@@ -1367,7 +1374,7 @@ void RenderContactsTab(App* app)
// Label (line 1) + muted address (line 2) — reserve trailing room for the globe + actions.
const float actHit = icoF->LegacySize + 8.0f * dp; // per-action square hit area
const float globeW = entry.isGlobal() ? (icoF->LegacySize + 8.0f * dp) : 0.0f;
const float trailW = 3.0f * actHit + 6.0f * dp + globeW;
const float trailW = 4.0f * actHit + 8.0f * dp + globeW; // copy / view-tx / edit / delete
const float cy = mn.y + rowH * 0.5f;
const float tx = (s_contactsShape == 2) ? (mn.x + tabW + pad)
: (mn.x + pad + avR * 2.0f + pad);
@@ -1399,10 +1406,11 @@ void RenderContactsTab(App* app)
rightX -= gs.x + 8.0f * dp;
}
if (rowHovered || selected) {
const char* actGlyph[3] = { ICON_MD_CONTENT_COPY, ICON_MD_EDIT, ICON_MD_DELETE };
const char* actTip[3] = { TR("copy_address"), TR("edit"), TR("delete") };
// Order (left->right): copy, view-transactions, edit, delete. Drawn right-to-left.
const char* actGlyph[4] = { ICON_MD_CONTENT_COPY, ICON_MD_RECEIPT_LONG, ICON_MD_EDIT, ICON_MD_DELETE };
const char* actTip[4] = { TR("copy_address"), TR("view_transactions"), TR("edit"), TR("delete") };
float ax = rightX - actHit; // rightmost action (delete), just left of the globe
for (int a = 2; a >= 0; --a) {
for (int a = 3; a >= 0; --a) {
ImGui::SetCursorScreenPos(ImVec2(ax, cy - actHit * 0.5f));
ImGui::PushID(a + 100);
const bool aClk = ImGui::InvisibleButton("##act", ImVec2(actHit, actHit));
@@ -1411,12 +1419,13 @@ void RenderContactsTab(App* app)
if (aClk) {
s_selected_index = static_cast<int>(i);
if (a == 0) doCopy();
else if (a == 1) openEdit();
else if (a == 1) doViewTransactions();
else if (a == 2) openEdit();
// Delete mutates book.entries() (removeEntry -> erase) — deferring it past the
// row loop avoids iterating a vector we shrink mid-loop (OOB on later rows).
else rowDeleteRequested = true;
}
const bool armedDel = (a == 2 && s_confirm_delete_idx == static_cast<int>(i));
const bool armedDel = (a == 3 && s_confirm_delete_idx == static_cast<int>(i));
const ImU32 acol = armedDel ? material::ReadableError()
: aHov ? material::OnSurface()
: material::OnSurfaceMedium();
@@ -1454,10 +1463,11 @@ void RenderContactsTab(App* app)
// Shared right-click context menu (opened by either view's row right-click; acts on the selection).
if (openContextMenu) ImGui::OpenPopup("##contactCtx");
if (ImGui::BeginPopup("##contactCtx")) {
if (ImGui::MenuItem(TR("copy_address"))) doCopy();
if (ImGui::MenuItem(TR("edit"))) openEdit();
if (ImGui::MenuItem(TR("copy_address"))) doCopy();
if (ImGui::MenuItem(TR("view_transactions"))) doViewTransactions();
if (ImGui::MenuItem(TR("edit"))) openEdit();
ImGui::Separator();
if (ImGui::MenuItem(TR("delete"))) doDelete();
if (ImGui::MenuItem(TR("delete"))) doDelete();
ImGui::EndPopup();
}