Skip to content

Activity: advanced filters, edited request labels, and used-address requests - #817

Open
epicleafies wants to merge 7 commits into
bitcoin-core:qt6from
epicleafies:advanced-activity-filters
Open

Activity: advanced filters, edited request labels, and used-address requests#817
epicleafies wants to merge 7 commits into
bitcoin-core:qt6from
epicleafies:advanced-activity-filters

Conversation

@epicleafies

@epicleafies epicleafies commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

This PR closes three related gaps in the Activity page. The list filters now reach parity with the Qt Widgets transaction view and payment request labels and used-address requests behave correctly. Fixes #707. Fixes #725. Fixes #726.

  • Added an Other transaction type, a minimum amount, a Last month date preset as well as a custom date range as Activity filter options
  • Added a Grid-based calendar component for the custom date range
  • Updated the activity page search and filter options layout and kept filter popups within the list area
  • Made payment request labels stay in sync both ways: editing a request keeps its label in the Activity row and writes it back to the address book, and editing an address label updates the matching request, its Activity row, and any detail page or editor already open on it
  • Fixed behavior for used-address payment requests. They now use the green receive triangle and "Received" text and only appear in the Activity list under the Payment request filter

Added unit, QML and functional tests.

image image image

Editing a saved payment request updated its label everywhere except the
pending request row in Activity, which kept showing the old label.

The Activity list refreshes a row's label on every read by looking the
row's address up in the wallet address book. For a pending receive
request that overwrote the label the request itself carries (set when the
request is saved and updated when it is edited) with the older stored
address book label, so an edited label was lost on the next render.

Skip the address book lookup for pending requests so they keep their own
label, mirroring how the status refresh already skips pending requests.
Qt Widgets distinguishes a dedicated Other transaction type and a Last
month date preset in its transaction filters. The QML Activity filters
folded Transaction::Other into Sent and had no Last month preset, so
those rows could not be isolated.

Classify Transaction::Other (and any unrecognized type) as its own Other
filter rather than Sent, matching the label already used for CSV export,
and add a Last month date bucket spanning the previous calendar month.
Both are surfaced in the Activity type and date filter menus.

The minimum amount and custom date range filters Qt Widgets also offers
are left for a follow-up, as they need filter input controls that the
Activity design does not yet define.
Qt Widgets offers a minimum amount filter and a custom date range in its
transaction filters; the QML Activity filters had neither.

Add a minimum amount filter: a new Amount button opens a popup with an
amount field in the active display unit plus Apply and Reset. The button
and the field show the unit the same way as the rest of the app (the
BitcoinAmount unit label: a bitcoin sign or sat/sats). The proxy filters
on absolute satoshi value, so a send and a receive of the same size both
pass a threshold, matching the Qt Widgets behaviour.

Add a custom date range: the date menu gains a Custom range entry that
switches the popup to a calendar. Qt has no stock date picker, so the day
grid is built from ItemDelegate cells in a Grid positioner (never a
Repeater inside a Quick Layout, which would hit a Qt 6.4 use-after-free).
The first click sets the start and the next sets the end (reordered if
earlier); the range then locks until Reset clears it. Endpoints, the days
between, and today are marked, with today carrying weight as well as a
ring so the state is not conveyed by color alone. Month and year steppers
use caret icons from the Bitcoin Icons set (the set has no double-caret,
so the year jump composes two carets); the week starts on the locale's
first day. The day cells, steppers, and the presets link carry Accessible
names and roles.

The proxy gains a CustomRange bucket whose bounds include both endpoints.
Moving dates across the QML/C++ boundary needs care in both directions
because a JavaScript Date and a QDate disagree on the day across time
zones: the range is applied through an ISO-string invokable rather than
by assigning the QDate properties from QML, and when the popup reopens on
an applied range the QDate read back (a UTC-midnight JavaScript Date) is
rebuilt at local midnight before it reseeds the calendar, so a reopen
does not shift the applied range west of UTC.

