From 50ffbb9dce2b7d6ffd68fc3aa5916ec30c60ea06 Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Mon, 11 Sep 2023 07:20:36 -0300 Subject: [PATCH] feat(@desktop/wallet): add collectibles list to send modal Fixes #12072 --- storybook/pages/SendModalPage.qml | 74 ++++ storybook/src/Models/WalletAssetsModel.qml | 14 +- .../src/Models/WalletCollectiblesModel.qml | 88 +++++ .../Models/WalletNestedCollectiblesModel.qml | 103 ++++++ storybook/src/Models/qmldir | 2 + ui/app/AppLayouts/Browser/BrowserLayout.qml | 3 +- .../Profile/views/EnsDetailsView.qml | 3 +- .../Profile/views/EnsSearchView.qml | 3 +- .../views/EnsTermsAndConditionsView.qml | 3 +- .../AppLayouts/Wallet/panels/WalletFooter.qml | 4 + ui/app/AppLayouts/Wallet/stores/RootStore.qml | 18 + .../Wallet/views/CollectiblesView.qml | 4 +- .../AppLayouts/Wallet/views/RightTabView.qml | 13 + ui/app/mainui/AppMain.qml | 13 +- .../controls/CollectibleNestedDelegate.qml | 62 ++++ ui/imports/shared/controls/qmldir | 1 + .../shared/panels/HoldingItemSelector.qml | 147 ++++++++ ui/imports/shared/panels/HoldingSelector.qml | 346 ++++++++++++++++++ ui/imports/shared/panels/qmldir | 3 + ui/imports/shared/popups/SendModal.qml | 126 ++++--- .../shared/status/StatusStickerMarket.qml | 3 +- .../status/StatusStickerPackClickPopup.qml | 3 +- ui/imports/shared/stores/TransactionStore.qml | 66 ++++ ui/imports/shared/views/TokenListView.qml | 8 +- ui/imports/utils/Constants.qml | 4 + 25 files changed, 1048 insertions(+), 66 deletions(-) create mode 100644 storybook/src/Models/WalletCollectiblesModel.qml create mode 100644 storybook/src/Models/WalletNestedCollectiblesModel.qml create mode 100644 ui/imports/shared/controls/CollectibleNestedDelegate.qml create mode 100644 ui/imports/shared/panels/HoldingItemSelector.qml create mode 100644 ui/imports/shared/panels/HoldingSelector.qml diff --git a/storybook/pages/SendModalPage.qml b/storybook/pages/SendModalPage.qml index fd07c520c6..3b14da4141 100644 --- a/storybook/pages/SendModalPage.qml +++ b/storybook/pages/SendModalPage.qml @@ -9,6 +9,8 @@ import utils 1.0 import shared.popups 1.0 import shared.stores 1.0 +import StatusQ.Core.Utils 0.1 + SplitView { id: root @@ -32,11 +34,14 @@ SplitView { visible: true modal: false closePolicy: Popup.NoAutoClose + onlyAssets: false store: TransactionStore { readonly property QtObject selectedSenderAccount: QtObject { readonly property var assets: WalletAssetsModel {} } + readonly property QtObject collectiblesModel: WalletCollectiblesModel {} + readonly property QtObject nestedCollectiblesModel: WalletNestedCollectiblesModel {} readonly property QtObject walletSectionSendInst: QtObject {} readonly property QtObject mainModuleInst: QtObject {} @@ -61,6 +66,75 @@ SplitView { return "" } + function getAsset(assetsList, symbol) { + const idx = ModelUtils.indexOf(assetsList, "symbol", symbol) + if (idx < 0) { + return {} + } + return ModelUtils.get(assetsList, idx) + } + + function getCollectible(uid) { + const idx = ModelUtils.indexOf(collectiblesModel, "uid", uid) + if (idx < 0) { + return {} + } + return ModelUtils.get(collectiblesModel, idx) + } + + function getSelectorCollectible(uid) { + const idx = ModelUtils.indexOf(nestedCollectiblesModel, "uid", uid) + if (idx < 0) { + return {} + } + return ModelUtils.get(nestedCollectiblesModel, idx) + } + + function getHolding(holdingId, holdingType) { + if (holdingType === Constants.HoldingType.Asset) { + return getAsset(selectedSenderAccount.assets, holdingId) + } else if (holdingType === Constants.HoldingType.Collectible) { + return getCollectible(holdingId) + } else { + return {} + } + } + + function getSelectorHolding(holdingId, holdingType) { + if (holdingType === Constants.HoldingType.Asset) { + return getAsset(selectedSenderAccount.assets, holdingId) + } else if (holdingType === Constants.HoldingType.Collectible) { + return getSelectorCollectible(holdingId) + } else { + return {} + } + } + + function assetToSelectorAsset(asset) { + return asset + } + + function collectibleToSelectorCollectible(collectible) { + return { + uid: collectible.uid, + chainId: collectible.chainId, + name: collectible.name, + iconUrl: collectible.imageUrl, + collectionUid: collectible.collectionUid, + collectionName: collectible.collectionName, + isCollection: false + } + } + + function holdingToSelectorHolding(holding, holdingType) { + if (holdingType === Constants.HoldingType.Asset) { + return assetToSelectorAsset(holding) + } else if (holdingType === Constants.HoldingType.Collectible) { + return collectibleToSelectorCollectible(holding) + } else { + return {} + } + } readonly property string currentCurrency: "USD" diff --git a/storybook/src/Models/WalletAssetsModel.qml b/storybook/src/Models/WalletAssetsModel.qml index f39f458d49..1b514de1dc 100644 --- a/storybook/src/Models/WalletAssetsModel.qml +++ b/storybook/src/Models/WalletAssetsModel.qml @@ -3,7 +3,12 @@ import QtQuick 2.15 ListModel { readonly property var data: [ { - totalBalance: 323.3, + totalBalance: ({ + displayDecimals: true, + stripTrailingZeroes: true, + amount: 323.3, + symbol: "ETH" + }), decimals: 2, totalCurrencyBalance: ({ displayDecimals: true, @@ -27,7 +32,12 @@ ListModel { allChecked: true }, { - totalBalance: 324343.3, + totalBalance: ({ + displayDecimals: true, + stripTrailingZeroes: true, + amount: 324343.3, + symbol: "SNT" + }), decimals: 2, totalCurrencyBalance: ({ displayDecimals: true, diff --git a/storybook/src/Models/WalletCollectiblesModel.qml b/storybook/src/Models/WalletCollectiblesModel.qml new file mode 100644 index 0000000000..8ccc0c73a7 --- /dev/null +++ b/storybook/src/Models/WalletCollectiblesModel.qml @@ -0,0 +1,88 @@ +import QtQuick 2.15 + +ListModel { + readonly property var rootData: [ + { + uid: "ID-Kitty1", + chainId: 1, + contractAddress: "0x1", + tokenId: "1", + name: "Furbeard", + imageUrl: ModelsData.collectibles.kitty1Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + }, + { + uid: "ID-Kitty2", + chainId: 1, + contractAddress: "0x1", + tokenId: "2", + name: "Magicat", + imageUrl: ModelsData.collectibles.kitty2Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + }, + { + uid: "ID-Kitty3", + chainId: 1, + contractAddress: "0x1", + tokenId: "3", + name: "Happy Meow", + imageUrl: ModelsData.collectibles.kitty3Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + }, + { + uid: "ID-Kitty4", + chainId: 1, + contractAddress: "0x1", + tokenId: "4", + name: "Furbeard-2", + imageUrl: ModelsData.collectibles.kitty4Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + }, + { + uid: "ID-Kitty5", + chainId: 1, + contractAddress: "0x1", + tokenId: "4", + name: "Magicat-3", + imageUrl: ModelsData.collectibles.kitty5Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties" + }, + { + uid: "ID-Anniversary", + chainId: 1, + contractAddress: "0x2", + tokenId: "1", + name: "Anniversary", + imageUrl: ModelsData.collectibles.anniversary, + collectionUid: "anniversary", + collectionName: "Anniversary", + }, + { + uid: "ID-SuperRare", + chainId: 1, + contractAddress: "0x3", + tokenId: "101", + name: "SuperRare", + imageUrl: ModelsData.collectibles.superRare, + collectionUid: "super-rare", + collectionName: "SuperRare", + }, + { + uid: "ID-Custom", + chainId: 1, + contractAddress: "0x04", + tokenId: "403", + name: "Custom Collectible", + imageUrl: ModelsData.collectibles.custom, + collectionUid: "custom", + collectionName: "Custom", + } + ] + + Component.onCompleted: append(rootData) +} diff --git a/storybook/src/Models/WalletNestedCollectiblesModel.qml b/storybook/src/Models/WalletNestedCollectiblesModel.qml new file mode 100644 index 0000000000..7243b56c42 --- /dev/null +++ b/storybook/src/Models/WalletNestedCollectiblesModel.qml @@ -0,0 +1,103 @@ +import QtQuick 2.15 + +ListModel { + readonly property var rootData: [ + { + uid: "ID-Anniversary", + chainId: 1, + name: "Anniversary", + iconUrl: ModelsData.collectibles.anniversary, + collectionUid: "anniversary", + collectionName: "Anniversary", + isCollection: false, + }, + { + uid: "ID-SuperRare", + chainId: 1, + name: "SuperRare", + iconUrl: ModelsData.collectibles.superRare, + collectionUid: "super-rare", + collectionName: "SuperRare", + isCollection: false, + }, + { + uid: "cryptokitties", + chainId: 1, + name: "CryptoKitties", + iconUrl: ModelsData.collectibles.cryptoKitties, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + isCollection: true, + }, + { + uid: "ID-Custom", + chainId: 1, + name: "Custom Collectible", + iconUrl: ModelsData.collectibles.custom, + collectionUid: "custom", + collectionName: "Custom", + isCollection: false, + } + ] + + readonly property var criptoKittiesData: [ + { + uid: "ID-Kitty1", + chainId: 1, + name: "Furbeard", + iconUrl: ModelsData.collectibles.kitty1Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + isCollection: false, + }, + { + uid: "ID-Kitty2", + chainId: 1, + name: "Magicat", + iconUrl: ModelsData.collectibles.kitty2Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + isCollection: false, + }, + { + uid: "ID-Kitty3", + chainId: 1, + name: "Happy Meow", + iconUrl: ModelsData.collectibles.kitty3Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + isCollection: false, + }, + { + uid: "ID-Kitty4", + chainId: 1, + name: "Furbeard-2", + iconUrl: ModelsData.collectibles.kitty4Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + isCollection: false, + }, + { + uid: "ID-Kitty5", + chainId: 1, + name: "Magicat-3", + iconUrl: ModelsData.collectibles.kitty5Big, + collectionUid: "cryptokitties", + collectionName: "CryptoKitties", + isCollection: false, + } + ] + + property string currentCollectionUid + + onCurrentCollectionUidChanged: { + clear() + if (currentCollectionUid === "") { + append(rootData) + } else if (currentCollectionUid === "cryptokitties") { + append(criptoKittiesData) + } + } + + Component.onCompleted: append(rootData) +} diff --git a/storybook/src/Models/qmldir b/storybook/src/Models/qmldir index f6df37989d..55c99f96e5 100644 --- a/storybook/src/Models/qmldir +++ b/storybook/src/Models/qmldir @@ -12,7 +12,9 @@ TokenHoldersModel 1.0 TokenHoldersModel.qml UsersModel 1.0 UsersModel.qml WalletAccountsModel 1.0 WalletAccountsModel.qml WalletAssetsModel 1.0 WalletAssetsModel.qml +WalletCollectiblesModel 1.0 WalletCollectiblesModel.qml WalletKeyPairModel 1.0 WalletKeyPairModel.qml +WalletNestedCollectiblesModel 1.0 WalletNestedCollectiblesModel.qml singleton ModelsData 1.0 ModelsData.qml singleton NetworksModel 1.0 NetworksModel.qml singleton PermissionsModel 1.0 PermissionsModel.qml diff --git a/ui/app/AppLayouts/Browser/BrowserLayout.qml b/ui/app/AppLayouts/Browser/BrowserLayout.qml index 6e0041355d..f4d99f789e 100644 --- a/ui/app/AppLayouts/Browser/BrowserLayout.qml +++ b/ui/app/AppLayouts/Browser/BrowserLayout.qml @@ -64,7 +64,8 @@ StatusSectionLayout { property Component sendTransactionModalComponent: SendModal { anchors.centerIn: parent selectedAccount: WalletStore.dappBrowserAccount - preSelectedAsset: store.getAsset(WalletStore.dappBrowserAccount.assets, "ETH") + preSelectedHolding: store.getAsset(WalletStore.dappBrowserAccount.assets, "ETH") + preSelectedHoldingType: Constants.HoldingType.Asset } property Component signMessageModalComponent: SignMessageModal {} diff --git a/ui/app/AppLayouts/Profile/views/EnsDetailsView.qml b/ui/app/AppLayouts/Profile/views/EnsDetailsView.qml index ecff6d3fcc..92fa031d6c 100644 --- a/ui/app/AppLayouts/Profile/views/EnsDetailsView.qml +++ b/ui/app/AppLayouts/Profile/views/EnsDetailsView.qml @@ -120,7 +120,8 @@ Item { sendType: Constants.SendType.ENSRelease preSelectedRecipient: root.ensUsernamesStore.getEnsRegisteredAddress() preDefinedAmountToSend: LocaleUtils.numberToLocaleString(0) - preSelectedAsset: store.getAsset(releaseEnsModal.store.assets, "ETH") + preSelectedHolding: store.getAsset(releaseEnsModal.store.assets, Constants.ethToken) + preSelectedHoldingType: Constants.HoldingType.Asset sendTransaction: function() { if(bestRoutes.count === 1) { let path = bestRoutes.firstItem() diff --git a/ui/app/AppLayouts/Profile/views/EnsSearchView.qml b/ui/app/AppLayouts/Profile/views/EnsSearchView.qml index 0938ba5f45..96d155e073 100644 --- a/ui/app/AppLayouts/Profile/views/EnsSearchView.qml +++ b/ui/app/AppLayouts/Profile/views/EnsSearchView.qml @@ -66,7 +66,8 @@ Item { sendType: Constants.SendType.ENSSetPubKey preSelectedRecipient: root.ensUsernamesStore.getEnsRegisteredAddress() preDefinedAmountToSend: LocaleUtils.numberToLocaleString(0) - preSelectedAsset: store.getAsset(connectEnsModal.store.assets, "ETH") + preSelectedHolding: store.getAsset(connectEnsModal.store.assets, Constants.ethToken) + preSelectedHoldingType: Constants.HoldingType.Asset sendTransaction: function() { if(bestRoutes.count === 1) { let path = bestRoutes.firstItem() diff --git a/ui/app/AppLayouts/Profile/views/EnsTermsAndConditionsView.qml b/ui/app/AppLayouts/Profile/views/EnsTermsAndConditionsView.qml index bc34b5312b..3c76883914 100644 --- a/ui/app/AppLayouts/Profile/views/EnsTermsAndConditionsView.qml +++ b/ui/app/AppLayouts/Profile/views/EnsTermsAndConditionsView.qml @@ -50,7 +50,8 @@ Item { sendType: Constants.SendType.ENSRegister preSelectedRecipient: root.ensUsernamesStore.getEnsRegisteredAddress() preDefinedAmountToSend: LocaleUtils.numberToLocaleString(10) - preSelectedAsset: store.getAsset(buyEnsModal.store.assets, JSON.parse(root.stickersStore.getStatusToken()).symbol) + preSelectedHolding: store.getAsset(buyEnsModal.store.assets, JSON.parse(root.stickersStore.getStatusToken()).symbol) + preSelectedHoldingType: Constants.HoldingType.Asset sendTransaction: function() { if(bestRoutes.count === 1) { let path = bestRoutes.firstItem() diff --git a/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml b/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml index 4ff41f6b04..de96d23678 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml @@ -40,6 +40,8 @@ Rectangle { text: qsTr("Send") interactive: networkConnectionStore.sendBuyBridgeEnabled onClicked: function() { + sendModal.preSelectedHoldingID = walletStore.currentViewedHoldingID + sendModal.preSelectedHoldingType = walletStore.currentViewedHoldingType sendModal.open() } tooltipText: networkConnectionStore.sendBuyBridgeToolTipText @@ -61,6 +63,8 @@ Rectangle { interactive: networkConnectionStore.sendBuyBridgeEnabled onClicked: function() { sendModal.isBridgeTx = true + sendModal.preSelectedHoldingID = walletStore.currentViewedHoldingID + sendModal.preSelectedHoldingType = walletStore.currentViewedHoldingType sendModal.open() } tooltipText: networkConnectionStore.sendBuyBridgeToolTipText diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index 81a87e6707..2cc5af012e 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -118,6 +118,24 @@ QtObject { property var cryptoRampServicesModel: walletSectionBuySellCrypto.model + function resetCurrentViewedHolding() { + currentViewedHoldingID = "" + currentViewedHoldingType = null + } + + function setCurrentViewedHoldingType(type) { + currentViewedHoldingID = "" + currentViewedHoldingType = type + } + + function setCurrentViewedHolding(id, type) { + currentViewedHoldingID = id + currentViewedHoldingType = type + } + + property string currentViewedHoldingID: "" + property var currentViewedHoldingType + // This should be exposed to the UI via "walletModule", WalletModule should use // Accounts Service which keeps the info about that (isFirstTimeAccountLogin). // Then in the View of WalletModule we may have either QtProperty or diff --git a/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml b/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml index 0585d830d6..e98d6d7eeb 100644 --- a/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml +++ b/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml @@ -16,7 +16,7 @@ Item { property var collectiblesModel width: parent.width - signal collectibleClicked(int chainId, string contractAddress, string tokenId) + signal collectibleClicked(int chainId, string contractAddress, string tokenId, string uid) Loader { id: contentLoader @@ -64,7 +64,7 @@ Item { backgroundColor: model.backgroundColor ? model.backgroundColor : "transparent" isLoading: !!model.isLoading - onClicked: root.collectibleClicked(model.chainId, model.contractAddress, model.tokenId) + onClicked: root.collectibleClicked(model.chainId, model.contractAddress, model.tokenId, model.uid) } ScrollBar.vertical: StatusScrollBar {} diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index 454dedd73f..34623fe485 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -8,6 +8,7 @@ import utils 1.0 import shared.controls 1.0 import shared.views 1.0 import shared.stores 1.0 +import shared.panels 1.0 import "./" import "../stores" @@ -124,6 +125,7 @@ Item { assetDetailsLaunched: stack.currentIndex === 2 onAssetClicked: { assetDetailView.token = token + RootStore.setCurrentViewedHolding(token.symbol, Constants.HoldingType.Asset) stack.currentIndex = 2 } } @@ -131,6 +133,7 @@ Item { collectiblesModel: RootStore.collectiblesStore.ownedCollectibles onCollectibleClicked: { RootStore.collectiblesStore.getDetailedCollectible(chainId, contractAddress, tokenId) + RootStore.setCurrentViewedHolding(uid, Constants.HoldingType.Collectible) stack.currentIndex = 1 } } @@ -151,6 +154,11 @@ Item { Layout.fillHeight: true collectible: RootStore.collectiblesStore.detailedCollectible isCollectibleLoading: RootStore.collectiblesStore.isDetailedCollectibleLoading + + onVisibleChanged: { + if (!visible) + RootStore.resetCurrentViewedHolding() + } } AssetsDetailView { id: assetDetailView @@ -163,6 +171,11 @@ Item { address: RootStore.overview.mixedcaseAddress networkConnectionStore: root.networkConnectionStore + + onVisibleChanged: { + if (!visible) + RootStore.resetCurrentViewedHolding() + } } TransactionDetailView { diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 4467287c5b..37e4bda264 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -1345,18 +1345,29 @@ Item { } property var selectedAccount property bool isBridgeTx + property string preSelectedHoldingID + property var preSelectedHoldingType + sourceComponent: SendModal { + onlyAssets: false onClosed: { sendModal.closed() sendModal.isBridgeTx = false + sendModal.preSelectedHoldingID = "" + sendModal.preSelectedHoldingType = Constants.HoldingType.Unknown } } onLoaded: { if (!!sendModal.selectedAccount) { item.selectedAccount = sendModal.selectedAccount } - if(isBridgeTx) + if(isBridgeTx) { item.isBridgeTx = sendModal.isBridgeTx + } + if(preSelectedHoldingType !== Constants.HoldingType.Unknown) { + item.preSelectedHoldingID = sendModal.preSelectedHoldingID + item.preSelectedHoldingType = sendModal.preSelectedHoldingType + } } } diff --git a/ui/imports/shared/controls/CollectibleNestedDelegate.qml b/ui/imports/shared/controls/CollectibleNestedDelegate.qml new file mode 100644 index 0000000000..0477de06b5 --- /dev/null +++ b/ui/imports/shared/controls/CollectibleNestedDelegate.qml @@ -0,0 +1,62 @@ +import QtQuick 2.13 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + +StatusListItem { + id: root + + property var getNetworkIcon: function(chainId) { + return "" + } + signal itemSelected(var selectedItem) + signal itemHovered(var selectedItem, bool hovered) + + QtObject { + id: d + + function selectItem() { + root.itemSelected(model) + } + } + + Connections { + target: root.sensor + function onContainsMouseChanged() { + root.itemHovered(model, root.sensor.containsMouse) + } + } + + title: name + statusListItemTitleAside.font.pixelSize: 15 + asset.name: iconUrl ? iconUrl : "" + asset.isImage: true + asset.width: 32 + asset.height: 32 + statusListItemLabel.color: Theme.palette.directColor1 + statusListItemInlineTagsSlot.spacing: 0 + + radius: sensor.containsMouse || root.highlighted ? 0 : 8 + color: sensor.containsMouse || root.highlighted ? Theme.palette.baseColor2 : "transparent" + + onClicked: d.selectItem() + + components: [ + StatusRoundedImage { + width: 20 + height: 20 + image.source: Style.svg("tiny/%1".arg(root.getNetworkIcon(chainId))) + visible: !isCollection && root.sensor.containsMouse + }, + StatusIcon { + icon: "tiny/chevron-right" + color: Theme.palette.baseColor1 + width: 16 + height: 16 + visible: isCollection + } + ] +} diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index 80274259c5..a6e8bce113 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -2,6 +2,7 @@ AddressInput 1.0 AddressInput.qml AmountInput 1.0 AmountInput.qml AssetAndAmountInput 1.0 AssetAndAmountInput.qml AssetDelegate 1.0 AssetDelegate.qml +CollectibleNestedDelegate 1.0 CollectibleNestedDelegate.qml ContactSelector 1.0 ContactSelector.qml ContactsListAndSearch 1.0 ContactsListAndSearch.qml CopyToClipBoardButton 1.0 CopyToClipBoardButton.qml diff --git a/ui/imports/shared/panels/HoldingItemSelector.qml b/ui/imports/shared/panels/HoldingItemSelector.qml new file mode 100644 index 0000000000..1a211da6d7 --- /dev/null +++ b/ui/imports/shared/panels/HoldingItemSelector.qml @@ -0,0 +1,147 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 + +import SortFilterProxyModel 0.2 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Backpressure 1.0 + +import shared.controls 1.0 +import utils 1.0 + +Item { + id: root + + property var comboBoxModel + + property var selectedItem + property var hoveredItem + property string defaultIconSource + property string placeholderText + + property var itemIconSourceFn: function (item) { + return "" + } + + property var itemTextFn: function (item) { + return "" + } + + property alias comboBoxControl: comboBox.control + property alias comboBoxDelegate: comboBox.delegate + property var comboBoxPopupHeader + + property int contentIconSize: 21 + property int contentTextSize: 28 + + function resetInternal() { + items = null + selectedItem = null + hoveredItem = null + } + + function openPopup() { + root.comboBoxControl.popup.open() + } + + implicitWidth: comboBox.width + implicitHeight: comboBox.implicitHeight + + onSelectedItemChanged: { + if (!!selectedItem) { + d.iconSource = itemIconSourceFn(selectedItem) ?? defaultIconSource + d.text = itemTextFn(selectedItem) ?? placeholderText + } + } + + onHoveredItemChanged: { + if (!!hoveredItem) { + d.iconSource = itemIconSourceFn(hoveredItem) ?? defaultIconSource + d.text = itemTextFn(hoveredItem) ?? placeholderText + } + } + + QtObject { + id: d + + property string iconSource: "" + property string text: "" + readonly property bool isItemSelected: !!root.selectedItem || !!root.hoveredItem + + } + + StatusComboBox { + id: comboBox + objectName: "assetSelectorButton" + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + control.padding: 4 + control.popup.width: 492 + control.popup.x: -root.x + control.popup.verticalPadding: 0 + + popupContentItemObjectName: "assetSelectorList" + + model: root.comboBoxModel + + control.background: Rectangle { + color: "transparent" + border.width: d.isItemSelected ? 0 : 1 + border.color: Theme.palette.directColor7 + radius: 12 + } + + contentItem: RowLayout { + id: rowLayout + implicitHeight: 38 + StatusRoundedImage { + Layout.preferredWidth: root.contentIconSize + Layout.preferredHeight: root.contentIconSize + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + visible: !!d.iconSource + image.source: d.iconSource + image.onStatusChanged: { + if (image.status === Image.Error) { + image.source = root.defaultIconSource + } + } + } + StatusBaseText { + Layout.alignment: Qt.AlignVCenter + font.pixelSize: root.contentTextSize + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + color: Theme.palette.miscColor1 + text: d.text + visible: d.isItemSelected + } + StatusIcon { + Layout.leftMargin: -3 + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + icon: "chevron-down" + color: Theme.palette.miscColor1 + visible: d.isItemSelected + } + } + + control.indicator: null + + Component.onCompleted: { + control.currentIndex = -1 + control.popup.contentItem.header = root.comboBoxPopupHeader + } + + control.popup.onOpened: { + control.currentIndex = -1 + } + } +} diff --git a/ui/imports/shared/panels/HoldingSelector.qml b/ui/imports/shared/panels/HoldingSelector.qml new file mode 100644 index 0000000000..3fa5e46e0e --- /dev/null +++ b/ui/imports/shared/panels/HoldingSelector.qml @@ -0,0 +1,346 @@ + +import QtQml 2.15 +import QtQuick 2.13 +import QtQuick.Layouts 1.13 + +import shared.controls 1.0 +import utils 1.0 + +import SortFilterProxyModel 0.2 + +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import "../controls" + +Item { + id: root + property var assetsModel + property var collectiblesModel + property string currentCurrencySymbol + property bool onlyAssets: true + + implicitWidth: holdingItemSelector.implicitWidth + implicitHeight: holdingItemSelector.implicitHeight + + property var searchAssetSymbolByAddressFn: function (address) { + return "" + } + + property var getNetworkIcon: function(chainId){ + return "" + } + + signal itemHovered(string holdingId, var holdingType) + signal itemSelected(string holdingId, var holdingType) + + property alias selectedItem: holdingItemSelector.selectedItem + property alias hoveredItem: holdingItemSelector.hoveredItem + + function setSelectedItem(item, holdingType) { + d.browsingHoldingType = holdingType + holdingItemSelector.selectedItem = null + d.currentHoldingType = holdingType + holdingItemSelector.selectedItem = item + } + + function setHoveredItem(item, holdingType) { + d.browsingHoldingType = holdingType + holdingItemSelector.hoveredItem = null + d.currentHoldingType = holdingType + holdingItemSelector.hoveredItem = item + } + + QtObject { + id: d + // Internal management properties and signals: + readonly property var holdingTypes: onlyAssets ? + [Constants.HoldingType.Asset] : + [Constants.HoldingType.Asset, Constants.HoldingType.Collectible] + + readonly property var tabsModel: onlyAssets ? + [qsTr("Assets")] : + [qsTr("Assets"), qsTr("Collectibles")] + + readonly property var updateSearchText: Backpressure.debounce(root, 1000, function(inputText) { + searchText = inputText + }) + + function isAsset(type) { + return type === Constants.HoldingType.Asset + } + + property int browsingHoldingType: Constants.HoldingType.Asset + readonly property bool isCurrentBrowsingTypeAsset: isAsset(browsingHoldingType) + readonly property bool isBrowsingCollection: !isCurrentBrowsingTypeAsset && !!collectiblesModel && collectiblesModel.currentCollectionUid !== "" + property string currentBrowsingCollectionName + + property var currentHoldingType: Constants.HoldingType.Unknown + + property string searchText + readonly property string assetSymbolByAddress: isCurrentBrowsingTypeAsset ? "": root.searchAssetSymbolByAddressFn(searchText) + readonly property string uppercaseSearchText: searchText.toUpperCase() + + property var assetTextFn: function (asset) { + return asset.symbol ? asset.symbol : "" + } + + property var assetIconSourceFn: function (asset) { + return asset.symbol ? Style.png("tokens/%1".arg(asset.symbol)) : "" + } + + property var assetComboBoxModel: SortFilterProxyModel { + sourceModel: root.assetsModel + filters: [ + ExpressionFilter { + expression: { + d.uppercaseSearchText; // Force re-evaluation when searchText changes + return visibleForNetwork && ( + d.uppercaseSearchText === "" || + symbol.startsWith(d.uppercaseSearchText) || + name.toUpperCase().startsWith(d.uppercaseSearchText) | + (d.assetSymbolByAddress !== "" && symbol.startsWith(d.assetSymbolByAddress)) + ) + } + } + ] + } + + property var collectibleTextFn: function (item) { + if (!!item) { + return !!item.collectionName ? item.collectionName + ": " + item.name : item.name + } + return "" + } + + property var collectibleIconSourceFn: function (item) { + return item.iconUrl ? item.iconUrl : "" + } + + property var collectibleComboBoxModel: SortFilterProxyModel { + sourceModel: root.collectiblesModel + filters: [ + ExpressionFilter { + expression: { + return d.uppercaseSearchText === "" || name.toUpperCase().startsWith(d.uppercaseSearchText) + } + } + ] + sorters: RoleSorter { + roleName: "isCollection" + sortOrder: Qt.DescendingOrder + } + } + + readonly property string searchPlaceholderText: { + if (isCurrentBrowsingTypeAsset) { + return qsTr("Search for token or enter token address") + } else if (isBrowsingCollection) { + return qsTr("Search %1").arg(d.currentBrowsingCollectionName ?? qsTr("collectibles in collection")) + } else { + return qsTr("Search collectibles") + } + } + + // By design values: + readonly property int padding: 16 + readonly property int headerTopMargin: 5 + readonly property int tabBarTopMargin: 20 + readonly property int tabBarHeight: 35 + readonly property int backButtonWidth: 56 + readonly property int backButtonHeight: 24 + readonly property int backButtonToContentSpace: 8 + readonly property int bottomInset: 20 + readonly property int assetContentIconSize: 21 + readonly property int collectibleContentIconSize: 28 + readonly property int assetContentTextSize: 28 + readonly property int collectibleContentTextSize: 15 + } + + HoldingItemSelector { + id: holdingItemSelector + anchors.fill: parent + + defaultIconSource: Style.png("tokens/DEFAULT-TOKEN@3x") + placeholderText: d.isCurrentBrowsingTypeAsset ? qsTr("Select token") : qsTr("Select collectible") + comboBoxDelegate: Item { + property var itemModel: model // read 'model' from the delegate's context + width: loader.width + height: loader.height + Loader { + id: loader + + // inject model properties to the loaded item's context + // common + property var model: itemModel + property var chainId: model.chainId + property var name: model.name + // asset + property var symbol: model.symbol + property var totalBalance: model.totalBalance + property var totalCurrencyBalance: model.totalCurrencyBalance + property var decimals: model.decimals + property var balances: model.balances + // collectible + property var uid: model.uid + property var iconUrl: model.iconUrl + property var collectionUid: model.collectionUid + property var collectionName: model.collectionName + property var isCollection: model.isCollection + + sourceComponent: d.isCurrentBrowsingTypeAsset ? assetComboBoxDelegate : collectibleComboBoxDelegate + } + } + + comboBoxPopupHeader: headerComponent + itemTextFn: d.isCurrentBrowsingTypeAsset ? d.assetTextFn : d.collectibleTextFn + itemIconSourceFn: d.isCurrentBrowsingTypeAsset ? d.assetIconSourceFn : d.collectibleIconSourceFn + comboBoxModel: d.isCurrentBrowsingTypeAsset ? d.assetComboBoxModel : d.collectibleComboBoxModel + + contentIconSize: d.isAsset(d.currentHoldingType) ? d.assetContentIconSize : d.collectibleContentIconSize + contentTextSize: d.isAsset(d.currentHoldingType) ? d.assetContentTextSize : d.collectibleContentTextSize + } + + Component { + id: headerComponent + ColumnLayout { + width: holdingItemSelector.comboBoxControl.popup.width + Layout.topMargin: d.headerTopMargin + spacing: -1 // Used to overlap rectangles from row components + + StatusTabBar { + id: tabBar + + visible: !root.onlyAssets + Layout.preferredHeight: d.tabBarHeight + Layout.fillWidth: true + Layout.leftMargin: d.padding + Layout.rightMargin: d.padding + Layout.topMargin: d.tabBarTopMargin + Layout.bottomMargin: 6 + currentIndex: d.holdingTypes.indexOf(d.browsingHoldingType) + + onCurrentIndexChanged: { + if (currentIndex >= 0) { + d.browsingHoldingType = d.holdingTypes[currentIndex] + } + } + + Repeater { + id: tabLabelsRepeater + model: d.tabsModel + + StatusTabButton { + text: modelData + width: implicitWidth + } + } + } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + visible: d.isBrowsingCollection + + color: "transparent" + border.color: Theme.palette.baseColor2 + border.width: 1 + + RowLayout{ + anchors.fill: parent + + StatusIconTextButton { + id: backButton + + Layout.preferredWidth: d.backButtonWidth + Layout.preferredHeight: d.backButtonHeight + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.leftMargin: d.padding + + statusIcon: "previous" + icon.width: 16 + icon.height: 16 + text: qsTr("Back") + + onClicked: { + if (!d.isCurrentBrowsingTypeAsset) { + root.collectiblesModel.currentCollectionUid = "" + } + } + } + StatusBaseText { + Layout.fillWidth: true + Layout.rightMargin: d.padding + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + text: "%1 %2".arg(collectiblesModel.count).arg(d.currentBrowsingCollectionName) + font.pixelSize: 13 + lineHeight: 18 + lineHeightMode: Text.FixedHeight + color: Theme.palette.baseColor1 + } + } + } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: searchInput.input.implicitHeight + + color: "transparent" + border.color: Theme.palette.baseColor2 + border.width: 1 + + StatusInput { + id: searchInput + anchors.fill: parent + + input.showBackground: false + placeholderText: d.searchPlaceholderText + onTextChanged: Qt.callLater(d.updateSearchText, text) + input.clearable: true + input.implicitHeight: 56 + input.rightComponent: StatusFlatRoundButton { + icon.name: "search" + type: StatusFlatRoundButton.Type.Secondary + enabled: false + } + } + } + } + } + + Component { + id: assetComboBoxDelegate + TokenBalancePerChainDelegate { + objectName: "AssetSelector_ItemDelegate_" + symbol + width: holdingItemSelector.comboBoxControl.popup.width + getNetworkIcon: root.getNetworkIcon + onTokenSelected: { + holdingItemSelector.selectedItem = selectedToken + d.currentHoldingType = Constants.HoldingType.Asset + root.itemSelected(selectedToken.symbol, Constants.HoldingType.Asset) + holdingItemSelector.comboBoxControl.popup.close() + } + } + } + + Component { + id: collectibleComboBoxDelegate + CollectibleNestedDelegate { + objectName: "CollectibleSelector_ItemDelegate_" + collectionUid + width: holdingItemSelector.comboBoxControl.popup.width + getNetworkIcon: root.getNetworkIcon + onItemSelected: { + if (isCollection) { + d.currentBrowsingCollectionName = collectionName + root.collectiblesModel.currentCollectionUid = collectionUid + } else { + holdingItemSelector.selectedItem = selectedItem + d.currentHoldingType = Constants.HoldingType.Collectible + root.itemSelected(selectedItem.uid, Constants.HoldingType.Collectible) + holdingItemSelector.comboBoxControl.popup.close() + } + } + } + } +} diff --git a/ui/imports/shared/panels/qmldir b/ui/imports/shared/panels/qmldir index 6f05e1fa2c..60a81867cc 100644 --- a/ui/imports/shared/panels/qmldir +++ b/ui/imports/shared/panels/qmldir @@ -22,6 +22,9 @@ Separator 1.0 Separator.qml SeparatorWithIcon 1.0 SeparatorWithIcon.qml SequenceColumnLayout 1.0 SequenceColumnLayout.qml SplitViewHandle 1.0 SplitViewHandle.qml +HoldingItemSelector 1.0 HoldingItemSelector.qml +HoldingSelector 1.0 HoldingSelector.qml +HoldingTypes 1.0 HoldingTypes.qml StatusAssetSelector 1.0 StatusAssetSelector.qml StyledText 1.0 StyledText.qml TextWithLabel 1.0 TextWithLabel.qml diff --git a/ui/imports/shared/popups/SendModal.qml b/ui/imports/shared/popups/SendModal.qml index b518afe03f..1168112788 100644 --- a/ui/imports/shared/popups/SendModal.qml +++ b/ui/imports/shared/popups/SendModal.qml @@ -28,14 +28,19 @@ StatusDialog { property string preSelectedRecipient property string preDefinedAmountToSend - property var preSelectedAsset + property var preSelectedHolding + property string preSelectedHoldingID + property var preSelectedHoldingType property bool interactive: true + property alias onlyAssets: holdingSelector.onlyAssets property alias modalHeader: modalHeader.text property var store: TransactionStore{} property var currencyStore: store.currencyStore property var selectedAccount: store.selectedSenderAccount + property var collectiblesModel: store.collectiblesModel + property var nestedCollectiblesModel: store.nestedCollectiblesModel property var bestRoutes property alias addressText: recipientLoader.addressText property bool isLoading: false @@ -59,9 +64,9 @@ StatusDialog { } property var recalculateRoutesAndFees: Backpressure.debounce(popup, 600, function() { - if(!!popup.selectedAccount && !!assetSelector.selectedAsset && recipientLoader.ready && amountToSendInput.inputNumberValid) { + if(!!popup.selectedAccount && d.isSelectedHoldingValidAsset && recipientLoader.ready && amountToSendInput.inputNumberValid) { popup.isLoading = true - let amount = Math.round(amountToSendInput.cryptoValueToSend * Math.pow(10, assetSelector.selectedAsset.decimals)) + let amount = Math.round(amountToSendInput.cryptoValueToSend * Math.pow(10, d.selectedHolding.decimals)) popup.store.suggestedRoutes(amount.toString(16), popup.sendType) } }) @@ -72,8 +77,8 @@ StatusDialog { (popup.bestRoutes && popup.bestRoutes.count === 0 && !!amountToSendInput.input.text && recipientLoader.ready && !popup.isLoading) ? Constants.NoRoute : Constants.NoError - readonly property double maxFiatBalance: !!assetSelector.selectedAsset ? assetSelector.selectedAsset.totalCurrencyBalance.amount : 0 - readonly property double maxCryptoBalance: !!assetSelector.selectedAsset ? assetSelector.selectedAsset.totalBalance.amount : 0 + readonly property double maxFiatBalance: isSelectedHoldingValidAsset ? selectedHolding.totalCurrencyBalance.amount : 0 + readonly property double maxCryptoBalance: isSelectedHoldingValidAsset ? selectedHolding.totalBalance.amount : 0 readonly property double maxInputBalance: amountToSendInput.inputIsFiat ? maxFiatBalance : maxCryptoBalance readonly property string selectedSymbol: store.selectedAssetSymbol readonly property string inputSymbol: amountToSendInput.inputIsFiat ? popup.currencyStore.currentCurrency : selectedSymbol @@ -83,6 +88,37 @@ StatusDialog { property string totalTimeEstimate property double totalFeesInFiat property double totalAmountToReceive + + property var selectedHolding: null + property var selectedHoldingType: Constants.HoldingType.Unknown + readonly property bool isSelectedHoldingValidAsset: !!selectedHolding && selectedHoldingType === Constants.HoldingType.Asset + property var hoveredHolding: null + property var hoveredHoldingType: Constants.HoldingType.Unknown + readonly property bool isHoveredHoldingValidAsset: !!hoveredHolding && hoveredHoldingType === Constants.HoldingType.Asset + + function setSelectedHoldingId(holdingId, holdingType) { + let holding = store.getHolding(holdingId, holdingType) + setSelectedHolding(holding, holdingType) + } + + function setSelectedHolding(holding, holdingType) { + d.selectedHolding = holding + d.selectedHoldingType = holdingType + let selectorHolding = store.holdingToSelectorHolding(holding, holdingType) + holdingSelector.setSelectedItem(selectorHolding, holdingType) + } + + function setHoveredHoldingId(holdingId, holdingType) { + let holding = store.getHolding(holdingId, holdingType) + setHoveredHolding(holding, holdingType) + } + + function setHoveredHolding(holding, holdingType) { + d.hoveredHolding = holding + d.hoveredHoldingType = holdingType + let selectorHolding = store.holdingToSelectorHolding(holding, holdingType) + holdingSelector.setHoveredItem(selectorHolding, holdingType) + } } width: 556 @@ -99,8 +135,12 @@ StatusDialog { onOpened: { amountToSendInput.input.input.edit.forceActiveFocus() - if(!!popup.preSelectedAsset) { - assetSelector.selectedAsset = popup.preSelectedAsset + if (popup.preSelectedHoldingType !== Constants.HoldingType.Unknown) { + if(!!popup.preSelectedHolding) { + d.setSelectedHolding(popup.preSelectedHolding, popup.preSelectedHoldingType) + } else if (!!popup.preSelectedHoldingID) { + d.setSelectedHoldingId(popup.preSelectedHoldingID, popup.preSelectedHoldingType) + } } if(!!popup.preDefinedAmountToSend) { @@ -118,8 +158,6 @@ StatusDialog { } } - onClosed: popup.store.resetTxStoreProperties() - header: AccountsModalHeader { anchors.top: parent.top anchors.topMargin: -height - 18 @@ -176,6 +214,7 @@ StatusDialog { StatusBaseText { id: modalHeader Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + verticalAlignment: Text.AlignVCenter text: popup.isBridgeTx ? qsTr("Bridge") : qsTr("Send") font.pixelSize: 28 lineHeight: 38 @@ -184,45 +223,29 @@ StatusDialog { color: Theme.palette.directColor1 Layout.maximumWidth: contentWidth } - StatusAssetSelector { - id: assetSelector + HoldingSelector { + id: holdingSelector Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - enabled: popup.interactive - assets: popup.selectedAccount && popup.selectedAccount.assets ? popup.selectedAccount.assets : null - defaultToken: Style.png("tokens/DEFAULT-TOKEN@3x") - placeholderText: qsTr("Select token") + Layout.fillHeight: true + assetsModel: popup.selectedAccount && popup.selectedAccount.assets ? popup.selectedAccount.assets : null + collectiblesModel: popup.selectedAccount ? popup.nestedCollectiblesModel : null currentCurrencySymbol: RootStore.currencyStore.currentCurrencySymbol - tokenAssetSourceFn: function (symbol) { - return symbol ? Style.png("tokens/%1".arg(symbol)) : defaultToken - } - searchTokenSymbolByAddressFn: function (address) { - return store.findTokenSymbolByAddress(address) - } + visible: !!d.selectedHolding || !!d.hoveredHolding getNetworkIcon: function(chainId){ return RootStore.getNetworkIcon(chainId) } - onAssetsChanged: { - // Todo we should not need to do this, this should be automatic when selected account changes - if(!!selectedAccount && !!assetSelector.selectedAsset) - assetSelector.selectedAsset = store.getAsset(selectedAccount.assets, assetSelector.selectedAsset.symbol) + onItemSelected: { + d.setSelectedHoldingId(holdingId, holdingType) } - onSelectedAssetChanged: { - store.setSelectedAssetSymbol(assetSelector.selectedAsset.symbol) - if (!assetSelector.selectedAsset || !amountToSendInput.inputNumberValid) { - return - } - popup.recalculateRoutesAndFees() - } - visible: !!assetSelector.selectedAsset || !!assetSelector.hoveredToken } + StatusListItemTag { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.preferredHeight: 22 - visible: !!assetSelector.selectedAsset || !!assetSelector.hoveredToken + visible: d.isSelectedHoldingValidAsset || d.isHoveredHoldingValidAsset title: { - if(!!assetSelector.hoveredToken) { - const balance = popup.currencyStore.formatCurrencyAmount((amountToSendInput.inputIsFiat ? assetSelector.hoveredToken.totalCurrencyBalance.amount : assetSelector.hoveredToken.totalBalance.amount) , assetSelector.hoveredToken.symbol) + if(d.isHoveredHoldingValidAsset) { + const balance = popup.currencyStore.formatCurrencyAmount((amountToSendInput.inputIsFiat ? d.hoveredHolding.totalCurrencyBalance.amount : d.hoveredHolding.totalBalance.amount) , d.hoveredHolding.symbol) return qsTr("Max: %1").arg(balance) } if (d.maxInputBalance <= 0) @@ -245,7 +268,7 @@ StatusDialog { Layout.fillWidth: true - visible: !assetSelector.selectedAsset + visible: !d.selectedHolding assets: popup.selectedAccount && popup.selectedAccount.assets ? popup.selectedAccount.assets : null searchTokenSymbolByAddressFn: function (address) { return store.findTokenSymbolByAddress(address) @@ -254,17 +277,18 @@ StatusDialog { return RootStore.getNetworkIcon(chainId) } onTokenSelected: { - assetSelector.selectedAsset = selectedToken + d.setSelectedHoldingId(symbol, Constants.HoldingType.Asset) } onTokenHovered: { - if(hovered) - assetSelector.hoveredToken = selectedToken - else - assetSelector.hoveredToken = null + if(hovered) { + d.setHoveredHoldingId(symbol, Constants.HoldingType.Asset) + } else { + d.setHoveredHoldingId("", Constants.HoldingType.Unknown) + } } } RowLayout { - visible: !!assetSelector.selectedAsset + visible: d.isSelectedHoldingValidAsset AmountToSend { id: amountToSendInput Layout.fillWidth:true @@ -342,7 +366,7 @@ StatusDialog { anchors.right: parent.right anchors.leftMargin: Style.current.bigPadding anchors.rightMargin: Style.current.bigPadding - visible: !isBridgeTx && !!assetSelector.selectedAsset + visible: !isBridgeTx && !!d.selectedHolding StatusBaseText { id: label elide: Text.ElideRight @@ -356,7 +380,7 @@ StatusDialog { store: popup.store isBridgeTx: popup.isBridgeTx interactive: popup.interactive - selectedAsset: assetSelector.selectedAsset + selectedAsset: d.selectedHolding onIsLoading: popup.isLoading = true onRecalculateRoutesAndFees: popup.recalculateRoutesAndFees() } @@ -374,7 +398,7 @@ StatusDialog { recipientLoader.selectedRecipientType = type recipientLoader.selectedRecipient = recipient } - visible: !recipientLoader.ready && !isBridgeTx && !!assetSelector.selectedAsset + visible: !recipientLoader.ready && !isBridgeTx && !!d.selectedHolding } NetworkSelector { @@ -390,9 +414,9 @@ StatusDialog { amountToSend: amountToSendInput.cryptoValueToSend minSendCryptoDecimals: amountToSendInput.minSendCryptoDecimals minReceiveCryptoDecimals: amountToSendInput.minReceiveCryptoDecimals - selectedAsset: assetSelector.selectedAsset + selectedAsset: d.selectedHolding onReCalculateSuggestedRoute: popup.recalculateRoutesAndFees() - visible: recipientLoader.ready && !!assetSelector.selectedAsset && amountToSendInput.inputNumberValid + visible: recipientLoader.ready && !!d.selectedHolding && amountToSendInput.inputNumberValid errorType: d.errorType isLoading: popup.isLoading isBridgeTx: popup.isBridgeTx @@ -404,7 +428,7 @@ StatusDialog { anchors.right: parent.right anchors.leftMargin: Style.current.bigPadding anchors.rightMargin: Style.current.bigPadding - visible: recipientLoader.ready && !!assetSelector.selectedAsset && networkSelector.advancedOrCustomMode && amountToSendInput.inputNumberValid + visible: recipientLoader.ready && !!d.selectedHolding && networkSelector.advancedOrCustomMode && amountToSendInput.inputNumberValid selectedTokenSymbol: d.selectedSymbol isLoading: popup.isLoading bestRoutes: popup.bestRoutes @@ -439,7 +463,7 @@ StatusDialog { d.totalTimeEstimate = popup.store.getLabelForEstimatedTxTime(gasTimeEstimate.totalTime) d.totalFeesInFiat = popup.currencyStore.getFiatValue( gasTimeEstimate.totalFeesInEth, "ETH", popup.currencyStore.currentCurrency) + popup.currencyStore.getFiatValue(gasTimeEstimate.totalTokenFees, fees.selectedTokenSymbol, popup.currencyStore.currentCurrency) - d.totalAmountToReceive = popup.store.getWei2Eth(txRoutes.amountToReceive, assetSelector.selectedAsset.decimals) + d.totalAmountToReceive = popup.store.getWei2Eth(txRoutes.amountToReceive, d.selectedHolding.decimals) networkSelector.toNetworksList = txRoutes.toNetworksModel popup.isLoading = false } diff --git a/ui/imports/shared/status/StatusStickerMarket.qml b/ui/imports/shared/status/StatusStickerMarket.qml index 1feed894cb..f1c005de8d 100644 --- a/ui/imports/shared/status/StatusStickerMarket.qml +++ b/ui/imports/shared/status/StatusStickerMarket.qml @@ -202,7 +202,8 @@ Item { sendType: Constants.SendType.StickersBuy preSelectedRecipient: root.store.stickersStore.getStickersMarketAddress() preDefinedAmountToSend: LocaleUtils.numberToLocaleString(parseFloat(price)) - preSelectedAsset: store.getAsset(buyStickersModal.store.assets, JSON.parse(root.store.stickersStore.getStatusToken()).symbol) + preSelectedHolding: store.getAsset(buyStickersModal.store.assets, JSON.parse(root.store.stickersStore.getStatusToken()).symbol) + preSelectedHoldingType: Constants.HoldingType.Asset sendTransaction: function() { if(bestRoutes.count === 1) { let path = bestRoutes.firstItem() diff --git a/ui/imports/shared/status/StatusStickerPackClickPopup.qml b/ui/imports/shared/status/StatusStickerPackClickPopup.qml index f052b7b22f..f2d323382a 100644 --- a/ui/imports/shared/status/StatusStickerPackClickPopup.qml +++ b/ui/imports/shared/status/StatusStickerPackClickPopup.qml @@ -71,7 +71,8 @@ ModalPopup { sendType: Constants.SendType.StickersBuy preSelectedRecipient: stickerPackDetailsPopup.store.stickersStore.getStickersMarketAddress() preDefinedAmountToSend: LocaleUtils.numberToLocaleString(parseFloat(price)) - preSelectedAsset: store.getAsset(buyStickersPackModal.store.assets, JSON.parse(stickerPackDetailsPopup.store.stickersStore.getStatusToken()).symbol) + preSelectedHolding: store.getAsset(buyStickersPackModal.store.assets, JSON.parse(stickerPackDetailsPopup.store.stickersStore.getStatusToken()).symbol) + preSelectedHoldingType: Constants.HoldingType.Asset sendTransaction: function() { if(bestRoutes.count === 1) { let path = bestRoutes.firstItem() diff --git a/ui/imports/shared/stores/TransactionStore.qml b/ui/imports/shared/stores/TransactionStore.qml index 177bfee133..7eba0c629e 100644 --- a/ui/imports/shared/stores/TransactionStore.qml +++ b/ui/imports/shared/stores/TransactionStore.qml @@ -6,6 +6,8 @@ import shared.stores 1.0 import utils 1.0 +import StatusQ.Core.Utils 0.1 + QtObject { id: root @@ -19,6 +21,8 @@ QtObject { property var senderAccounts: walletSectionSendInst.senderAccounts property var selectedSenderAccount: walletSectionSendInst.selectedSenderAccount property var accounts: walletSectionSendInst.accounts + property var collectiblesModel: walletSectionSendInst.collectiblesModel + property var nestedCollectiblesModel: walletSectionSendInst.nestedCollectiblesModel property bool areTestNetworksEnabled: networksModule.areTestNetworksEnabled property var tmpActivityController: walletSection.tmpActivityController property var savedAddressesModel: SortFilterProxyModel { @@ -105,6 +109,68 @@ QtObject { return {} } + function getCollectible(uid) { + const idx = ModelUtils.indexOf(collectiblesModel, "uid", uid) + if (idx < 0) { + return {} + } + return ModelUtils.get(collectiblesModel, idx) + } + + function getSelectorCollectible(uid) { + const idx = ModelUtils.indexOf(nestedCollectiblesModel, "uid", uid) + if (idx < 0) { + return {} + } + return ModelUtils.get(nestedCollectiblesModel, idx) + } + + function getHolding(holdingId, holdingType) { + if (holdingType === Constants.HoldingType.Asset) { + return getAsset(selectedSenderAccount.assets, holdingId) + } else if (holdingType === Constants.HoldingType.Collectible) { + return getCollectible(holdingId) + } else { + return {} + } + } + + function getSelectorHolding(holdingId, holdingType) { + if (holdingType === Constants.HoldingType.Asset) { + return getAsset(selectedSenderAccount.assets, holdingId) + } else if (holdingType === Constants.HoldingType.Collectible) { + return getSelectorCollectible(holdingId) + } else { + return {} + } + } + + function assetToSelectorAsset(asset) { + return asset + } + + function collectibleToSelectorCollectible(collectible) { + return { + uid: collectible.uid, + chainId: collectible.chainId, + name: collectible.name, + iconUrl: collectible.imageUrl, + collectionUid: collectible.collectionUid, + collectionName: collectible.collectionName, + isCollection: false + } + } + + function holdingToSelectorHolding(holding, holdingType) { + if (holdingType === Constants.HoldingType.Asset) { + return assetToSelectorAsset(holding) + } else if (holdingType === Constants.HoldingType.Collectible) { + return collectibleToSelectorCollectible(holding) + } else { + return {} + } + } + function switchSenderAccount(index) { walletSectionSendInst.switchSenderAccount(index) } diff --git a/ui/imports/shared/views/TokenListView.qml b/ui/imports/shared/views/TokenListView.qml index 98f98f2cc6..e73f98b109 100644 --- a/ui/imports/shared/views/TokenListView.qml +++ b/ui/imports/shared/views/TokenListView.qml @@ -16,8 +16,8 @@ Item { id: root property var assets: null - signal tokenSelected(var selectedToken) - signal tokenHovered(var selectedToken, bool hovered) + signal tokenSelected(string symbol) + signal tokenHovered(string symbol, bool hovered) property var searchTokenSymbolByAddressFn: function (address) { return "" } @@ -84,8 +84,8 @@ Item { delegate: TokenBalancePerChainDelegate { width: ListView.view.width getNetworkIcon: root.getNetworkIcon - onTokenSelected: root.tokenSelected(selectedToken) - onTokenHovered: root.tokenHovered(selectedToken, hovered) + onTokenSelected: root.tokenSelected(symbol) + onTokenHovered: root.tokenHovered(symbol, hovered) } } } diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 62c2ef49d4..8335472dff 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -1211,4 +1211,8 @@ QtObject { Link = 0, Image = 1 } + + enum HoldingType { + Unknown, Asset, Collectible + } }