feat(@desktop/wallet): Transaction collectibles filtering (#12162)
This commit is contained in:
parent
521be27ae0
commit
fddcc3a83f
|
@ -30,6 +30,9 @@ proc toRef*[T](obj: T): ref T =
|
|||
const FETCH_BATCH_COUNT_DEFAULT = 10
|
||||
const FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT = 2000
|
||||
|
||||
type
|
||||
CollectiblesToTokenConverter* = proc (id: string): backend_activity.Token
|
||||
|
||||
QtObject:
|
||||
type
|
||||
Controller* = ref object of QObject
|
||||
|
@ -54,6 +57,8 @@ QtObject:
|
|||
|
||||
requestId: int32
|
||||
|
||||
collectiblesToTokenConverter: CollectiblesToTokenConverter
|
||||
|
||||
proc setup(self: Controller) =
|
||||
self.QObject.setup
|
||||
|
||||
|
@ -236,7 +241,8 @@ QtObject:
|
|||
proc newController*(requestId: int32,
|
||||
currencyService: currency_service.Service,
|
||||
tokenService: token_service.Service,
|
||||
events: EventEmitter): Controller =
|
||||
events: EventEmitter,
|
||||
collectiblesConverter: CollectiblesToTokenConverter): Controller =
|
||||
new(result, delete)
|
||||
|
||||
result.requestId = requestId
|
||||
|
@ -256,6 +262,8 @@ QtObject:
|
|||
result.allAddressesSelected = false
|
||||
result.chainIds = @[]
|
||||
|
||||
result.collectiblesToTokenConverter = collectiblesConverter
|
||||
|
||||
result.setup()
|
||||
|
||||
result.setupEventHandlers()
|
||||
|
@ -284,6 +292,20 @@ QtObject:
|
|||
|
||||
self.currentActivityFilter.counterpartyAddresses = addresses
|
||||
|
||||
proc setFilterCollectibles*(self: Controller, collectiblesArrayJsonString: string) {.slot.} =
|
||||
let collectiblesJson = parseJson(collectiblesArrayJsonString)
|
||||
if collectiblesJson.kind != JArray:
|
||||
error "invalid array of json strings"
|
||||
return
|
||||
|
||||
var collectibles = newSeq[backend_activity.Token]()
|
||||
for i in 0 ..< collectiblesJson.len:
|
||||
let uid = collectiblesJson[i].getStr()
|
||||
let token = self.collectiblesToTokenConverter(uid)
|
||||
collectibles.add(token)
|
||||
|
||||
self.currentActivityFilter.collectibles = collectibles
|
||||
|
||||
# Depends on self.filterTokenCodes and self.chainIds, so should be called after updating them
|
||||
proc updateAssetsIdentities(self: Controller) =
|
||||
var assets = newSeq[backend_activity.Token]()
|
||||
|
|
|
@ -206,7 +206,7 @@ QtObject:
|
|||
if self.metadata.tokenIn.isSome:
|
||||
let address = self.metadata.tokenIn.unsafeGet().address
|
||||
if address.isSome:
|
||||
return toHex(address.unsafeGet())
|
||||
return "0x" & toHex(address.unsafeGet())
|
||||
return ""
|
||||
|
||||
QtProperty[string] tokenInAddress:
|
||||
|
@ -216,7 +216,7 @@ QtObject:
|
|||
if self.metadata.tokenOut.isSome:
|
||||
let address = self.metadata.tokenOut.unsafeGet().address
|
||||
if address.isSome:
|
||||
return toHex(address.unsafeGet())
|
||||
return "0x" & toHex(address.unsafeGet())
|
||||
return ""
|
||||
|
||||
QtProperty[string] tokenOutAddress:
|
||||
|
|
|
@ -35,6 +35,8 @@ import app_service/service/network_connection/service as network_connection_serv
|
|||
import app_service/service/devices/service as devices_service
|
||||
|
||||
import backend/collectibles as backend_collectibles
|
||||
import backend/activity as backend_activity
|
||||
|
||||
|
||||
logScope:
|
||||
topics = "wallet-section-module"
|
||||
|
@ -120,13 +122,16 @@ proc newModule*(
|
|||
result.networksService = networkService
|
||||
|
||||
result.transactionService = transactionService
|
||||
result.activityController = activityc.newController(int32(ActivityID.History), currencyService, tokenService, events)
|
||||
result.tmpActivityController = activityc.newController(int32(ActivityID.Temporary), currencyService, tokenService, events)
|
||||
result.collectiblesController = collectiblesc.newController(
|
||||
let collectiblesController = collectiblesc.newController(
|
||||
requestId = int32(backend_collectibles.CollectiblesRequestID.WalletAccount),
|
||||
autofetch = false,
|
||||
events = events
|
||||
)
|
||||
result.collectiblesController = collectiblesController
|
||||
let collectiblesToTokenConverter = proc(id: string): backend_activity.Token =
|
||||
return collectiblesController.getActivityToken(id)
|
||||
result.activityController = activityc.newController(int32(ActivityID.History), currencyService, tokenService, events, collectiblesToTokenConverter)
|
||||
result.tmpActivityController = activityc.newController(int32(ActivityID.Temporary), currencyService, tokenService, events, collectiblesToTokenConverter)
|
||||
result.collectibleDetailsController = collectible_detailsc.newController(int32(backend_collectibles.CollectiblesRequestID.WalletAccount), networkService, events)
|
||||
result.filter = initFilter(result.controller)
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import NimQml, Tables, strutils, strformat, sequtils, stint
|
|||
import logging
|
||||
|
||||
import ./collectibles_item, ./collectible_trait_model
|
||||
import web3/ethtypes as eth
|
||||
import backend/activity as backend_activity
|
||||
|
||||
type
|
||||
CollectibleRole* {.pure.} = enum
|
||||
|
@ -322,4 +324,20 @@ QtObject:
|
|||
for item in self.items:
|
||||
if(cmpIgnoreCase(item.getId(), id) == 0):
|
||||
return item.getName()
|
||||
return ""
|
||||
return ""
|
||||
|
||||
proc getActivityToken*(self: Model, id: string): backend_activity.Token =
|
||||
for item in self.items:
|
||||
if(cmpIgnoreCase(item.getId(), id) == 0):
|
||||
result.tokenType = backend_activity.TokenType.Erc721
|
||||
result.chainId = backend_activity.ChainId(item.getChainId())
|
||||
var contract = item.getContractAddress()
|
||||
if len(contract) > 0:
|
||||
var address: eth.Address
|
||||
address = eth.fromHex(eth.Address, contract)
|
||||
result.address = some(address)
|
||||
var tokenId = item.getTokenId()
|
||||
if tokenId > 0:
|
||||
result.tokenId = some(backend_activity.TokenId("0x" & stint.toHex(tokenId)))
|
||||
return result
|
||||
return result
|
||||
|
|
|
@ -8,6 +8,7 @@ import events_handler
|
|||
import app/core/eventemitter
|
||||
|
||||
import backend/collectibles as backend_collectibles
|
||||
import backend/activity as backend_activity
|
||||
|
||||
const FETCH_BATCH_COUNT_DEFAULT = 50
|
||||
|
||||
|
@ -123,7 +124,7 @@ QtObject:
|
|||
|
||||
let res = fromJson(response, backend_collectibles.FilterOwnedCollectiblesResponse)
|
||||
|
||||
let isError = res.errorCode != ErrorCodeSuccess
|
||||
let isError = res.errorCode != backend_collectibles.ErrorCodeSuccess
|
||||
|
||||
if isError:
|
||||
error "error fetching collectibles entries: ", res.errorCode
|
||||
|
@ -203,3 +204,6 @@ QtObject:
|
|||
self.eventsHandler.updateSubscribedChainIDs(self.chainIds)
|
||||
|
||||
self.resetModel()
|
||||
|
||||
proc getActivityToken*(self: Controller, id: string): backend_activity.Token =
|
||||
return self.model.getActivityToken(id)
|
|
@ -25,6 +25,10 @@ Column {
|
|||
activityFilterStore.updateFilterBase()
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
activityFilterMenu.resetView()
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
|
||||
|
@ -157,10 +161,27 @@ Column {
|
|||
Repeater {
|
||||
model: activityFilterStore.collectiblesFilter
|
||||
delegate: ActivityFilterTagItem {
|
||||
tagPrimaryLabel.text: activityFilterStore.collectiblesList.getName(modelData)
|
||||
iconAsset.icon: activityFilterStore.collectiblesList.getImageUrl(modelData)
|
||||
id: collectibleTag
|
||||
property string uid: modelData
|
||||
readonly property bool isValid: tagPrimaryLabel.text.length > 0
|
||||
tagPrimaryLabel.text: activityFilterStore.collectiblesList.getName(uid)
|
||||
iconAsset.icon: activityFilterStore.collectiblesList.getImageUrl(uid)
|
||||
iconAsset.color: "transparent"
|
||||
onClosed: activityFilterStore.toggleCollectibles(model.id)
|
||||
onClosed: activityFilterStore.toggleCollectibles(uid)
|
||||
|
||||
Connections {
|
||||
// Collectibles model is fetched asynchronousl, so data might not be available
|
||||
target: activityFilterStore.collectiblesList
|
||||
enabled: !collectibleTag.isValid
|
||||
function onIsFetchingChanged() {
|
||||
if (activityFilterStore.collectiblesList.isFetching)
|
||||
return
|
||||
collectibleTag.uid = ""
|
||||
collectibleTag.uid = modelData
|
||||
if (!collectibleTag.isValid)
|
||||
activityFilterStore.collectiblesList.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,7 +249,7 @@ Column {
|
|||
collectiblesList: activityFilterStore.collectiblesList
|
||||
collectiblesFilter: activityFilterStore.collectiblesFilter
|
||||
onUpdateTokensFilter: activityFilterStore.toggleToken(tokenSymbol)
|
||||
onUpdateCollectiblesFilter: activityFilterStore.toggleCollectibles(id)
|
||||
onUpdateCollectiblesFilter: activityFilterStore.toggleCollectibles(uid)
|
||||
|
||||
store: root.store
|
||||
recentsList: activityFilterStore.recentsList
|
||||
|
|
|
@ -16,8 +16,9 @@ ColumnLayout {
|
|||
property string nftName
|
||||
property string nftUrl
|
||||
property string tokenId
|
||||
property string contractAddress
|
||||
property string tokenAddress
|
||||
property bool strikethrough: false
|
||||
property bool areTestNetworksEnabled: false
|
||||
|
||||
spacing: Style.current.padding
|
||||
|
||||
|
@ -100,8 +101,11 @@ ColumnLayout {
|
|||
icon.name: "external"
|
||||
type: StatusRoundButton.Type.Quinary
|
||||
radius: 8
|
||||
visible: nftPreviewSensor.hovered && !!root.tokenId && !!root.contractAddress
|
||||
onClicked: Global.openLink("https://etherscan.io/nft/%1/%2".arg(root.contractAddress).arg(root.tokenId))
|
||||
visible: nftPreviewSensor.hovered && !!root.tokenId && !!root.tokenAddress
|
||||
onClicked: {
|
||||
const link = areTestNetworksEnabled ? Constants.networkExplorerLinks.goerliEtherscan : Constants.networkExplorerLinks.etherscan
|
||||
Global.openLink("%1/nft/%2/%3".arg(link).arg(root.tokenAddress).arg(root.tokenId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ StatusMenu {
|
|||
property var collectiblesList: []
|
||||
property var collectiblesFilter: []
|
||||
readonly property bool allCollectiblesChecked: tokensMenu.allCollectiblesChecked
|
||||
signal updateCollectiblesFilter(double id)
|
||||
signal updateCollectiblesFilter(string uid)
|
||||
|
||||
// Recents filter
|
||||
property var recentsList
|
||||
|
@ -52,6 +52,11 @@ StatusMenu {
|
|||
|
||||
implicitWidth: 176
|
||||
|
||||
function resetView() {
|
||||
counterPartyMenu.resetView()
|
||||
tokensMenu.resetView()
|
||||
}
|
||||
|
||||
// Filter By Period
|
||||
ActivityFilterMenuItem {
|
||||
text: qsTr("Period")
|
||||
|
@ -92,7 +97,7 @@ StatusMenu {
|
|||
collectiblesList: root.collectiblesList
|
||||
collectiblesFilter: root.collectiblesFilter
|
||||
onTokenToggled: updateTokensFilter(tokenSymbol)
|
||||
onCollectibleToggled: updateCollectiblesFilter(id)
|
||||
onCollectibleToggled: updateCollectiblesFilter(uid)
|
||||
closePolicy: root.closePolicy
|
||||
}
|
||||
ActivityCounterpartyFilterSubMenu {
|
||||
|
|
|
@ -40,6 +40,10 @@ StatusMenu {
|
|||
|
||||
Component.onCompleted: root.updateRecipientsModel()
|
||||
|
||||
function resetView() {
|
||||
searchBox.reset()
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
|
|
|
@ -26,15 +26,20 @@ StatusMenu {
|
|||
|
||||
signal back()
|
||||
signal tokenToggled(string tokenSymbol)
|
||||
signal collectibleToggled(double id)
|
||||
signal collectibleToggled(string uid)
|
||||
|
||||
property var searchTokenSymbolByAddressFn: function (address) { return "" } // TODO
|
||||
|
||||
implicitWidth: 289
|
||||
|
||||
function resetView() {
|
||||
tokensSearchBox.reset()
|
||||
collectiblesSearchBox.reset()
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
property bool isFetching: root.collectiblesList.isFetching
|
||||
readonly property bool isFetching: root.collectiblesList.isFetching
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
|
@ -99,12 +104,14 @@ StatusMenu {
|
|||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
model: SortFilterProxyModel {
|
||||
id: tokenProxyModel
|
||||
sourceModel: root.tokensList
|
||||
filters: ExpressionFilter {
|
||||
readonly property string tokenSearchValue: tokensSearchBox.text.toUpperCase()
|
||||
readonly property string tokenSymbolByAddress: root.searchTokenSymbolByAddressFn(tokensSearchBox.text)
|
||||
enabled: root.tokensList.count > 0
|
||||
expression: {
|
||||
const tokenSymbolByAddress = root.searchTokenSymbolByAddressFn(tokensSearchBox.text)
|
||||
return symbol.startsWith(tokensSearchBox.text.toUpperCase()) || name.toUpperCase().startsWith(tokensSearchBox.text.toUpperCase()) || (tokenSymbolByAddress!=="" && symbol.startsWith(tokenSymbolByAddress))
|
||||
return symbol.startsWith(tokenSearchValue) || name.toUpperCase().startsWith(tokenSearchValue) || (tokenSymbolByAddress!=="" && symbol.startsWith(tokenSymbolByAddress))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,28 +162,30 @@ StatusMenu {
|
|||
spacing: 0
|
||||
reuseItems: true
|
||||
model: SortFilterProxyModel {
|
||||
id: collectibleProxyModel
|
||||
sourceModel: root.collectiblesList
|
||||
filters: ExpressionFilter {
|
||||
enabled: root.collectiblesList.count > 0 && !!collectiblesSearchBox.text
|
||||
readonly property string searchText: collectiblesSearchBox.text.toUpperCase()
|
||||
expression: {
|
||||
const searchText = collectiblesSearchBox.text.toUpperCase()
|
||||
return name.toUpperCase().startsWith(searchText)
|
||||
return String(name).toUpperCase().startsWith(searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: ActivityTypeCheckBox {
|
||||
required property var model
|
||||
width: ListView.view.width
|
||||
height: 44
|
||||
title: model.name
|
||||
assetSettings.name: model.imageUrl
|
||||
title: model.name ?? ""
|
||||
assetSettings.name: model.imageUrl ?? ""
|
||||
assetSettings.isImage: true
|
||||
assetSettings.bgWidth: 32
|
||||
assetSettings.bgHeight: 32
|
||||
assetSettings.bgRadius: assetSettings.bgHeight/2
|
||||
buttonGroup: collectibleButtonGroup
|
||||
allChecked: root.allCollectiblesChecked
|
||||
checked: root.allCollectiblesChecked || root.collectiblesFilter.includes(model.id)
|
||||
onActionTriggered: root.collectibleToggled(model.id)
|
||||
checked: !loading && (root.allCollectiblesChecked || root.collectiblesFilter.includes(model.uid))
|
||||
onActionTriggered: root.collectibleToggled(model.uid)
|
||||
loading: d.isFetching
|
||||
}
|
||||
}
|
||||
|
|
|
@ -191,15 +191,13 @@ QtObject {
|
|||
// To-do: Get list of collectibles with activity from backend
|
||||
property var collectiblesList: walletSection.collectiblesController.model
|
||||
property var collectiblesFilter: []
|
||||
function toggleCollectibles(id) {
|
||||
function toggleCollectibles(uid) {
|
||||
// update filters
|
||||
collectiblesFilter = d.toggleFilterState(collectiblesFilter, id, collectiblesList.count)
|
||||
// TODO go side filtering is pending
|
||||
// activityController.setFilterCollectibles(JSON.stringify(collectiblesFilter))
|
||||
// activityController.updateFilter()
|
||||
collectiblesFilter = d.toggleFilterState(collectiblesFilter, uid, collectiblesList.count)
|
||||
activityController.setFilterCollectibles(JSON.stringify(collectiblesFilter))
|
||||
activityController.updateFilter()
|
||||
}
|
||||
|
||||
|
||||
property var recentsList: activityController.recipientsModel
|
||||
property bool loadingRecipients: activityController.status.loadingRecipients
|
||||
property var recentsFilters: []
|
||||
|
@ -251,7 +249,7 @@ QtObject {
|
|||
activityController.setFilterStatus(JSON.stringify(statusFilters))
|
||||
activityController.setFilterAssets(JSON.stringify(tokensFilter), false)
|
||||
activityController.setFilterToAddresses(JSON.stringify(recentsFilters.concat(savedAddressFilters)))
|
||||
// TODO call update filter for collectibles
|
||||
activityController.setFilterCollectibles(JSON.stringify(collectiblesFilter))
|
||||
|
||||
activityController.updateFilter()
|
||||
}
|
||||
|
@ -266,7 +264,6 @@ QtObject {
|
|||
collectiblesFilter = []
|
||||
recentsFilters = []
|
||||
savedAddressFilters = []
|
||||
// TODO reset filter for collectibles
|
||||
|
||||
applyAllFilters()
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ Item {
|
|||
function resetView() {
|
||||
stack.currentIndex = 0
|
||||
root.currentTabIndex = 0
|
||||
historyView.resetView()
|
||||
}
|
||||
|
||||
function resetStack() {
|
||||
|
@ -141,6 +142,7 @@ Item {
|
|||
}
|
||||
}
|
||||
HistoryView {
|
||||
id: historyView
|
||||
overview: RootStore.overview
|
||||
showAllAccounts: root.showAllAccounts
|
||||
onLaunchTransactionDetail: function (entry, entryIndex) {
|
||||
|
|
|
@ -40,7 +40,7 @@ Item {
|
|||
|
||||
property var details: null
|
||||
readonly property bool isDetailsValid: details !== undefined && !!details
|
||||
readonly property bool isIncoming: transactionType === Constants.TransactionType.Received
|
||||
readonly property bool isIncoming: transactionType === Constants.TransactionType.Received || transactionType === Constants.TransactionType.ContractDeployment
|
||||
readonly property string networkShortName: root.isTransactionValid ? RootStore.getNetworkShortName(transaction.chainId) : ""
|
||||
readonly property string networkIcon: isTransactionValid ? RootStore.getNetworkIcon(transaction.chainId) : "network/Network=Custom"
|
||||
readonly property int blockNumber: isDetailsValid ? details.blockNumber : 0
|
||||
|
@ -188,7 +188,8 @@ Item {
|
|||
nftUrl: root.isTransactionValid && !!transaction.nftImageUrl ? transaction.nftImageUrl : ""
|
||||
strikethrough: d.transactionType === Constants.TransactionType.Destroy
|
||||
tokenId: root.isTransactionValid ? transaction.tokenID : ""
|
||||
contractAddress: d.isDetailsValid ? d.details.contract : ""
|
||||
tokenAddress: root.isTransactionValid ? transaction.tokenAddress : ""
|
||||
areTestNetworksEnabled: WalletStores.RootStore.areTestNetworksEnabled
|
||||
}
|
||||
|
||||
Column {
|
||||
|
@ -561,6 +562,7 @@ Item {
|
|||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
visible: amountSentTile.visible || amountReceiveTile.visible || feesTile.visible || totalTile.visible
|
||||
StatusBaseText {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
font.pixelSize: 15
|
||||
|
@ -580,6 +582,7 @@ Item {
|
|||
|
||||
DetailsPanel {
|
||||
TransactionDataTile {
|
||||
id: amountSentTile
|
||||
width: parent.width
|
||||
title: qsTr("Amount sent")
|
||||
subTitle: transactionHeader.isMultiTransaction ? d.outCryptoValueFormatted : d.cryptoValueFormatted
|
||||
|
@ -598,6 +601,7 @@ Item {
|
|||
}
|
||||
}
|
||||
TransactionDataTile {
|
||||
id: amountReceiveTile
|
||||
width: parent.width
|
||||
title: transactionHeader.transactionStatus === Constants.TransactionStatus.Pending ? qsTr("Amount to receive") : qsTr("Amount received")
|
||||
subTitle: {
|
||||
|
@ -625,6 +629,7 @@ Item {
|
|||
visible: !!subTitle
|
||||
}
|
||||
TransactionDataTile {
|
||||
id: feesTile
|
||||
width: parent.width
|
||||
title: d.symbol ? qsTr("Fees") : qsTr("Estimated max fee")
|
||||
subTitle: {
|
||||
|
@ -659,6 +664,7 @@ Item {
|
|||
visible: !!subTitle
|
||||
}
|
||||
TransactionDataTile {
|
||||
id: totalTile
|
||||
width: parent.width
|
||||
readonly property bool fieldIsHidden: (transactionHeader.isNFT && d.isIncoming) || !d.symbol
|
||||
readonly property bool showMaxFee: d.transactionType === Constants.TransactionType.ContractDeployment && transactionHeader.transactionStatus === Constants.TransactionStatus.Pending
|
||||
|
|
|
@ -504,8 +504,8 @@ StatusListItem {
|
|||
return qsTr("Via %1 on %2").arg(name).arg(networkName)
|
||||
case Constants.TransactionType.Mint:
|
||||
if (allAccounts)
|
||||
return qsTr("%1 via %2 in %4").arg(transactionValue).arg(networkName).arg(toAddress)
|
||||
return qsTr("%1 via %2").arg(transactionValue).arg(networkName).arg(networkName)
|
||||
return qsTr("%1 via %2 in %3").arg(transactionValue).arg(networkName).arg(toAddress)
|
||||
return qsTr("%1 via %2").arg(transactionValue).arg(networkName)
|
||||
default:
|
||||
if (allAccounts)
|
||||
return qsTr("%1 from %2 to %3 via %4").arg(transactionValue).arg(fromAddress).arg(toAddress).arg(networkName)
|
||||
|
|
|
@ -31,6 +31,12 @@ ColumnLayout {
|
|||
|
||||
signal launchTransactionDetail(var transaction, int entryIndex)
|
||||
|
||||
function resetView() {
|
||||
if (!!filterPanelLoader.item) {
|
||||
filterPanelLoader.item.resetView()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
return
|
||||
|
|
Loading…
Reference in New Issue