Merge pull request #10991 from f321x/qml_history_calendar_sections
What changed, and why it matters
This commit is a user-interface improvement for the Electrum mobile/QML wallet's transaction history screen. It changes how transactions are grouped by date, adding sections like 'Earlier this week' and 'Earlier this month' and making the week start follow the user's locale. There is no security issue here.
No security action needed. This is a routine UI/UX change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors transaction history sectioning in the QML GUI. It introduces calendar-based grouping (today, yesterday, this week, last week, this month, last month, older) using QLocale.system().firstDayOfWeek() to determine week boundaries, and updates corresponding date formatting and unit tests. No cryptographic, networking, wallet, or permission-related code is touched.
Changed components
electrum/gui/qml/components/History.qmlelectrum/gui/qml/qetransactionlistmodel.pytests/qml/test_qml_qetransactionlistmodel.pyInspect captured patch +72 / −17
### electrum/gui/qml/components/History.qml
@@ -49,7 +49,9 @@ Pane {
'mempool': qsTr('Mempool'),
'today': qsTr('Today'),
'yesterday': qsTr('Yesterday'),
+ 'thisweek': qsTr('Earlier this week'),
'lastweek': qsTr('Last week'),
+ 'thismonth': qsTr('Earlier this month'),
'lastmonth': qsTr('Last month'),
'older': qsTr('Older')
}
### electrum/gui/qml/qetransactionlistmodel.py
@@ -2,7 +2,7 @@
from typing import TYPE_CHECKING, Dict, Any
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
-from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex
+from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex, QLocale
from electrum.logging import get_logger
from electrum.util import Satoshis, TxMinedInfo
@@ -168,14 +168,24 @@ def tx_to_model(self, tx_item):
def get_section_by_timestamp(timestamp):
txts = datetime.fromtimestamp(timestamp)
today = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
-
- if txts > today:
+ first_day_of_week = QLocale.system().firstDayOfWeek()
+ # Qt.DayOfWeek and isoweekday() both count Monday=1 .. Sunday=7
+ this_week_start = today - timedelta(days=(today.isoweekday() - first_day_of_week.value) % 7)
+ this_month_start = today.replace(day=1)
+ last_month_start = (this_month_start - timedelta(days=1)).replace(day=1)
+
+ # calendar based, the most specific section wins (e.g. on mondays sunday is 'yesterday', not 'lastweek')
+ if txts >= today:
return 'today'
- elif txts > today - timedelta(days=1):
+ elif txts >= today - timedelta(days=1):
return 'yesterday'
- elif txts > today - timedelta(days=7):
+ elif txts >= this_week_start:
+ return 'thisweek'
+ elif txts >= this_week_start - timedelta(days=7):
return 'lastweek'
- elif txts > today - timedelta(days=31):
+ elif txts >= this_month_start:
+ return 'thismonth'
+ elif txts >= last_month_start:
return 'lastmonth'
else:
return 'older'
@@ -186,7 +196,9 @@ def format_date_by_section(section: str, date: datetime):
dfmt = {
'today': '%H:%M',
'yesterday': '%H:%M',
- 'lastweek': '%a, %H:%M',
+ 'thisweek': '%a, %H:%M',
+ 'lastweek': '%a %d, %H:%M',
+ 'thismonth': '%a %d, %H:%M',
'lastmonth': '%a %d, %H:%M',
'older': '%Y-%m-%d %H:%M'
}
### tests/qml/test_qml_qetransactionlistmodel.py
@@ -1,6 +1,8 @@
from datetime import datetime
from unittest.mock import patch
+from PyQt6.QtCore import Qt
+
from electrum.gui.qml.qetransactionlistmodel import QETransactionListModel
from .. import ElectrumTestCase
@@ -11,41 +13,74 @@ class TestQETransactionListModel(ElectrumTestCase):
def test_get_section_by_timestamp(self):
f = QETransactionListModel.get_section_by_timestamp
- mock_today = datetime(2023, 6, 15, 0, 0, 0, 0)
- with patch('electrum.gui.qml.qetransactionlistmodel.datetime') as mock_dt:
+ mock_today = datetime(2023, 6, 15, 0, 0, 0, 0) # thursday
+ with patch('electrum.gui.qml.qetransactionlistmodel.datetime') as mock_dt, \
+ patch('electrum.gui.qml.qetransactionlistmodel.QLocale') as mock_locale:
mock_dt.today.return_value = mock_today
mock_dt.fromtimestamp = datetime.fromtimestamp
+ mock_first_day_of_week = mock_locale.system.return_value.firstDayOfWeek
+ mock_first_day_of_week.return_value = Qt.DayOfWeek.Monday
today_ts = datetime(2023, 6, 15, 10, 30, 0).timestamp()
self.assertEqual(f(today_ts), 'today')
- today_edge_ts = datetime(2023, 6, 15, 0, 0, 1).timestamp()
+ today_edge_ts = datetime(2023, 6, 15, 0, 0, 0).timestamp()
self.assertEqual(f(today_edge_ts), 'today')
yesterday_ts = datetime(2023, 6, 14, 15, 0, 0).timestamp()
self.assertEqual(f(yesterday_ts), 'yesterday')
yesterday_edge_ts = datetime(2023, 6, 13, 23, 59, 59).timestamp()
- self.assertEqual(f(yesterday_edge_ts), 'lastweek')
+ self.assertEqual(f(yesterday_edge_ts), 'thisweek')
+
+ thisweek_boundary_ts = datetime(2023, 6, 12, 0, 0, 0).timestamp()
+ self.assertEqual(f(thisweek_boundary_ts), 'thisweek')
- lastweek_ts = datetime(2023, 6, 12, 12, 0, 0).timestamp()
+ lastweek_ts = datetime(2023, 6, 11, 23, 59, 59).timestamp()
self.assertEqual(f(lastweek_ts), 'lastweek')
- lastweek_boundary_ts = datetime(2023, 6, 8, 12, 0, 0).timestamp()
+ lastweek_boundary_ts = datetime(2023, 6, 5, 0, 0, 0).timestamp()
self.assertEqual(f(lastweek_boundary_ts), 'lastweek')
- lastmonth_ts = datetime(2023, 6, 5, 9, 0, 0).timestamp()
+ thismonth_ts = datetime(2023, 6, 4, 23, 59, 59).timestamp()
+ self.assertEqual(f(thismonth_ts), 'thismonth')
+
+ thismonth_boundary_ts = datetime(2023, 6, 1, 0, 0, 0).timestamp()
+ self.assertEqual(f(thismonth_boundary_ts), 'thismonth')
+
+ lastmonth_ts = datetime(2023, 5, 31, 23, 59, 59).timestamp()
self.assertEqual(f(lastmonth_ts), 'lastmonth')
- lastmonth_boundary_ts = datetime(2023, 5, 15, 8, 0, 0).timestamp()
+ lastmonth_boundary_ts = datetime(2023, 5, 1, 0, 0, 0).timestamp()
self.assertEqual(f(lastmonth_boundary_ts), 'lastmonth')
- older_ts = datetime(2023, 5, 14, 10, 0, 0).timestamp()
+ older_ts = datetime(2023, 4, 30, 23, 59, 59).timestamp()
self.assertEqual(f(older_ts), 'older')
much_older_ts = datetime(2022, 1, 1, 0, 0, 0).timestamp()
self.assertEqual(f(much_older_ts), 'older')
+ # the week boundaries follow the locale's first day of the week
+ sunday_ts = datetime(2023, 6, 11, 12, 0, 0).timestamp()
+ prev_sunday_ts = datetime(2023, 6, 4, 12, 0, 0).timestamp()
+ prev_saturday_ts = datetime(2023, 6, 3, 12, 0, 0).timestamp()
+ self.assertEqual(f(sunday_ts), 'lastweek')
+ self.assertEqual(f(prev_sunday_ts), 'thismonth')
+ mock_first_day_of_week.return_value = Qt.DayOfWeek.Sunday
+ self.assertEqual(f(sunday_ts), 'thisweek')
+ self.assertEqual(f(prev_sunday_ts), 'lastweek')
+ self.assertEqual(f(prev_saturday_ts), 'thismonth')
+ mock_first_day_of_week.return_value = Qt.DayOfWeek.Saturday
+ self.assertEqual(f(sunday_ts), 'thisweek')
+ self.assertEqual(f(prev_saturday_ts), 'lastweek')
+
+ # the more specific section wins where weeks and months overlap
+ mock_first_day_of_week.return_value = Qt.DayOfWeek.Monday
+ mock_dt.today.return_value = datetime(2023, 5, 2, 0, 0, 0, 0) # tuesday
+ self.assertEqual(f(datetime(2023, 5, 1, 12, 0, 0).timestamp()), 'yesterday')
+ self.assertEqual(f(datetime(2023, 4, 30, 12, 0, 0).timestamp()), 'lastweek')
+ self.assertEqual(f(datetime(2023, 4, 23, 12, 0, 0).timestamp()), 'lastmonth')
+
def test_format_date_by_section(self):
f = QETransactionListModel.format_date_by_section
@@ -57,9 +92,15 @@ def test_format_date_by_section(self):
result = f('yesterday', test_date)
self.assertEqual(result, '14:30')
- result = f('lastweek', test_date)
+ result = f('thisweek', test_date)
self.assertEqual(result, 'Thu, 14:30')
+ result = f('lastweek', test_date)
+ self.assertEqual(result, 'Thu 15, 14:30')
+
+ result = f('thismonth', test_date)
+ self.assertEqual(result, 'Thu 15, 14:30')
+
result = f('lastmonth', test_date)
self.assertEqual(result, 'Thu 15, 14:30')
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.