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:
@@ -13,6 +13,7 @@
|
||||
#include "address_transfer_dialog.h"
|
||||
#include "mining_tab_helpers.h"
|
||||
#include "send_tab.h"
|
||||
#include "transactions_tab.h"
|
||||
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
@@ -693,6 +694,10 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
SetSendFromAddress(addr.address);
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("view_transactions"))) {
|
||||
SetTransactionsAddressFilter(addr.address);
|
||||
app->setCurrentPage(NavPage::History);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(TR("set_label"))) {
|
||||
AddressLabelDialog::show(app, addr.address, row.isZ);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../../wallet/lite_wallet_controller.h"
|
||||
#include "../../ui/widgets/qr_code.h"
|
||||
#include "../sidebar.h"
|
||||
#include "transactions_tab.h"
|
||||
#include "../layout.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
@@ -535,6 +536,7 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
|
||||
S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0)) {
|
||||
if (!tx.address.empty()) SetTransactionsAddressFilter(tx.address);
|
||||
app->setCurrentPage(ui::NavPage::History);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,17 @@ static bool containsIgnoreCase(const std::string& str, const std::string& search
|
||||
return dragonx::util::containsIgnoreCase(str, search);
|
||||
}
|
||||
|
||||
// Pending "view transactions for this address" request. Set by SetTransactionsAddressFilter()
|
||||
// from Contacts / the Balance address list / the Receive tab, consumed on the next
|
||||
// RenderTransactionsTab frame to pre-fill the search box (see the consume block below).
|
||||
static std::string s_pending_addr_filter;
|
||||
static bool s_pending_addr_filter_set = false;
|
||||
|
||||
void SetTransactionsAddressFilter(const std::string& address) {
|
||||
s_pending_addr_filter = address;
|
||||
s_pending_addr_filter_set = true;
|
||||
}
|
||||
|
||||
// A display-ready transaction that may be a merged autoshield pair.
|
||||
// For non-merged entries, send_idx or recv_idx is -1.
|
||||
struct DisplayTx {
|
||||
@@ -313,6 +324,15 @@ void RenderTransactionsTab(App* app)
|
||||
// Search & Filter bar
|
||||
// ================================================================
|
||||
static char search_filter[128] = "";
|
||||
// Consume a pending "view transactions for this address" request: pre-fill the search box
|
||||
// with the address and reset the type filter to All so no matching row is hidden. The
|
||||
// changed search_str resets pagination/expansion via filterInputsChanged() below.
|
||||
if (s_pending_addr_filter_set) {
|
||||
strncpy(search_filter, s_pending_addr_filter.c_str(), sizeof(search_filter) - 1);
|
||||
search_filter[sizeof(search_filter) - 1] = '\0';
|
||||
type_filter = 0;
|
||||
s_pending_addr_filter_set = false;
|
||||
}
|
||||
float searchMaxW = (searchInput.maxWidth >= 0) ? searchInput.maxWidth : 300.0f;
|
||||
float searchRatio = (searchInput.widthRatio >= 0) ? searchInput.widthRatio : 0.30f;
|
||||
float searchWidth = std::min(searchMaxW * hs, availWidth * searchRatio);
|
||||
@@ -530,6 +550,7 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
if (!search_str.empty()) {
|
||||
if (!containsIgnoreCase(dtx.address, search_str) &&
|
||||
!containsIgnoreCase(dtx.from_address, search_str) &&
|
||||
!containsIgnoreCase(dtx.txid, search_str) &&
|
||||
!containsIgnoreCase(dtx.memo, search_str) &&
|
||||
!(dtx.is_shield && containsIgnoreCase(std::string("shielded"), search_str))) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
class App;
|
||||
|
||||
@@ -15,5 +17,16 @@ namespace ui {
|
||||
*/
|
||||
void RenderTransactionsTab(App* app);
|
||||
|
||||
/**
|
||||
* @brief Pre-filter the Transactions (History) tab by an address.
|
||||
*
|
||||
* Sets the History search box to @p address and resets the type filter to All on the next
|
||||
* render, then the caller navigates to the History tab. Used by the "View transactions"
|
||||
* actions in Contacts, the Balance address list, and the Receive tab. Because shielded /
|
||||
* received-from attribution is incomplete, this shows transactions *involving* the address,
|
||||
* not a guaranteed-complete set.
|
||||
*/
|
||||
void SetTransactionsAddressFilter(const std::string& address);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -1195,6 +1195,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mature"] = "Mature";
|
||||
strings_["copy_txid"] = "Copy TxID";
|
||||
strings_["view_details"] = "View Details";
|
||||
strings_["view_transactions"] = "View Transactions";
|
||||
strings_["tt_view_transactions"] = "Show transactions involving this address in the History tab";
|
||||
strings_["full_details"] = "Full Details";
|
||||
strings_["showing_transactions"] = "Showing %d\xe2\x80\x93%d of %d transactions (total: %zu)";
|
||||
strings_["txs_count"] = "%d txs";
|
||||
|
||||
Reference in New Issue
Block a user