diff --git a/storybook/pages/SimpleSendModalPage.qml b/storybook/pages/SimpleSendModalPage.qml index 04e0ae4981..f2bddf9e8c 100644 --- a/storybook/pages/SimpleSendModalPage.qml +++ b/storybook/pages/SimpleSendModalPage.qml @@ -1,18 +1,35 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +import SortFilterProxyModel 0.2 + +import Models 1.0 import Storybook 1.0 import AppLayouts.Wallet.popups.simpleSend 1.0 +import AppLayouts.Wallet.stores 1.0 +import AppLayouts.Wallet.adaptors 1.0 + +import utils 1.0 SplitView { id: root orientation: Qt.Horizontal - function launchPopup() { - simpleSend.createObject(root) + QtObject { + id: d + + readonly property SortFilterProxyModel filteredNetworksModel: SortFilterProxyModel { + sourceModel: NetworksModel.flatNetworks + filters: ValueFilter { roleName: "isTest"; value: testNetworksCheckbox.checked } + } + + readonly property WalletAssetsStore walletAssetStore: WalletAssetsStore { + assetsWithFilteredBalances: groupedAccountsAssetsModel + } + + readonly property var walletAccountsModel: WalletAccountsModel{} } PopupBackground { @@ -27,25 +44,369 @@ SplitView { text: "Reopen" enabled: !simpleSend.visible - onClicked: launchPopup() + onClicked: simpleSend.open() } - Component.onCompleted: launchPopup() + Component.onCompleted: simpleSend.open() } - Component { + SimpleSendModal { id: simpleSend - SimpleSendModal { - visible: true - modal: false - closePolicy: Popup.NoAutoClose + + visible: true + modal: false + closePolicy: Popup.CloseOnEscape + + accountsModel: d.walletAccountsModel + assetsModel: assetsSelectorViewAdaptor.outputAssetsModel + collectiblesModel: collectiblesSelectionAdaptor.model + networksModel: d.filteredNetworksModel + Component.onCompleted: simpleSend.open() + } + + TokenSelectorViewAdaptor { + id: assetsSelectorViewAdaptor + + assetsModel: d.walletAssetStore.groupedAccountAssetsModel + + flatNetworksModel: NetworksModel.flatNetworks + + currentCurrency: "USD" + accountAddress: simpleSend.selectedAccountAddress + showCommunityAssets: true + enabledChainIds: [simpleSend.selectedChainId] + } + + CollectiblesSelectionAdaptor { + id: collectiblesSelectionAdaptor + + accountKey: simpleSend.selectedAccountAddress + + networksModel: d.filteredNetworksModel + collectiblesModel: collectiblesBySymbolModel + } + + ListModel { + id: collectiblesBySymbolModel + + readonly property var data: [ + // collection 2 + { + tokenId: "id_3", + symbol: "abc", + chainId: NetworksModel.mainnetChainId, + name: "Multi-sequencer Test NFT 1", + contractAddress: "contract_2", + collectionName: "Multi-sequencer Test NFT", + collectionUid: "collection_2", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 1, + txTimestamp: 1714059810 + } + ], + imageUrl: Constants.tokenIcon("ETH", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + { + tokenId: "id_4", + symbol: "def", + chainId: NetworksModel.mainnetChainId, + name: "Multi-sequencer Test NFT 2", + contractAddress: "contract_2", + collectionName: "Multi-sequencer Test NFT", + collectionUid: "collection_2", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 1, + txTimestamp: 1714059811 + } + ], + imageUrl: Constants.tokenIcon("ETH", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + { + tokenId: "id_5", + symbol: "ghi", + chainId: NetworksModel.mainnetChainId, + name: "Multi-sequencer Test NFT 3", + contractAddress: "contract_2", + collectionName: "Multi-sequencer Test NFT", + collectionUid: "collection_2", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 1, + txTimestamp: 1714059899 + } + ], + imageUrl: Constants.tokenIcon("ETH", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + // collection 1 + { + tokenId: "id_1", + symbol: "jkl", + chainId: NetworksModel.mainnetChainId, + name: "Genesis", + contractAddress: "contract_1", + collectionName: "ERC-1155 Faucet", + collectionUid: "collection_1", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 23, + txTimestamp: 1714059862 + }, + { + accountAddress: d.walletAccountsModel.accountAddress2, + balance: 29, + txTimestamp: 1714054862 + } + ], + imageUrl: Constants.tokenIcon("DAI", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + { + tokenId: "id_2", + symbol: "mno", + chainId: NetworksModel.mainnetChainId, + name: "QAERC1155", + contractAddress: "contract_1", + collectionName: "ERC-1155 Faucet", + collectionUid: "collection_1", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 500, + txTimestamp: 1714059864 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + // collection 3, community token + { + tokenId: "id_6", + symbol: "pqr", + chainId: NetworksModel.optChainId, + name: "My Token", + contractAddress: "contract_3", + collectionName: "My Token", + collectionUid: "collection_3", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 1, + txTimestamp: 1714059899 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_1", + communityName: "My community", + communityImage: Constants.tokenIcon("KIN", false) + }, + { + tokenId: "id_7", + symbol: "stu", + chainId: NetworksModel.optChainId, + name: "My Token", + contractAddress: "contract_3", + collectionName: "My Token", + collectionUid: "collection_3", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 1, + txTimestamp: 1714059899 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_1", + communityName: "My community", + communityImage: Constants.tokenIcon("KIN", false) + }, + { + tokenId: "id_8", + symbol: "vwx", + chainId: NetworksModel.optChainId, + name: "My Token", + contractAddress: "contract_3", + collectionName: "My Token", + collectionUid: "collection_3", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress2, + balance: 1, + txTimestamp: 1714059999 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_1", + communityName: "My community", + communityImage: Constants.tokenIcon("KIN", false) + }, + { + tokenId: "id_9", + symbol: "yz1", + chainId: NetworksModel.optChainId, + name: "My Other Token", + contractAddress: "contract_4", + collectionName: "My Other Token", + collectionUid: "collection_4", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 1, + txTimestamp: 1714059991 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_1", + communityName: "My community", + communityImage: Constants.tokenIcon("KIN", false) + }, + { + tokenId: "id_10", + symbol: "234", + chainId: NetworksModel.arbChainId, + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 1, + txTimestamp: 1714059777 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + }, + { + tokenId: "id_11", + symbol: "567", + chainId: NetworksModel.arbChainId, + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress1, + balance: 1, + txTimestamp: 1714059778 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + }, + { + tokenId: "id_11", + symbol: "8910", + chainId: NetworksModel.arbChainId, + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress2, + balance: 1, + txTimestamp: 1714059779 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + }, + { + tokenId: "id_12", + symbol: "111213", + chainId: NetworksModel.arbChainId, + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress3, + balance: 1, + txTimestamp: 1714059779 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + }, + { + tokenId: "id_13", + symbol: "141516", + chainId: NetworksModel.arbChainId, + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: d.walletAccountsModel.accountAddress3, + balance: 1, + txTimestamp: 1714059788 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + } + ] + + Component.onCompleted: { + append(data) } } LogsAndControlsPanel { SplitView.minimumHeight: 100 - SplitView.preferredHeight: 100 + SplitView.minimumWidth: 300 + CheckBox { + id: testNetworksCheckbox + text: "are test networks enabled" + } } } diff --git a/storybook/pages/StatusDialogPage.qml b/storybook/pages/StatusDialogPage.qml index fafe9dc929..245cd41312 100644 --- a/storybook/pages/StatusDialogPage.qml +++ b/storybook/pages/StatusDialogPage.qml @@ -75,6 +75,7 @@ SplitView { header: StatusDialogHeader { //color: Theme.palette.baseColor3 color: !!ctrlHeaderBgColor.text ? ctrlHeaderBgColor.text : Theme.palette.statusModal.backgroundColor + dropShadowEnabled: ctrlHeaderDropShadow.checked visible: dialog.title || dialog.subtitle headline.title: dialog.title @@ -233,6 +234,10 @@ SplitView { id: ctrlDropShadow text: "Footer drop shadow" } + CheckBox { + id: ctrlHeaderDropShadow + text: "Header drop shadow" + } } } } diff --git a/storybook/src/Models/NetworksModel.qml b/storybook/src/Models/NetworksModel.qml index e2214532c2..e3cb64a5ff 100644 --- a/storybook/src/Models/NetworksModel.qml +++ b/storybook/src/Models/NetworksModel.qml @@ -12,6 +12,14 @@ QtObject { readonly property int testnetNet: 5 readonly property int customNet: 6 + readonly property int mainnetChainId: 1 + readonly property int sepMainnetChainId: 11155111 + readonly property int optChainId: 10 + readonly property int sepOptChainId: 11155420 + readonly property int arbChainId: 42161 + readonly property int sepArbChainId: 421614 + + function getShortChainName(chainId) { if(chainId === root.ethNet) return "eth" @@ -55,7 +63,7 @@ QtObject { readonly property var flatNetworks: ListModel { Component.onCompleted: append([ { - chainId: 1, + chainId: mainnetChainId, chainName: "Mainnet", blockExplorerURL: "https://etherscan.io/", iconUrl: "network/Network=Ethereum", @@ -69,7 +77,7 @@ QtObject { isRouteEnabled: true, }, { - chainId: 11155111, + chainId: sepMainnetChainId, chainName: "Sepolia Mainnet", blockExplorerURL: "https://sepolia.etherscan.io/", iconUrl: "network/Network=Ethereum", @@ -83,7 +91,7 @@ QtObject { isRouteEnabled: true, }, { - chainId: 10, + chainId: optChainId, chainName: "Optimism", blockExplorerURL: "https://optimistic.etherscan.io", iconUrl: "network/Network=Optimism", @@ -97,7 +105,7 @@ QtObject { isRouteEnabled: true, }, { - chainId: 11155420, + chainId: sepOptChainId, chainName: "Optimism Sepolia", blockExplorerURL: "https://sepolia-optimism.etherscan.io/", iconUrl: "network/Network=Optimism", @@ -111,7 +119,7 @@ QtObject { isRouteEnabled: true, }, { - chainId: 42161, + chainId: arbChainId, chainName: "Arbitrum", blockExplorerURL: "https://arbiscan.io/", iconUrl: "network/Network=Arbitrum", @@ -125,7 +133,7 @@ QtObject { isRouteEnabled: true, }, { - chainId: 421614, + chainId: sepArbChainId, chainName: "Arbitrum Sepolia", blockExplorerURL: "https://sepolia-explorer.arbitrum.io/", iconUrl: "network/Network=Arbitrum", diff --git a/storybook/src/Models/WalletAccountsModel.qml b/storybook/src/Models/WalletAccountsModel.qml index 3cb142c77c..418e96e592 100644 --- a/storybook/src/Models/WalletAccountsModel.qml +++ b/storybook/src/Models/WalletAccountsModel.qml @@ -3,13 +3,20 @@ import QtQuick 2.15 import utils 1.0 ListModel { + + readonly property string accountAddress1: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" + readonly property string accountAddress2: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881" + readonly property string accountAddress3: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8882" + readonly property string accountAddress4: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8883" + readonly property string accountAddress5: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8884" + readonly property var data: [ { name: "helloworld", emoji: "😋", colorId: Constants.walletAccountColors.primary, color: "#2A4AF5", - address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", + address: accountAddress1, walletType: "", canSend: true, position: 0, @@ -53,7 +60,7 @@ ListModel { emoji: "🚗", colorId: Constants.walletAccountColors.army, color: "#216266", - address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", + address: accountAddress2, walletType: Constants.generatedWalletType, canSend: true, position: 3, @@ -79,7 +86,7 @@ ListModel { emoji: "🎨", colorId: Constants.walletAccountColors.magenta, color: "#EC266C", - address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8882", + address: accountAddress3, walletType: Constants.seedWalletType, canSend: true, position: 1, @@ -114,7 +121,7 @@ ListModel { emoji: "⌚", colorId: Constants.walletAccountColors.copper, color: "#CB6256", - address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8883", + address: accountAddress4, walletType: Constants.watchWalletType, canSend: false, position: 2, @@ -131,7 +138,7 @@ ListModel { emoji: "🔑", colorId: Constants.walletAccountColors.camel, color: "#C78F67", - address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8884", + address: accountAddress5, walletType: Constants.keyWalletType, canSend: true, position: 4, diff --git a/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialog.qml b/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialog.qml index dfd9660230..bd8a75ca3a 100644 --- a/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialog.qml +++ b/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialog.qml @@ -1,5 +1,6 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 import QtQuick.Layouts 1.15 import QtQml.Models 2.15 import QtQml 2.15 @@ -32,7 +33,8 @@ Dialog { anchors.centerIn: Overlay.overlay padding: 16 - margins: 64 + // by design + margins: root.contentItem.Window.window.height <= 780 ? 28: 64 modal: true // workaround for https://bugreports.qt.io/browse/QTBUG-87804 diff --git a/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialogHeader.qml b/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialogHeader.qml index 1d2c216f7d..ce11c13ece 100644 --- a/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialogHeader.qml +++ b/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialogHeader.qml @@ -1,5 +1,6 @@ import QtQuick 2.14 import QtQuick.Layouts 1.14 +import QtGraphicalEffects 1.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 @@ -9,6 +10,7 @@ Rectangle { readonly property alias headline: headline readonly property alias actions: actions + property bool dropShadowEnabled property alias leftComponent: leftComponentLoader.sourceComponent @@ -63,4 +65,12 @@ Rectangle { anchors.bottom: parent.bottom width: parent.width } + + layer.enabled: root.dropShadowEnabled + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 2 + samples: 37 + color: Theme.palette.dropShadow + } } diff --git a/ui/app/AppLayouts/Wallet/controls/TokenSelector.qml b/ui/app/AppLayouts/Wallet/controls/TokenSelector.qml index ff6dd52d9a..04bf8eca45 100644 --- a/ui/app/AppLayouts/Wallet/controls/TokenSelector.qml +++ b/ui/app/AppLayouts/Wallet/controls/TokenSelector.qml @@ -17,6 +17,9 @@ Control { /** Expected model structure: see SearchableCollectiblesPanel::model **/ property alias collectiblesModel: tokenSelectorPanel.collectiblesModel + /** Exposes insatnce of popup **/ + property var popup: dropdown + readonly property bool isTokenSelected: tokenSelectorButton.selected signal assetSelected(string key) @@ -54,7 +57,6 @@ Control { y: parent.height + 4 width: 448 - closePolicy: Popup.CloseOnPressOutsideParent horizontalPadding: 0 bottomPadding: 0 diff --git a/ui/app/AppLayouts/Wallet/panels/SendModalHeader.qml b/ui/app/AppLayouts/Wallet/panels/SendModalHeader.qml new file mode 100644 index 0000000000..8e92653f2d --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/SendModalHeader.qml @@ -0,0 +1,140 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Wallet.controls 1.0 + +RowLayout { + id: root + + /** + Expected model structure: + - tokensKey: unique string ID of the token (asset); e.g. "ETH" or contract address + - name: user visible token name (e.g. "Ethereum") + - symbol: user visible token symbol (e.g. "ETH") + - decimals: number of decimal places + - communityId:optional; ID of the community this token belongs to, if any + - marketDetails: object containing props like `currencyPrice` for the computed values below + - balances: submodel[ chainId:int, account:string, balance:BigIntString, iconUrl:string ] + - currentBalance: amount of tokens + - currencyBalance: e.g. `1000.42` in user's fiat currency + - currencyBalanceAsString: e.g. "1 000,42 CZK" formatted as a string according to the user's locale + - balanceAsString: `1.42` formatted as e.g. "1,42" in user's locale + - iconSource: string + **/ + required property var assetsModel + /** + Expected model structure: + - groupName: group name (from collection or community name) + - icon: from imageUrl or mediaUrl + - type: can be "community" or "other" + - subitems: submodel of collectibles/collections of the group + - key: key of collection (community type) or collectible (other type) + - name: name of the subitem (of collectible or collection) + - balance: balance of collection (in case of community collectibles) + or collectible (in case of ERC-1155) + - icon: icon of the subitem + **/ + required property var collectiblesModel + /** + Expected model structure: + - chainId: network chain id + - chainName: name of network + - iconUrl: network icon url + **/ + required property var networksModel + + /** input property holds header is being scrolled **/ + property bool isScrolling + + /** input property holds if the header is the sticky header **/ + property bool isStickyHeader + + /** property that exposes the selected network **/ + readonly property int selectedNetworkChainId: selectedNetworkEntry.item.chainId + + /** signal to propagate that an asset was selected **/ + signal assetSelected(string key) + /** signal to propagate that a collection was selected **/ + signal collectionSelected(string key) + /** signal to propagate that a collectible was selected **/ + signal collectibleSelected(string key) + + implicitHeight: sendModalTitleText.height + + spacing: 8 + + // if not closed during scrolling they move with the header and it feels undesirable + onIsScrollingChanged: { + tokenSelector.popup.close() + networkFilter.control.popup.close() + } + + StatusBaseText { + id: sendModalTitleText + + Layout.preferredWidth: contentWidth + + lineHeightMode: Text.FixedHeight + lineHeight: root.isStickyHeader ? 30 : 38 + font.pixelSize: root.isStickyHeader ? 22 : 28 + elide: Text.ElideRight + + text: qsTr("Send") + } + + TokenSelector { + id: tokenSelector + + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + + onCollectibleSelected: root.collectibleSelected(key) + onCollectionSelected: root.collectionSelected(key) + onAssetSelected: root.assetSelected(key) + } + + // Horizontal spacer + RowLayout {} + + StatusBaseText { + Layout.alignment: Qt.AlignRight + + text: qsTr("On:") + color: Theme.palette.baseColor1 + font.pixelSize: 13 + lineHeight: 38 + lineHeightMode: Text.FixedHeight + verticalAlignment: Text.AlignVCenter + + visible: networkFilter.visible + } + + NetworkFilter { + id: networkFilter + + Layout.alignment: Qt.AlignTop + + control.bottomPadding: 0 + + flatNetworks: root.networksModel + + multiSelection: false + showSelectionIndicator: false + showTitle: false + + } + + ModelEntry { + id: selectedNetworkEntry + sourceModel: root.networksModel + key: "chainId" + value: networkFilter.selection[0] + } +} diff --git a/ui/app/AppLayouts/Wallet/panels/StickySendModalHeader.qml b/ui/app/AppLayouts/Wallet/panels/StickySendModalHeader.qml new file mode 100644 index 0000000000..5233769b5a --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/StickySendModalHeader.qml @@ -0,0 +1,128 @@ +import QtQuick 2.14 +import QtGraphicalEffects 1.15 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups.Dialog 0.1 + +Rectangle { + id: root + + /** + Expected model structure: + - tokensKey: unique string ID of the token (asset); e.g. "ETH" or contract address + - name: user visible token name (e.g. "Ethereum") + - symbol: user visible token symbol (e.g. "ETH") + - decimals: number of decimal places + - communityId:optional; ID of the community this token belongs to, if any + - marketDetails: object containing props like `currencyPrice` for the computed values below + - balances: submodel[ chainId:int, account:string, balance:BigIntString, iconUrl:string ] + - currentBalance: amount of tokens + - currencyBalance: e.g. `1000.42` in user's fiat currency + - currencyBalanceAsString: e.g. "1 000,42 CZK" formatted as a string according to the user's locale + - balanceAsString: `1.42` formatted as e.g. "1,42" in user's locale + - iconSource: string + **/ + required property var assetsModel + /** + Expected model structure: + - groupName: group name (from collection or community name) + - icon: from imageUrl or mediaUrl + - type: can be "community" or "other" + - subitems: submodel of collectibles/collections of the group + - key: key of collection (community type) or collectible (other type) + - name: name of the subitem (of collectible or collection) + - balance: balance of collection (in case of community collectibles) + or collectible (in case of ERC-1155) + - icon: icon of the subitem + **/ + required property var collectiblesModel + /** + Expected model structure: + - chainId: network chain id + - chainName: name of network + - iconUrl: network icon url + **/ + required property var networksModel + + /** this property decided if the sticky header is visible or not. + Not using visible property directly here as the animation on + implicitHeight doesnt work + **/ + property bool isScrolling + + /** property that exposes the selected network **/ + readonly property int selectedNetworkChainId: sendModalHeader.selectedNetworkChainId + + /** signal to propagate that an asset was selected **/ + signal assetSelected(string key) + /** signal to propagate that a collection was selected **/ + signal collectionSelected(string key) + /** signal to propagate that a collectible was selected **/ + signal collectibleSelected(string key) + + enabled: root.isScrolling + color: Theme.palette.baseColor3 + radius: 8 + + implicitHeight: root.isScrolling ? + sendModalHeader.implicitHeight + + sendModalHeader.anchors.topMargin + + sendModalHeader.anchors.bottomMargin: + 0 + implicitWidth: sendModalHeader.implicitWidth + + sendModalHeader.anchors.leftMargin + + sendModalHeader.anchors.rightMargin + + + // Drawer animation for stickey heade + Behavior on implicitHeight { + NumberAnimation { duration: 350 } + } + + // cover for the bottom rounded corners + Rectangle { + width: parent.width + height: parent.radius + anchors.bottom: parent.bottom + color: parent.color + } + + SendModalHeader { + id: sendModalHeader + + width: parent.width + + anchors { + fill: parent + leftMargin: Theme.xlPadding + rightMargin: Theme.xlPadding + topMargin: 16 + bottomMargin: 12 + } + + isStickyHeader: true + isScrolling: root.isScrolling + + networksModel: root.networksModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + + onCollectibleSelected: root.collectibleSelected(key) + onCollectionSelected: root.collectionSelected(key) + onAssetSelected: root.assetSelected(key) + } + + StatusDialogDivider { + anchors.bottom: parent.bottom + width: parent.width + } + + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 2 + samples: 37 + color: Theme.palette.dropShadow + } +} + diff --git a/ui/app/AppLayouts/Wallet/panels/qmldir b/ui/app/AppLayouts/Wallet/panels/qmldir index 8d837dfea8..01221de437 100644 --- a/ui/app/AppLayouts/Wallet/panels/qmldir +++ b/ui/app/AppLayouts/Wallet/panels/qmldir @@ -12,3 +12,5 @@ SignInfoBox 1.0 SignInfoBox.qml SwapInputPanel 1.0 SwapInputPanel.qml TokenSelectorPanel 1.0 TokenSelectorPanel.qml WalletHeader 1.0 WalletHeader.qml +SendModalHeader 1.0 SendModalHeader.qml +StickySendModalHeader 1.0 StickySendModalHeader.qml diff --git a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml index 40fe860ca2..e58262c3f6 100644 --- a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml @@ -1,17 +1,206 @@ import QtQuick 2.15 +import QtQuick.Layouts 1.14 +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Popups.Dialog 0.1 +import shared.popups.send.views 1.0 +import shared.controls 1.0 + +import AppLayouts.Wallet.panels 1.0 + StatusDialog { - id: popup + id: root - title: qsTr("Send") + /** + TODO: use the newly defined WalletAccountsSelectorAdaptor + in https://github.com/status-im/status-desktop/pull/16834 + This will also remove watch only accounts from the list + Expected model structure: + - name: name of account + - address: wallet address + - color: color of the account + - emoji: emoji selected for the account + - currencyBalance: total currency balance in CurrencyAmount + - accountBalance: balance of selected token + selected chain + **/ + required property var accountsModel + /** + Expected model structure: + - tokensKey: unique string ID of the token (asset); e.g. "ETH" or contract address + - name: user visible token name (e.g. "Ethereum") + - symbol: user visible token symbol (e.g. "ETH") + - decimals: number of decimal places + - communityId:optional; ID of the community this token belongs to, if any + - marketDetails: object containing props like `currencyPrice` for the computed values below + - balances: submodel[ chainId:int, account:string, balance:BigIntString, iconUrl:string ] + - currentBalance: amount of tokens + - currencyBalance: e.g. `1000.42` in user's fiat currency + - currencyBalanceAsString: e.g. "1 000,42 CZK" formatted as a string according to the user's locale + - balanceAsString: `1.42` formatted as e.g. "1,42" in user's locale + - iconSource: string + **/ + required property var assetsModel + /** + Expected model structure: + - groupName: group name (from collection or community name) + - icon: from imageUrl or mediaUrl + - type: can be "community" or "other" + - subitems: submodel of collectibles/collections of the group + - key: key of collection (community type) or collectible (other type) + - name: name of the subitem (of collectible or collection) + - balance: balance of collection (in case of community collectibles) + or collectible (in case of ERC-1155) + - icon: icon of the subitem + **/ + required property var collectiblesModel + /** + Expected model structure: + - chainId: network chain id + - chainName: name of network + - iconUrl: network icon url + **/ + required property var networksModel + /** TODO: rethink property definitions needed to pre set values + + expose values to outside world **/ + property int selectedChainId: sendModalHeader.selectedNetworkChainId + property string selectedAccountAddress: accountSelector.currentAccountAddress + + QtObject { + id: d + + readonly property bool isScrolling: + scrollView.flickable.contentY > sendModalHeader.height + } + + width: 556 padding: 0 + leftPadding: Theme.xlPadding + rightPadding: Theme.xlPadding + topMargin: margins + accountSelector.height + Theme.padding + background: StatusDialogBackground { - implicitHeight: 846 - implicitWidth: 556 color: Theme.palette.baseColor3 } + + Item { + id: sendModalcontentItem + + anchors.fill: parent + anchors.top: parent.top + + implicitWidth: parent.width + implicitHeight: scrollView.implicitHeight + + // Floating account Selector + AccountSelectorHeader { + id: accountSelector + + anchors.top: parent.top + anchors.topMargin: -accountSelector.height - Theme.padding + anchors.left: parent.left + anchors.leftMargin: -Theme.xlPadding + + model: root.accountsModel + } + + // Sticky header only visible when scrolling + StickySendModalHeader { + width: root.width + anchors.top: accountSelector.bottom + anchors.topMargin:Theme.padding + anchors.left: parent.left + anchors.leftMargin: -Theme.xlPadding + z: 1 + + networksModel: root.networksModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + isScrolling: d.isScrolling + + onCollectibleSelected: console.log("collectible selected:", key) + onCollectionSelected: console.log("collection selected:", key) + onAssetSelected: console.log("asset selected:", key) + } + + // Main scrollable Layout + StatusScrollView { + id: scrollView + + anchors.fill: parent + anchors.topMargin: 28 + contentWidth: availableWidth + + padding: 0 + + StatusScrollBar.vertical { + id: verticalScrollbar + + parent: sendModalcontentItem + x: sendModalcontentItem.width + root.rightPadding - verticalScrollbar.width + } + + ColumnLayout { + id: scrollViewLayout + + width: scrollView.availableWidth + spacing: 20 + + // Header that scrolls + SendModalHeader { + id: sendModalHeader + + Layout.fillWidth: true + + isScrolling: d.isScrolling + + networksModel: root.networksModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + + onCollectibleSelected: console.log("collectible selected:", key) + onCollectionSelected: console.log("collection selected:", key) + onAssetSelected: console.log("asset selected:", key) + } + + // TODO: Remove these Dummy items added only to test dialog resizing + readonly property string longLoremIpsum: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." + Text { + Layout.fillWidth: true + text: scrollViewLayout.longLoremIpsum.repeat(3) + wrapMode: Text.WordWrap + } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 200 + opacity: 0.2 + color: "red" + } + Text { + Layout.fillWidth: true + text: scrollViewLayout.longLoremIpsum.repeat(3) + wrapMode: Text.WordWrap + } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 200 + opacity: 0.2 + color: "red" + } + // End Dummy items + } + } + } + + // TODO:: move to new location and rework if needed + footer: TransactionModalFooter { + width: parent.width + pending: false + nextButtonText: qsTr("Review Send") + maxFiatFees: "..." + totalTimeEstimate: "..." + } }