fix(@desktop/wallet): fix scrolling transactions list reset to top when

new ones are fetched.

Fixes #9617
This commit is contained in:
Ivan Belyakov 2023-03-23 10:34:05 +03:00 committed by IvanBelyakoff
parent 172c849c54
commit 89055e1b08
2 changed files with 114 additions and 52 deletions

View File

@ -43,12 +43,10 @@ QtObject:
type
Model* = ref object of QAbstractListModel
items: seq[Item]
itemsWithDateHeaders: seq[Item]
hasMore: bool
proc delete(self: Model) =
self.items = @[]
self.itemsWithDateHeaders = @[]
self.QAbstractListModel.delete
proc setup(self: Model) =
@ -60,20 +58,20 @@ QtObject:
result.hasMore = true
proc `$`*(self: Model): string =
for i in 0 ..< self.itemsWithDateHeaders.len:
result &= fmt"""[{i}]:({$self.itemsWithDateHeaders[i]})"""
for i in 0 ..< self.items.len:
result &= fmt"""[{i}]:({$self.items[i]})"""
proc countChanged(self: Model) {.signal.}
proc getCount*(self: Model): int {.slot.} =
self.itemsWithDateHeaders.len
self.items.len
QtProperty[int] count:
read = getCount
notify = countChanged
method rowCount(self: Model, index: QModelIndex = nil): int =
return self.itemsWithDateHeaders.len
return self.items.len
method roleNames(self: Model): Table[int, string] =
{
@ -114,10 +112,10 @@ QtObject:
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.itemsWithDateHeaders.len):
if (index.row < 0 or index.row >= self.items.len):
return
let item = self.itemsWithDateHeaders[index.row]
let item = self.items[index.row]
let enumRole = role.ModelRole
case enumRole:
@ -186,14 +184,14 @@ QtObject:
proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel()
self.itemsWithDateHeaders = items
self.items = items
self.endResetModel()
self.countChanged()
proc getLastTxBlockNumber*(self: Model): string {.slot.} =
if (self.itemsWithDateHeaders.len == 0):
if (self.items.len == 0):
return "0x0"
return self.itemsWithDateHeaders[^1].getBlockNumber()
return self.items[^1].getBlockNumber()
proc hasMoreChanged*(self: Model) {.signal.}
@ -219,43 +217,49 @@ QtObject:
result = cmp(x.getNonce(), y.getNonce())
proc addNewTransactions*(self: Model, transactions: seq[Item], wasFetchMore: bool) =
let existingTxIds = self.items.map(tx => tx.getId())
let hasNewTxs = transactions.len > 0 and transactions.any(tx => not existingTxIds.contains(tx.getId()))
if transactions.len == 0:
return
if hasNewTxs or not wasFetchMore:
var allTxs = self.items.concat(transactions)
allTxs.sort(cmpTransactions, SortOrder.Descending)
eth_service_utils.deduplicate(allTxs, tx => tx.getTxHash())
var txs = transactions
# add day headers to the transaction list
var itemsWithDateHeaders: seq[Item] = @[]
var tempTimeStamp: Time
for tx in allTxs:
let durationInDays = (tempTimeStamp.toTimeInterval() - fromUnix(tx.getTimestamp()).toTimeInterval()).days
if(durationInDays != 0):
itemsWithDateHeaders.add(initTimestampItem(tx.getTimestamp()))
itemsWithDateHeaders.add(tx)
tempTimeStamp = fromUnix(tx.getTimestamp())
# Reset the model if empty
if self.items.len == 0:
eth_service_utils.deduplicate(txs, tx => tx.getTxHash())
self.setItems(txs)
return
self.items = allTxs
self.setItems(itemsWithDateHeaders)
self.setHasMore(true)
# Concatenate existing and new, filter out duplicates
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
var newItems = concat(self.items, txs)
eth_service_utils.deduplicate(newItems, tx => tx.getTxHash())
# Though we claim that we insert rows to preserve listview indices without
# model reset, we actually reset the list, but since old items order is not changed
# by deduplicate() call, the order is preserved and only new items are added
# to the end. For model it looks like we inserted.
# Unsorted, sorting is done on UI side
self.beginInsertRows(parentModelIndex, self.items.len, newItems.len - 1)
self.items = newItems
self.endInsertRows()
self.countChanged()
proc addPageSizeBuffer*(self: Model, pageSize: int) =
if pageSize > 0:
var itemsWithDateHeaders: seq[Item] = @[]
itemsWithDateHeaders.add(initTimestampItem(0))
self.beginInsertRows(newQModelIndex(), self.items.len, self.items.len + pageSize - 1)
for i in 0 ..< pageSize:
self.beginInsertRows(newQModelIndex(), self.itemsWithDateHeaders.len, self.itemsWithDateHeaders.len)
self.itemsWithDateHeaders.add(initLoadingItem())
self.items.add(initLoadingItem())
self.endInsertRows()
self.countChanged()
proc removePageSizeBuffer*(self: Model) =
for i in 0 ..< self.itemsWithDateHeaders.len:
if self.itemsWithDateHeaders[i].getLoadingTransaction():
self.beginRemoveRows(newQModelIndex(), i, self.itemsWithDateHeaders.len-1)
self.itemsWithDateHeaders.delete(i, self.itemsWithDateHeaders.len-1)
var removed = false
for i in 0 ..< self.items.len:
if self.items[i].getLoadingTransaction():
self.beginRemoveRows(newQModelIndex(), i, self.items.len-1)
self.items.delete(i, self.items.len-1)
self.endRemoveRows()
self.countChanged()
return

