Wallet(SendModal): New tokens selector intially integrated

Closes: #15512
This commit is contained in:
Michał Cieślak 2024-07-10 00:10:13 +02:00 committed by Michał
parent baa65de1ae
commit f6320f69cb
8 changed files with 196 additions and 128 deletions

View File

@ -4,9 +4,11 @@ import QtQuick.Layouts 1.15
import StatusQ 0.1 import StatusQ 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import Storybook 1.0 import Storybook 1.0
import Models 1.0
import utils 1.0 import utils 1.0
import shared.popups.send 1.0 import shared.popups.send 1.0
@ -22,18 +24,68 @@ SplitView {
orientation: Qt.Horizontal orientation: Qt.Horizontal
property WalletAssetsStore walletAssetStore: WalletAssetsStore { property WalletAssetsStore walletAssetStore: WalletAssetsStore {
assetsWithFilteredBalances: root.assetsWithFilteredBalances
}
// Workaround to satisfy stub which is not empty (but should be)
assetsWithFilteredBalances: ListModel {}
property SubmodelProxyModel assetsWithFilteredBalances: SubmodelProxyModel { property var groupedAccountAssetsModel: ListModel {
sourceModel: root.walletAssetStore.groupedAccountsAssetsModel Component.onCompleted: {
submodelRoleName: "balances" const data = [
delegateModel: SortFilterProxyModel { {
sourceModel: submodel tokensKey: "key_eth",
filters: FastExpressionFilter { name: "Ethereum",
expression: txStore.selectedSenderAccountAddress === model.account symbol: "ETH",
expectedRoles: ["account"] decimals: 18,
communityId: "",
balances: [
{
chainId: "1",
balance: "122082928968121891",
account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240",
},
{
chainId: "420",
balance: "559133758939097000",
account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
}
],
currentCurrencyBalance: 234.234,
marketDetails: {
currencyPrice: {
amount: 12234.23,
displayDecimals: true
}
}
},
{
tokensKey: "key_dai",
name: "DAI",
symbol: "DAI",
decimals: 18,
communityId: "",
balances: [
{
chainId: "420",
balance: "1142155111",
account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
},
{
chainId: "1",
balance: "4411211243121551121",
account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
}
],
currentCurrencyBalance: 234.234,
marketDetails: {
currencyPrice: {
amount: 234.23,
displayDecimals: true
}
}
}
]
append(data)
} }
} }
} }

View File

@ -9,6 +9,16 @@ import SortFilterProxyModel 0.2
import AppLayouts.Wallet.stores 1.0 import AppLayouts.Wallet.stores 1.0
// TODO: This store, as all other stores should be empty QtObject {}.
// All mocking should be done in place in Storybook pages and unit tests.
// If it's necessary to share mocks between tests/pages, such mock can be
// created by deriving from empty stub and putting in mocks dir.
// Stores itself should be simple, thin layers over functionality exposed from
// the backend. No additional logic should there. Data transformation logic
// should be delegated to adaptors, stateles helpers to proper utility singletons.
//
// PLEASE DO NOT ADD ANY NEW CONTENT HERE
QtObject { QtObject {
id: root id: root

View File

@ -53,6 +53,9 @@ QObject {
// output model // output model
readonly property SortFilterProxyModel outputAssetsModel: SortFilterProxyModel { readonly property SortFilterProxyModel outputAssetsModel: SortFilterProxyModel {
objectName: "TokenSelectorViewAdaptor_outputAssetsModel"
sourceModel: showAllTokens && !!plainTokensBySymbolModel ? concatModel : assetsObjectProxyModel sourceModel: showAllTokens && !!plainTokensBySymbolModel ? concatModel : assetsObjectProxyModel
proxyRoles: [ proxyRoles: [
@ -135,6 +138,7 @@ QObject {
} }
expression: isPresentOnEnabledNetworks(model.addressPerChain) expression: isPresentOnEnabledNetworks(model.addressPerChain)
expectedRoles: ["addressPerChain"] expectedRoles: ["addressPerChain"]
enabled: root.enabledChainIds.length
} }
] ]
@ -203,6 +207,8 @@ QObject {
id: assetsObjectProxyModel id: assetsObjectProxyModel
sourceModel: root.assetsModel sourceModel: root.assetsModel
objectName: "TokenSelectorViewAdaptor_assetsObjectProxyModel"
delegate: SortFilterProxyModel { delegate: SortFilterProxyModel {
id: delegateRoot id: delegateRoot

View File

@ -31,7 +31,7 @@ QtObject {
} }
readonly property var collectiblesController: ManageTokensController { readonly property var collectiblesController: ManageTokensController {
sourceModel: _jointCollectiblesBySymbolModel sourceModel: root.jointCollectiblesBySymbolModel
settingsKey: "WalletCollectibles" settingsKey: "WalletCollectibles"
serializeAsCollectibles: true serializeAsCollectibles: true
@ -85,8 +85,8 @@ QtObject {
] ]
} }
/* PRIVATE: This model joins the "Tokens By Symbol Model" and "Communities Model" by communityId */ /* TODO: move all transformations to a dedicated adaptors */
property LeftJoinModel _jointCollectiblesBySymbolModel: LeftJoinModel { readonly property LeftJoinModel jointCollectiblesBySymbolModel: LeftJoinModel {
objectName: "jointCollectiblesBySymbolModel" objectName: "jointCollectiblesBySymbolModel"
leftModel: allCollectiblesModel leftModel: allCollectiblesModel

View File

@ -1522,8 +1522,12 @@ Item {
sourceComponent: SendPopups.SendModal { sourceComponent: SendPopups.SendModal {
onlyAssets: sendModal.onlyAssets onlyAssets: sendModal.onlyAssets
store: appMain.transactionStore
loginType: appMain.rootStore.loginType loginType: appMain.rootStore.loginType
store: appMain.transactionStore
collectiblesStore: appMain.walletCollectiblesStore
onClosed: { onClosed: {
sendModal.closed() sendModal.closed()
sendModal.preSelectedSendType = Constants.SendType.Unknown sendModal.preSelectedSendType = Constants.SendType.Unknown

View File

@ -1,6 +1,6 @@
import QtQuick 2.13 import QtQuick 2.15
import QtQuick.Controls 2.13 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.13 import QtQuick.Layouts 1.15
import QtQuick.Dialogs 1.3 import QtQuick.Dialogs 1.3
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
@ -20,8 +20,10 @@ import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 import StatusQ.Core.Utils 0.1
import StatusQ.Popups.Dialog 0.1 import StatusQ.Popups.Dialog 0.1
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.adaptors 1.0 import AppLayouts.Wallet.adaptors 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStores
import shared.popups.send.panels 1.0 import shared.popups.send.panels 1.0
import "./controls" import "./controls"
@ -48,7 +50,8 @@ StatusDialog {
property alias modalHeader: modalHeader.text property alias modalHeader: modalHeader.text
required property TransactionStore store required property TransactionStore store
property var nestedCollectiblesModel: store.nestedCollectiblesModel property WalletStores.CollectiblesStore collectiblesStore
property var bestRoutes property var bestRoutes
property bool isLoading: false property bool isLoading: false
property int loginType property int loginType
@ -117,40 +120,6 @@ StatusDialog {
property var hoveredHoldingType: Constants.TokenType.Unknown property var hoveredHoldingType: Constants.TokenType.Unknown
readonly property bool isHoveredHoldingValidAsset: !!hoveredHolding && hoveredHoldingType === Constants.TokenType.ERC20 readonly property bool isHoveredHoldingValidAsset: !!hoveredHolding && hoveredHoldingType === Constants.TokenType.ERC20
function getHolding(holdingId, holdingType) {
if (holdingType === Constants.TokenType.ERC20) {
return store.getAsset(assetsAdaptor.model, holdingId)
} else if (holdingType === Constants.TokenType.ERC721 || holdingType === Constants.TokenType.ERC1155) {
return store.getCollectible(holdingId)
} else {
return {}
}
}
function setSelectedHoldingId(holdingId, holdingType) {
let holding = getHolding(holdingId, holdingType)
setSelectedHolding(holding, holdingType)
}
function setSelectedHolding(holding, holdingType) {
d.selectedHoldingType = holdingType
d.selectedHolding = holding
let selectorHolding = store.holdingToSelectorHolding(holding, holdingType)
holdingSelector.setSelectedItem(selectorHolding, holdingType)
}
function setHoveredHoldingId(holdingId, holdingType) {
let holding = getHolding(holdingId, holdingType)
setHoveredHolding(holding, holdingType)
}
function setHoveredHolding(holding, holdingType) {
d.hoveredHoldingType = holdingType
d.hoveredHolding = holding
let selectorHolding = store.holdingToSelectorHolding(holding, holdingType)
holdingSelector.setHoveredItem(selectorHolding, holdingType)
}
onSelectedHoldingChanged: { onSelectedHoldingChanged: {
if (d.selectedHoldingType === Constants.TokenType.ERC20) { if (d.selectedHoldingType === Constants.TokenType.ERC20) {
if(!d.ensOrStickersPurpose && store.sendType !== Constants.SendType.Bridge) if(!d.ensOrStickersPurpose && store.sendType !== Constants.SendType.Bridge)
@ -173,19 +142,6 @@ StatusDialog {
} }
} }
SendModalAssetsAdaptor {
id: assetsAdaptor
controller: popup.store.walletAssetStore.assetsController
showCommunityAssets: popup.store.tokensStore.showCommunityAssetsInSend
tokensModel: popup.store.walletAssetStore.groupedAccountAssetsModel
account: popup.store.selectedSenderAccountAddress
marketValueThreshold:
popup.store.tokensStore.displayAssetsBelowBalance
? popup.store.tokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount()
: 0
}
LeftJoinModel { LeftJoinModel {
id: fromNetworksRouteModel id: fromNetworksRouteModel
leftModel: popup.store.fromNetworksRouteModel leftModel: popup.store.fromNetworksRouteModel
@ -219,11 +175,35 @@ StatusDialog {
if(popup.preSelectedSendType !== Constants.SendType.Unknown) { if(popup.preSelectedSendType !== Constants.SendType.Unknown) {
store.setSendType(popup.preSelectedSendType) store.setSendType(popup.preSelectedSendType)
} }
if ((popup.preSelectedHoldingType > Constants.TokenType.Native) && if (!!popup.preSelectedHoldingID
(popup.preSelectedHoldingType < Constants.TokenType.Unknown)) { && popup.preSelectedHoldingType > Constants.TokenType.Native
tokenListRect.browsingHoldingType = popup.preSelectedHoldingType && popup.preSelectedHoldingType < Constants.TokenType.Unknown) {
if (!!popup.preSelectedHoldingID) {
d.setSelectedHoldingId(popup.preSelectedHoldingID, popup.preSelectedHoldingType) if (popup.preSelectedHoldingType === Constants.TokenType.ERC20) {
const entry = ModelUtils.getByKey(
assetsAdaptor.outputAssetsModel, "tokensKey",
popup.preSelectedHoldingID)
d.selectedHoldingType = Constants.TokenType.ERC20
d.selectedHolding = entry
holdingSelector.setCustom(entry.symbol, entry.iconSource,
popup.preSelectedHoldingID)
holdingSelector.selectedItem = entry
} else {
const entry = ModelUtils.getByKey(
popup.store.collectiblesModel,
"uid", popup.preSelectedHoldingID)
d.selectedHoldingType = entry.tokenType
d.selectedHolding = entry
const id = entry.communityId ? entry.collectionUid : entry.uid
holdingSelector.setCustom(entry.name,
entry.imageUrl || entry.mediaUrl,
id)
holdingSelector.selectedItem = entry
holdingSelector.currentTab = TokenSelectorPanel.Tabs.Collectibles
} }
} }
@ -267,9 +247,13 @@ StatusDialog {
selectedAddress: !!popup.preSelectedAccount && !!popup.preSelectedAccount.address ? popup.preSelectedAccount.address : "" selectedAddress: !!popup.preSelectedAccount && !!popup.preSelectedAccount.address ? popup.preSelectedAccount.address : ""
onCurrentAccountAddressChanged: { onCurrentAccountAddressChanged: {
store.setSenderAccount(currentAccountAddress) store.setSenderAccount(currentAccountAddress)
if (d.isSelectedHoldingValidAsset) { if (d.isSelectedHoldingValidAsset) {
d.setSelectedHoldingId(d.selectedHolding.symbol, d.selectedHoldingType) d.selectedHolding = ModelUtils.getByKey(
holdingSelector.assetsModel, "tokensKey",
d.selectedHolding.tokensKey)
} }
popup.recalculateRoutesAndFees() popup.recalculateRoutesAndFees()
} }
} }
@ -316,25 +300,69 @@ StatusDialog {
text: d.isBridgeTx ? qsTr("Bridge") : qsTr("Send") text: d.isBridgeTx ? qsTr("Bridge") : qsTr("Send")
} }
HoldingSelector { TokenSelectorNew {
id: holdingSelector id: holdingSelector
property var selectedItem
property bool onlyAssets: false
assetsModel: assetsAdaptor.outputAssetsModel
collectiblesModel: collectiblesAdaptorLoader.active
? collectiblesAdaptorLoader.item.model : null
TokenSelectorViewAdaptor {
id: assetsAdaptor
assetsModel: popup.store.walletAssetStore.groupedAccountAssetsModel
flatNetworksModel: popup.store.flatNetworksModel
currentCurrency: popup.store.currencyStore.currentCurrency
accountAddress: popup.preSelectedAccount ? popup.preSelectedAccount.address : ""
showCommunityAssets: popup.store.tokensStore.showCommunityAssetsInSend
}
Loader {
id: collectiblesAdaptorLoader
active: !d.isBridgeTx
sourceComponent: CollectiblesSelectionAdaptor {
accountKey: popup.preSelectedAccount ? popup.preSelectedAccount.address : ""
collectiblesModel: collectiblesStore
? collectiblesStore.jointCollectiblesBySymbolModel
: null
}
}
onAssetSelected: {
const entry = ModelUtils.getByKey(
assetsModel, "tokensKey", key)
d.selectedHoldingType = Constants.TokenType.ERC20
d.selectedHolding = entry
selectedItem = entry
}
onCollectibleSelected: {
const entry = ModelUtils.getByKey(
popup.store.collectiblesModel,
"uid", key)
d.selectedHoldingType = entry.tokenType
d.selectedHolding = entry
selectedItem = entry
}
onCollectionSelected: {
const entry = ModelUtils.getByKey(
popup.store.collectiblesModel,
"collectionUid", key)
d.selectedHoldingType = entry.tokenType
d.selectedHolding = entry
selectedItem = entry
}
}
Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
assetsModel: assetsAdaptor.model
collectiblesModel: popup.preSelectedAccount ? popup.nestedCollectiblesModel : null
networksModel: popup.store.flatNetworksModel
visible: (!!d.selectedHolding && d.selectedHoldingType !== Constants.TokenType.Unknown) ||
(!!d.hoveredHolding && d.hoveredHoldingType !== Constants.TokenType.Unknown)
onItemSelected: {
d.setSelectedHoldingId(holdingId, holdingType)
}
onSearchTextChanged: assetsAdaptor.assetSearchString = assetSearchString
formatCurrentCurrencyAmount: function(balance){
return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency)
}
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){
return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true})
}
} }
MaxSendButton { MaxSendButton {
@ -404,7 +432,7 @@ StatusDialog {
ColumnLayout { ColumnLayout {
spacing: 8 spacing: 8
Layout.fillWidth: true Layout.fillWidth: true
visible: !d.isBridgeTx && !!d.selectedHolding visible: !d.isBridgeTx
StatusBaseText { StatusBaseText {
id: label id: label
elide: Text.ElideRight elide: Text.ElideRight
@ -429,39 +457,6 @@ StatusDialog {
} }
} }
TokenListView {
id: tokenListRect
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
Layout.leftMargin: Style.current.xlPadding
Layout.rightMargin: Style.current.xlPadding
Layout.bottomMargin: Style.current.xlPadding + Style.current.padding
visible: !d.selectedHolding
assets: assetsAdaptor.model
collectibles: popup.preSelectedAccount ? popup.nestedCollectiblesModel : null
networksModel: popup.store.flatNetworksModel
onlyAssets: holdingSelector.onlyAssets
onTokenSelected: function (symbolOrTokenKey, holdingType) {
d.setSelectedHoldingId(symbolOrTokenKey, holdingType)
}
onTokenHovered: {
if(hovered) {
d.setHoveredHoldingId(symbol, holdingType)
} else {
d.setHoveredHoldingId("", Constants.TokenType.Unknown)
}
}
onAssetSearchStringChanged: assetsAdaptor.assetSearchString = assetSearchString
formatCurrentCurrencyAmount: function(balance){
return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency)
}
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals) {
return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true})
}
}
RecipientSelectorPanel { RecipientSelectorPanel {
id: recipientsPanel id: recipientsPanel
@ -472,9 +467,7 @@ StatusDialog {
Layout.rightMargin: Style.current.xlPadding Layout.rightMargin: Style.current.xlPadding
Layout.bottomMargin: Style.current.padding Layout.bottomMargin: Style.current.padding
// TODO: To be removed after all other refactors done (initial tokens selector page removed, bridge modal separated) visible: !recipientInputLoader.ready && !d.isBridgeTx
// This panel must be shown by default if no recipient already selected, otherwise, hidden
visible: !recipientInputLoader.ready && !d.isBridgeTx && !!d.selectedHolding
savedAddressesModel: popup.store.savedAddressesModel savedAddressesModel: popup.store.savedAddressesModel
myAccountsModel: d.accountsAdaptor.model myAccountsModel: d.accountsAdaptor.model
@ -513,7 +506,8 @@ StatusDialog {
contentWidth: availableWidth contentWidth: availableWidth
visible: recipientInputLoader.ready && !!d.selectedHolding && (amountToSendInput.inputNumberValid || d.isCollectiblesTransfer) visible: recipientInputLoader.ready &&
(amountToSendInput.inputNumberValid || d.isCollectiblesTransfer)
objectName: "sendModalScroll" objectName: "sendModalScroll"

View File

@ -83,6 +83,8 @@ QObject {
ObjectProxyModel { ObjectProxyModel {
id: proxyModel id: proxyModel
objectName: "sendModalAssetsAdaptor_proxyModel"
sourceModel: root.tokensModel ?? null sourceModel: root.tokensModel ?? null
delegate: QObject { delegate: QObject {

View File

@ -180,7 +180,7 @@ Loader {
SendRecipientInput { SendRecipientInput {
width: parent.width width: parent.width
height: visible ? implicitHeight: 0 height: visible ? implicitHeight: 0
visible: !root.isBridgeTx && !!root.selectedAsset visible: !root.isBridgeTx
text: root.addressText text: root.addressText
function validateInput() { function validateInput() {