fix(@desktop/wallet): Remove the 'Load More' button from the Activity view and replace with automatic loading when the user scrolls down using a skeleton loading state

fixes #8987
This commit is contained in:
Khushboo Mehta 2023-01-13 00:26:48 +01:00 committed by Khushboo-dev-cpp
parent e8ed91b3ba
commit 2dbf2d4635
13 changed files with 204 additions and 81 deletions

View File

@ -82,7 +82,7 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_TRANSACTION_LOADING_COMPLETED_FOR_ALL_NETWORKS) do(e:Args):
let args = TransactionsLoadedArgs(e)
self.delegate.setHistoryFetchState(args.address, isFetching = false)
self.delegate.setHistoryFetchState(args.address, args.allTxLoaded, isFetching = false)
self.events.on(SIGNAL_CURRENCY_FORMATS_UPDATED) do(e:Args):
# TODO: Rebuild Transaction items

View File

@ -34,7 +34,7 @@ method setTrxHistoryResult*(self: AccessInterface, transactions: seq[Transaction
method setHistoryFetchState*(self: AccessInterface, addresses: seq[string], isFetching: bool) {.base.} =
raise newException(ValueError, "No implementation available")
method setHistoryFetchState*(self: AccessInterface, address: string, isFetching: bool) {.base.} =
method setHistoryFetchState*(self: AccessInterface, address: string, allTxLoaded: bool, isFetching: bool) {.base.} =
raise newException(ValueError, "No implementation available")
method setIsNonArchivalNode*(self: AccessInterface, isNonArchivalNode: bool) {.base.} =

View File

@ -29,6 +29,7 @@ type
totalFees: CurrencyAmount
maxTotalFees: CurrencyAmount
symbol: string
loadingTransaction: bool
proc initItem*(
id: string,
@ -56,7 +57,8 @@ proc initItem*(
baseGasFees: CurrencyAmount,
totalFees: CurrencyAmount,
maxTotalFees: CurrencyAmount,
symbol: string
symbol: string,
loadingTransaction: bool = false
): Item =
result.id = id
result.typ = typ
@ -84,6 +86,7 @@ proc initItem*(
result.totalFees = totalFees
result.maxTotalFees = maxTotalFees
result.symbol = symbol
result.loadingTransaction = loadingTransaction
proc initTimestampItem*(timestamp: int): Item =
result.timestamp = timestamp
@ -98,6 +101,20 @@ proc initTimestampItem*(timestamp: int): Item =
result.totalFees = newCurrencyAmount()
result.maxTotalFees = newCurrencyAmount()
proc initLoadingItem*(): Item =
result.timestamp = 0
result.gasPrice = newCurrencyAmount()
result.value = newCurrencyAmount()
result.chainId = 0
result.maxFeePerGas = newCurrencyAmount()
result.maxPriorityFeePerGas = newCurrencyAmount()
result.multiTransactionID = 0
result.isTimeStamp = false
result.baseGasFees = newCurrencyAmount()
result.totalFees = newCurrencyAmount()
result.maxTotalFees = newCurrencyAmount()
result.loadingTransaction = true
proc `$`*(self: Item): string =
result = fmt"""AllTokensItem(
id: {self.id},
@ -126,6 +143,7 @@ proc `$`*(self: Item): string =
totalFees: {self.totalFees},
maxTotalFees: {self.maxTotalFees},
symbol: {self.symbol},
loadingTransaction: {self.loadingTransaction},
]"""
proc getId*(self: Item): string =
@ -205,3 +223,6 @@ proc getMaxTotalFees*(self: Item): CurrencyAmount =
proc getSymbol*(self: Item): string =
return self.symbol
proc getLoadingTransaction*(self: Item): bool =
return self.loadingTransaction

View File

@ -31,6 +31,7 @@ type
TotalFees
MaxTotalFees
Symbol
LoadingTransaction
QtObject:
type
@ -95,7 +96,8 @@ QtObject:
ModelRole.BaseGasFees.int: "baseGasFees",
ModelRole.TotalFees.int: "totalFees",
ModelRole.MaxTotalFees.int: "maxTotalFees",
ModelRole.Symbol.int: "symbol"
ModelRole.Symbol.int: "symbol",
ModelRole.LoadingTransaction.int: "loadingTransaction"
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -161,6 +163,8 @@ QtObject:
result = newQVariant(item.getMaxTotalFees())
of ModelRole.Symbol:
result = newQVariant(item.getSymbol())
of ModelRole.LoadingTransaction:
result = newQVariant(item.getLoadingTransaction())
proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel()
@ -185,7 +189,7 @@ QtObject:
QtProperty[bool] hasMore:
read = getHasMore
write = setHasMore
notify = currentTransactionsChanged
notify = hasMoreChanged
proc cmpTransactions*(x, y: Item): int =
# Sort proc to compare transactions from a single account.
@ -218,3 +222,22 @@ QtObject:
self.items = allTxs
self.setItems(itemsWithDateHeaders)
self.setHasMore(true)
proc addPageSizeBuffer*(self: Model, pageSize: int) =
if pageSize > 0:
var itemsWithDateHeaders: seq[Item] = @[]
itemsWithDateHeaders.add(initTimestampItem(0))
for i in 0 ..< pageSize:
self.beginInsertRows(newQModelIndex(), self.itemsWithDateHeaders.len, self.itemsWithDateHeaders.len)
self.itemsWithDateHeaders.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)
self.endRemoveRows()
self.countChanged()
return

View File

@ -115,8 +115,8 @@ method setTrxHistoryResult*(self: Module, transactions: seq[TransactionDto], add
method setHistoryFetchState*(self: Module, addresses: seq[string], isFetching: bool) =
self.view.setHistoryFetchStateForAccounts(addresses, isFetching)
method setHistoryFetchState*(self: Module, address: string, isFetching: bool) =
self.view.setHistoryFetchState(address, isFetching)
method setHistoryFetchState*(self: Module, address: string, allTxLoaded: bool, isFetching: bool) =
self.view.setHistoryFetchState(address, allTxLoaded, isFetching)
method setIsNonArchivalNode*(self: Module, isNonArchivalNode: bool) =
self.view.setIsNonArchivalNode(isNonArchivalNode)

View File

@ -48,15 +48,16 @@ QtObject:
proc loadingTrxHistoryChanged*(self: View, isLoading: bool, address: string) {.signal.}
proc setHistoryFetchState*(self: View, address: string, isFetching: bool) =
proc setHistoryFetchState*(self: View, address: string, allTxLoaded: bool, isFetching: bool) =
if self.models.hasKey(address):
if not isFetching:
self.models[address].removePageSizeBuffer()
elif isFetching and self.models[address].getCount() == 0:
self.models[address].addPageSizeBuffer(20)
self.models[address].setHasMore(not allTxLoaded)
self.fetchingHistoryState[address] = isFetching
self.loadingTrxHistoryChanged(isFetching, address)
proc setHistoryFetchState*(self: View, accounts: seq[string], isFetching: bool) =
for acc in accounts:
self.fetchingHistoryState[acc] = isFetching
self.loadingTrxHistoryChanged(isFetching, acc)
proc isFetchingHistory*(self: View, address: string): bool {.slot.} =
if self.fetchingHistoryState.hasKey(address):
return self.fetchingHistoryState[address]
@ -66,7 +67,9 @@ QtObject:
return self.model.getCount() > 0
proc loadTransactionsForAccount*(self: View, address: string, toBlock: string = "0x0", limit: int = 20, loadMore: bool = false) {.slot.} =
self.setHistoryFetchState(address, true)
if self.models.hasKey(address):
self.setHistoryFetchState(address, allTxLoaded=not self.models[address].getHasMore(), isFetching=true)
self.models[address].addPageSizeBuffer(limit)
self.delegate.loadTransactions(address, toBlock, limit, loadMore)
proc setTrxHistoryResult*(self: View, transactions: seq[Item], address: string, wasFetchMore: bool) =
@ -74,10 +77,15 @@ QtObject:
self.models[address] = newModel()
self.models[address].addNewTransactions(transactions, wasFetchMore)
if self.fetchingHistoryState.hasKey(address) and self.fetchingHistoryState[address] and wasFetchMore:
self.models[address].addPageSizeBuffer(transactions.len)
proc setHistoryFetchStateForAccounts*(self: View, addresses: seq[string], isFetching: bool) =
for address in addresses:
self.setHistoryFetchState(address, isFetching)
if self.models.hasKey(address):
self.setHistoryFetchState(address, allTxLoaded = not self.models[address].getHasMore(), isFetching)
else:
self.setHistoryFetchState(address, allTxLoaded = false, isFetching)
proc setModel*(self: View, address: string) {.slot.} =
if not self.models.hasKey(address):
@ -165,4 +173,4 @@ QtObject:
let fromAddress = tx.getfrom()
if not self.models.hasKey(fromAddress):
self.models[fromAddress] = newModel()
self.models[fromAddress].addNewTransactions(@[tx], false)
self.models[fromAddress].addNewTransactions(@[tx], wasFetchMore=false)

View File

@ -17,16 +17,19 @@ type
toBlock: Uint256
limit: int
loadMore: bool
allTxLoaded: bool
const loadTransactionsTask*: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let
arg = decode[LoadTransactionsTaskArg](argEncoded)
limitAsHex = "0x" & eth_utils.stripLeadingZeros(arg.limit.toHex)
response = transactions.getTransfersByAddress(arg.chainId, arg.address, arg.toBlock, limitAsHex, arg.loadMore).result
output = %*{
"address": arg.address,
"chainId": arg.chainId,
"history": transactions.getTransfersByAddress(arg.chainId, arg.address, arg.toBlock, limitAsHex, arg.loadMore),
"history": response,
"loadMore": arg.loadMore,
"allTxLoaded": response.getElems().len < arg.limit
}
arg.finish(output)

View File

@ -70,6 +70,8 @@ type
transactions*: seq[TransactionDto]
address*: string
wasFetchMore*: bool
allTxLoaded*: bool
tempLoadingTx*: int
type
TransactionSentArgs* = ref object of Args
@ -96,6 +98,7 @@ QtObject:
settingsService: settings_service.Service
tokenService: token_service.Service
txCounter: Table[string, seq[int]]
allTxLoaded: Table[string, bool]
# Forward declaration
proc loadTransactions*(self: Service, address: string, toBlock: Uint256, limit: int = 20, loadMore: bool = false)
@ -118,6 +121,7 @@ QtObject:
result.settingsService = settingsService
result.tokenService = tokenService
result.txCounter = initTable[string, seq[int]]()
result.allTxLoaded = initTable[string, bool]()
proc init*(self: Service) =
signalConnect(singletonInstance.localAccountSensitiveSettings, "isWalletEnabledChanged()", self, "onIsWalletEnabledChanged()", 2)
@ -202,10 +206,16 @@ QtObject:
let address = historyData["address"].getStr
let chainID = historyData["chainId"].getInt
let wasFetchMore = historyData["loadMore"].getBool
let allTxLoaded = historyData["allTxLoaded"].getBool
var transactions: seq[TransactionDto] = @[]
for tx in historyData["history"]["result"].getElems():
for tx in historyData["history"].getElems():
transactions.add(tx.toTransactionDto())
if self.allTxLoaded.hasKey(address):
self.allTxLoaded[address] = self.allTxLoaded[address] and allTxLoaded
else:
self.allTxLoaded[address] = allTxLoaded
# emit event
self.events.emit(SIGNAL_TRANSACTIONS_LOADED, TransactionsLoadedArgs(
transactions: transactions,
@ -214,16 +224,18 @@ QtObject:
))
# when requests for all networks are completed then set loading state as completed
if self.txCounter.hasKey(address):
if self.txCounter.hasKey(address) and self.allTxLoaded.hasKey(address) :
var chainIDs = self.txCounter[address]
chainIDs.del(chainIDs.find(chainID))
self.txCounter[address] = chainIDs
if self.txCounter[address].len == 0:
self.txCounter.del(address)
self.events.emit(SIGNAL_TRANSACTION_LOADING_COMPLETED_FOR_ALL_NETWORKS, TransactionsLoadedArgs(address: address))
self.events.emit(SIGNAL_TRANSACTION_LOADING_COMPLETED_FOR_ALL_NETWORKS, TransactionsLoadedArgs(address: address, allTxLoaded: self.allTxLoaded[address]))
proc loadTransactions*(self: Service, address: string, toBlock: Uint256, limit: int = 20, loadMore: bool = false) =
let networks = self.networkService.getNetworks()
self.allTxLoaded.del(address)
if not self.txCounter.hasKey(address):
var networkChains: seq[int] = @[]
self.txCounter[address] = networkChains

View File

@ -281,7 +281,6 @@ Rectangle {
lineHeight: 24
visible: inlineTagModelRepeater.count > 0
loading: statusListItem.loading
}
StatusScrollView {

View File

@ -369,9 +369,15 @@ Item {
Layout.alignment: detailsFlow.isOverflowing ? Qt.AlignLeft : Qt.AlignRight
iconAsset.icon: "browser"
tagPrimaryLabel.text: qsTr("Website")
controlBackground.color: Theme.palette.baseColor2
controlBackground.border.color: "transparent"
visible: typeof token != "undefined" && token && token.assetWebsiteUrl !== ""
customBackground: Component {
Rectangle {
color: Theme.palette.baseColor2
border.width: 1
border.color: "transparent"
radius: 36
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
@ -385,9 +391,15 @@ Item {
image.source: token && token.builtOn !== "" ? Style.svg("tiny/" + RootStore.getNetworkIconUrl(token.builtOn)) : ""
tagPrimaryLabel.text: token && token.builtOn !== "" ? RootStore.getNetworkName(token.builtOn) : "---"
tagSecondaryLabel.text: token && token.address !== "" ? token.address : "---"
controlBackground.color: Theme.palette.baseColor2
controlBackground.border.color: "transparent"
visible: typeof token != "undefined" && token && token.builtOn !== "" && token.address !== ""
customBackground: Component {
Rectangle {
color: Theme.palette.baseColor2
border.width: 1
border.color: "transparent"
radius: 36
}
}
}
}
}

View File

@ -4,30 +4,44 @@ import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import utils 1.0
Control {
id: root
property alias image : image
property alias iconAsset : iconAsset
property alias tagPrimaryLabel: tagPrimaryLabel
property alias tagSecondaryLabel: tagSecondaryLabel
property alias controlBackground: controlBackground
property alias rightComponent: rightComponent.sourceComponent
property bool loading: false
property Component customBackground: Component {
Rectangle {
color: "transparent"
border.width: 1
border.color: Theme.palette.baseColor2
radius: 36
}
}
QtObject {
id: d
property var loadingComponent: Component { LoadingComponent {}}
}
horizontalPadding: Style.current.halfPadding
verticalPadding: 5
background: Rectangle {
id: controlBackground
color: "transparent"
border.width: 1
border.color: Theme.palette.baseColor2
radius: 36
background: Loader {
sourceComponent: root.loading ? d.loadingComponent : root.customBackground
}
contentItem: RowLayout {
spacing: 4
visible: !root.loading
// FIXME this could be StatusIcon but it can't load images from an arbitrary URL
Image {
id: image

View File

@ -4,6 +4,7 @@ import QtQuick.Layouts 1.3
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import utils 1.0
import shared 1.0
@ -11,6 +12,9 @@ import shared 1.0
StatusListItem {
id: root
property alias cryptoValueText: cryptoValueText
property alias fiatValueText: fiatValueText
property var modelData
property string symbol
property bool isIncoming
@ -24,8 +28,9 @@ StatusListItem {
property string savedAddressName
state: "normal"
asset.isImage: true
asset.isImage: !loading
asset.name: root.symbol ? Style.png("tokens/%1".arg(root.symbol)) : ""
asset.isLetterIdenticon: loading
title: modelData !== undefined && !!modelData ?
isIncoming ? qsTr("Receive %1").arg(root.symbol) : !!savedAddressName ?
qsTr("Send %1 to %2").arg(root.symbol).arg(savedAddressName) :
@ -36,16 +41,23 @@ StatusListItem {
tagPrimaryLabel.text: networkName
tagPrimaryLabel.color: networkColor
image.source: !!networkIcon ? Style.svg("tiny/%1".arg(networkIcon)) : ""
background: Rectangle {
id: controlBackground
implicitWidth: 51
implicitHeight: 24
color: "transparent"
border.width: 1
border.color: Theme.palette.baseColor2
radius: 36
customBackground: Component {
Rectangle {
color: "transparent"
border.width: 1
border.color: Theme.palette.baseColor2
radius: 36
}
}
width: 51
height: root.loading ? textMetrics.tightBoundingRect.height : 24
rightComponent: transferStatus === Constants.TransactionStatus.Success ? completedIcon : loadingIndicator
loading: root.loading
}
TextMetrics {
id: textMetrics
font: statusListItemSubTitle.font
text: statusListItemSubTitle.text
}
components: [
ColumnLayout {
@ -57,18 +69,23 @@ StatusListItem {
icon: "arrow-up"
rotation: isIncoming ? 135 : 45
height: 18
visible: !root.loading
}
StatusBaseText {
StatusTextWithLoadingState {
id: cryptoValueText
text: LocaleUtils.currencyAmountToLocaleString(cryptoValue)
color: Theme.palette.directColor1
customColor: Theme.palette.directColor1
loading: root.loading
}
}
StatusBaseText {
StatusTextWithLoadingState {
id: fiatValueText
Layout.alignment: Qt.AlignRight
text: LocaleUtils.currencyAmountToLocaleString(fiatValue)
font.pixelSize: 15
color: Theme.palette.baseColor1
customColor: Theme.palette.baseColor1
loading: root.loading
}
}
]

View File

@ -15,39 +15,34 @@ import "../stores"
import "../controls"
ColumnLayout {
id: historyView
id: root
property var account
property int pageSize: 20 // number of transactions per page
property bool isLoading: false
signal launchTransactionDetail(var transaction)
function fetchHistory() {
if (RootStore.isFetchingHistory(historyView.account.address)) {
isLoading = true
} else {
RootStore.loadTransactionsForAccount(historyView.account.address, pageSize)
if (!RootStore.isFetchingHistory(root.account.address)) {
d.isLoading = true
RootStore.loadTransactionsForAccount(root.account.address, pageSize)
}
}
QtObject {
id: d
property bool isLoading: false
}
Connections {
target: RootStore.history
function onLoadingTrxHistoryChanged(isLoading: bool, address: string) {
if (historyView.account.address.toLowerCase() === address.toLowerCase()) {
historyView.isLoading = isLoading
if (root.account.address.toLowerCase() === address.toLowerCase()) {
d.isLoading = isLoading
}
}
}
Loader {
id: loadingImg
active: isLoading
sourceComponent: loadingImageComponent
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
Layout.rightMargin: Style.current.padding
}
StyledText {
id: nonArchivalNodeError
Layout.alignment: Qt.AlignTop
@ -60,7 +55,7 @@ ColumnLayout {
StyledText {
id: noTxs
visible: !isLoading && transactionListRoot.count === 0
visible: !d.isLoading && transactionListRoot.count === 0
text: qsTr("No transactions found")
font.pixelSize: Style.current.primaryTextFontSize
}
@ -76,26 +71,17 @@ ColumnLayout {
model: RootStore.historyTransactions
delegate: Loader {
width: parent.width
width: ListView.view.width
sourceComponent: isTimeStamp ? dateHeader : transactionDelegate
onLoaded: {
item.modelData = model
}
}
footer: footerComp
ScrollBar.vertical: StatusScrollBar {}
footer: StatusButton {
id: loadMoreButton
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Load More")
// TODO: handle case when requested limit === transaction count -- there
// is currently no way to know that there are no more results
enabled: !isLoading && RootStore.historyTransactions.hasMore
onClicked: fetchHistory()
loading: isLoading
}
onAtYEndChanged: if(atYEnd && RootStore.historyTransactions.hasMore) fetchHistory()
}
Component {
@ -113,23 +99,51 @@ ColumnLayout {
Component {
id: transactionDelegate
TransactionDelegate {
property bool modelDataValid: modelData !== undefined && !!modelData
property bool modelDataValid: !!modelData
isIncoming: modelDataValid ? modelData.to === account.address: false
cryptoValue: modelDataValid ? modelData.value : undefined
fiatValue: modelDataValid ? RootStore.getFiatValue(cryptoValue.amount, symbol, RootStore.currentCurrency) : undefined
fiatValue: modelDataValid && !!cryptoValue ? RootStore.getFiatValue(cryptoValue.amount, symbol, RootStore.currentCurrency) : undefined
networkIcon: modelDataValid ? RootStore.getNetworkIcon(modelData.chainId) : ""
networkColor: modelDataValid ? RootStore.getNetworkColor(modelData.chainId) : ""
networkName: modelDataValid ? RootStore.getNetworkShortName(modelData.chainId) : ""
symbol: modelDataValid ? modelData.symbol : ""
symbol: modelDataValid && !!modelData.symbol ? modelData.symbol : ""
transferStatus: modelDataValid ? RootStore.hex2Dec(modelData.txStatus) : ""
shortTimeStamp: modelDataValid ? LocaleUtils.formatTime(modelData.timestamp * 1000, Locale.ShortFormat) : ""
savedAddressName: modelDataValid ? RootStore.getNameForSavedWalletAddress(modelData.to) : ""
onClicked: launchTransactionDetail(modelData)
loading: modelDataValid ? modelData.loadingTransaction : false
}
}
Component {
id: loadingImageComponent
StatusLoadingIndicator {}
id: footerComp
ColumnLayout {
width: root.width
visible: !RootStore.historyTransactions.hasMore && transactionListRoot.count !== 0
spacing: 12
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.curent.padding
Layout.preferredWidth: parent.width - 100
Layout.preferredHeight: 1
color: Theme.palette.directColor8
visible: !RootStore.historyTransactions.hasMore
}
StatusBaseText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
text: qsTr("You have reached the beginning of the activity for this account")
font.pixelSize: 13
color: Theme.palette.baseColor1
visible: !RootStore.historyTransactions.hasMore
horizontalAlignment: Text.AlignHCenter
}
StatusButton {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Back to most recent transaction")
visible: !RootStore.historyTransactions.hasMore
onClicked: transactionListRoot.positionViewAtBeginning()
}
}
}
}