View File

@ -7,6 +7,8 @@ import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import SortFilterProxyModel 0.2
import utils 1.0
import "../panels"
@ -69,32 +71,88 @@ ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
model: RootStore.historyTransactions
property string firstSection
model: SortFilterProxyModel {
id: txModel
sourceModel: RootStore.historyTransactions
// LocaleUtils is not accessable from inside expression, but local function works
property var formatDate: (ms) => LocaleUtils.formatDate(ms, Locale.ShortFormat)
sorters: RoleSorter {
roleName: "timestamp"
sortOrder: Qt.DescendingOrder
}
proxyRoles: ExpressionRole {
name: "date"
expression: {
return timestamp > 0 ? txModel.formatDate(timestamp * 1000) : ""
}
}
}
delegate: Loader {
width: ListView.view.width
sourceComponent: isTimeStamp ? dateHeader : transactionDelegate
sourceComponent: transactionDelegate
onLoaded: {
item.modelData = model
if (index == 0)
ListView.view.firstSection = date
}
}
footer: footerComp
displaced: Transition {
NumberAnimation { properties: "y"; duration: 250; easing.type: Easing.Linear; alwaysRunToEnd: true }
}
readonly property point lastVisibleItemPos: Qt.point(0, contentY + height - 1)
property int lastVisibleIndex: indexAt(lastVisibleItemPos.x, lastVisibleItemPos.y)
onCountChanged: {
const lastVisibleItem = itemAtIndex(lastVisibleIndex)
const newItem = itemAt(lastVisibleItemPos.x, lastVisibleItemPos.y)
const lastVisibleItemY = lastVisibleItem ? lastVisibleItem.y : -1
if (newItem) {
if (newItem.y < lastVisibleItemY) { // item inserted higher than last visible
lastVisibleIndex = indexAt(lastVisibleItemPos.x, lastVisibleItemPos.y)
}
}
}
currentIndex: 0
property bool userScrolled: false
onMovingVerticallyChanged: {
if (!userScrolled) {
userScrolled = true
currentIndex = Qt.binding(() => lastVisibleIndex >= 0 ? lastVisibleIndex : (count - 1))
}
lastVisibleIndex = indexAt(lastVisibleItemPos.x, lastVisibleItemPos.y)
}
ScrollBar.vertical: StatusScrollBar {}
onAtYEndChanged: if(atYEnd && RootStore.historyTransactions.hasMore) fetchHistory()
}
section.property: "date"
section.delegate: Item {
width: ListView.view.width
height: ListView.view.firstSection === section || section.length > 0 ? 40 : 0
visible: height > 0
Component {
id: dateHeader
StatusListItem {
property var modelData
height: 40
title: modelData !== undefined && !!modelData ? LocaleUtils.formatDate(modelData.timestamp * 1000, Locale.ShortFormat) : ""
statusListItemTitle.color: Theme.palette.baseColor1
color: Theme.palette.statusListItem.backgroundColor
sensor.enabled: false
required property string section
StatusBaseText {
anchors.verticalCenter: parent.verticalCenter
text: parent.section
color: Theme.palette.baseColor1
font.pixelSize: 15
}
}
onAtYEndChanged: if (atYEnd && RootStore.historyTransactions.hasMore) fetchHistory()
}
Component {
id: transactionDelegate