With a fourth control added, the search field moves to its own row and
the date, type, and amount buttons sit in a row beneath it, so they no
longer compete with the search field for one row's width.

Both apply to the visible list and the CSV export and clear with the
existing reset path. The new minAmount, rangeStart and rangeEnd proxy
properties are covered by C++ unit tests, the apply paths by QML tests
driving the amount field and the calendar (including a reopen round-trip
that guards the timezone reseed), and the whole flow by the Activity
filter functional test; the filtered-empty hint now mentions the amount
filter.
A saved receive request whose address already had a real transaction was
skipped by ActivityListModel::addPendingReceiveRequests(), so it never
became an Activity row. The Payment request filter could therefore not
surface it, even though the transaction details for that address already
list the matching requests.

Materialize those used-address requests as rows as well, flagged with a
new isUsedAddressRequest marker. The proxy shows a used-address request
only when the Payment request filter is selected, and hides it in every
other view so it does not duplicate the address's real transaction row.

Apply the same rule live: when a payment arrives for a pending request,
keep the request as a used-address request and add the received
transaction as its own row, rather than consuming the request in place.
Without this, a fulfilled request dropped out of the Payment request
filter until the wallet was reloaded; now the filter is consistent
whether the payment just arrived or the wallet was restarted. Used-address
requests are not tracked for pending-request fulfillment, since they have
no unfulfilled payment left to wait for.

The visibility rule is covered by a proxy unit test (hidden by default
and under other type filters, shown under the Payment request filter),
the used-address flag by a WalletQmlModel unit test, and the live
fulfillment path by a WalletQmlModel test that drives a received
transaction and asserts the request stays a used-address request while
the transaction appears as its own row.
Editing a payment request updated the request record and the Activity row,
but not the address's address book label, so the Addresses page kept
showing the label the address was created with. Creating a request already
labels its address (getNewDestination writes the address book), so the two
only diverged on an edit.

Write the request label back to the address book whenever a request is
saved, so the Addresses page and any other address-book reader reflect an
edited request rather than a stale label.
The previous commit made request label edits flow to the address book, but
the reverse still diverged: editing a label on the Addresses page wrote only
the address book, leaving the payment request and its Activity row showing the
old label.

Propagate the edit the other way too. When setAddressLabel writes an address
book label, update any payment request saved for that address to match: rewrite
the stored request, its receive-request history entry, and its Activity row.
setAddressLabel is the shared path both the Addresses page and the request-save
sync go through, so both edits now converge on one label. The guard skips a
request already carrying the new label, so the request-save sync (which calls
setAddressLabel) does not redundantly rewrite what it just stored.

Covered by a WalletQmlModel unit test that edits an address label and asserts
the request record and Activity row follow, and a functional test that renames
a request's address on the Addresses page and checks both the address row and
the Activity row show the new label.
Editing an address label on the Addresses page rewrites the stored payment
request, its history entry, and its Activity row, but a request already open
in the Activity detail page or in the locked Receive editor kept showing the
old label until it was closed and reopened. The wallet tabs live in a
StackLayout that keeps every page alive, so those views hold the shared
detailPaymentRequest and currentPaymentRequest objects that were filled when
the view loaded, and the reverse sync never touched them.

Refresh the held objects from syncPaymentRequestLabelToAddress. The editor
object is skipped while a request is being edited so the sync cannot clobber
an unsaved draft; the label the user saves wins in that case.

The detail page follows automatically through its label bindings. The
editor name field does not: any assignment to a TextField text property
(typing, or the isEditing re-sync handler) replaces its declarative binding,
so the field re-syncs from the labelChanged signal instead, again only while
the form is locked.

Covered by a unit test asserting both held objects follow an address label
edit and that a draft survives one, a QML test asserting the locked editor
field follows an external label change, and a functional assertion that the
held-open editor shows the edited label after the address is renamed on the
Addresses page.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant