diff --git a/src/app/global/utils.nim b/src/app/global/utils.nim index 4224f5df45..319c77e4b7 100644 --- a/src/app/global/utils.nim +++ b/src/app/global/utils.nim @@ -1,4 +1,4 @@ -import NimQml, strutils, uri, strformat, strutils, stint +import NimQml, strutils, uri, strformat, strutils, stint, re import stew/byteutils import ./utils/qrcodegen @@ -69,7 +69,7 @@ QtObject: proc hex2Dec*(self: Utils, value: string): string {.slot.} = # somehow this value crashes the app - if value == "0x0": + if value.find(re("0x0+$")) >= 0: return "0" return $stint.fromHex(StUint[256], value) diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index d5807bc907..f272ec14ba 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -281,22 +281,10 @@ ListModel { title: "TokenItem" section: "Components" } - ListElement { - title: "TransactionDelegate" - section: "Components" - } ListElement { title: "CommunityPermissionsRow" section: "Components" } - ListElement { - title: "TransactionAddress" - section: "Components" - } - ListElement { - title: "TransactionAddressTile" - section: "Components" - } ListElement { title: "StatusImageCropPanel" section: "Components" @@ -321,6 +309,22 @@ ListModel { title: "PopupSizing" section: "Research / Examples" } + ListElement { + title: "TransactionDelegate" + section: "Wallet" + } + ListElement { + title: "TransactionAddress" + section: "Wallet" + } + ListElement { + title: "TransactionAddressTile" + section: "Wallet" + } + ListElement { + title: "TransactionDetailView" + section: "Wallet" + } ListElement { title: "WalletHeader" section: "Wallet" diff --git a/storybook/pages/TransactionDetailViewPage.qml b/storybook/pages/TransactionDetailViewPage.qml new file mode 100644 index 0000000000..5873a48ceb --- /dev/null +++ b/storybook/pages/TransactionDetailViewPage.qml @@ -0,0 +1,194 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Storybook 1.0 + +import AppLayouts.Wallet 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStores + +import "../../ui/app/AppLayouts/Wallet/views" // NOTE - there is no AppLayout.Wallet.views +import shared.controls 1.0 +import shared.stores 1.0 + +import utils 1.0 + +SplitView { + id: root + + property bool globalUtilsReady: false + property bool mainModuleReady: false + property bool rootStoreReady: false + + Component.onCompleted: { + RootStore.getFiatValue = (cryptoValue, symbol, currentCurrency) => { return 123 } + RootStore.getNetworkIcon = (chainId) => { return "tiny/network/Network=Ethereum" } + RootStore.getLatestBlockNumber = () => { return 4 } + RootStore.hex2Dec = (number) => { return 10 } + RootStore.getNetworkColor = (number) => { return "blue" } + RootStore.getNetworkFullName = (chainId) => { return "Ethereum Mainnet" } + RootStore.getNetworkShortName = (chainId) => { return "eth" } + RootStore.formatCurrencyAmount = (value, symbol) => { return value + " " + symbol } + RootStore.getNameForSavedWalletAddress = (address) => { return "Saved Wallet Name" } + RootStore.getNameForAddress = (address) => { return "Address Name" } + RootStore.getEnsForSavedWalletAddress = (address) => { return "123" } + RootStore.getChainShortNamesForSavedWalletAddress = (address) => { return "" } + RootStore.currentCurrency = "USD" + + root.rootStoreReady = true + } + + // globalUtilsInst mock + QtObject { + function getCompressedPk(publicKey) { return "zx3sh" + publicKey } + function getColorHashAsJson(publicKey) { + return JSON.stringify([{"segmentLength":1,"colorId":12},{"segmentLength":5,"colorId":18}, + {"segmentLength":3,"colorId":25},{"segmentLength":3,"colorId":23}, + {"segmentLength":1,"colorId":10},{"segmentLength":3,"colorId":26}, + {"segmentLength":2,"colorId":30},{"segmentLength":1,"colorId":18}, + {"segmentLength":4,"colorId":28},{"segmentLength":1,"colorId":17}, + {"segmentLength":2,"colorId":2}]) + } + function isCompressedPubKey(publicKey) { return true } + function getColorId(publicKey) { return Math.floor(Math.random() * 10) } + + Component.onCompleted: { + Utils.globalUtilsInst = this + root.globalUtilsReady = true + } + Component.onDestruction: { + root.globalUtilsReady = false + Utils.globalUtilsInst = {} + } + } + + // mainModuleInst mock + QtObject { + function getContactDetailsAsJson(publicKey, getVerification) { + return JSON.stringify({ + displayName: "ArianaP", + displayIcon: "", + publicKey: publicKey, + name: "", + alias: "", + localNickname: "", + isContact: true + }) + } + function isEnsVerified(publicKey) { return false } + + Component.onCompleted: { + Utils.mainModuleInst = this + root.mainModuleReady = true + } + Component.onDestruction: { + root.mainModuleReady = false + Utils.mainModuleInst = {} + } + } + + QtObject { + id: contactsStoreMockup + readonly property var myContactsModel: QtObject { + signal itemChanged(address: string) + } + + function getContactPublicKeyByAddress(address) { + return "" + } + } + + QtObject { + id: transactionData + + property int chainId: 1 + property string blockNumber: "0x124" + property int timestamp: Date.now() / 1000 + property int txStatus: 0 + property string type: "eth" + property string nonce: "0x123" + property string from: "0x29D7d1dd5B6f9C864d9db560D72a247c178aE86B" + property string to: "0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35" + property string contract: "0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35" + property bool isNFT: false + property string input: "0xdasdja214i12r0uf0jh013rfj01rfj12-09fuj12f012fuj0-129fuj012ujf1209u120912er902iue30912e" + property string tokenID: "4981676894159712808201908443964193325271219637660871887967796332739046670337" + property string nftName: "Happy Meow" + property string nftImageUrl: Style.png("collectibles/HappyMeow") + property string symbol: "ETH" + property string txHash: "0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35" + + readonly property var value: QtObject { + property real amount: amountSpinbox.realValue + property string symbol: "eth" + } + } + + SplitView { + orientation: Qt.Vertical + SplitView.fillWidth: true + Item { + SplitView.fillWidth: true + SplitView.fillHeight: true + + Rectangle { + anchors.fill: viewLoader + anchors.margins: -1 + color: "transparent" + border.width: 1 + border.color: "#808080" + } + + Loader { + id: viewLoader + anchors.centerIn: parent + width: 800 + height: 500 + + active: root.globalUtilsReady && root.mainModuleReady && root.rootStoreReady + sourceComponent: TransactionDetailView { + contactsStore: contactsStoreMockup + transaction: transactionData + } + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 150 + + SplitView.fillWidth: true + } + } + + Pane { + SplitView.minimumWidth: 300 + SplitView.preferredWidth: 300 + + ColumnLayout { + Label { + text: "Amount:" + } + SpinBox { + id: amountSpinbox + from: 0 + to: 999999999 + value: 12345 + stepSize: 1 + editable: true + + readonly property int multiplier: Math.pow(10, decimals) + property int decimals: 5 + property real realValue: value / multiplier + validator: DoubleValidator { bottom: 0.0 } + textFromValue: function(value, locale) { return Number(value / amountSpinbox.multiplier).toLocaleString(locale, 'f', amountSpinbox.decimals) } + valueFromText: function(text, locale) { return Number.fromLocaleString(locale, text) * amountSpinbox.multiplier } + } + CheckBox { + text: "is NFT" + checked: transactionData.isNFT + onCheckedChanged: transactionData.isNFT = checked + } + } + } +} diff --git a/storybook/stubs/shared/stores/RootStore.qml b/storybook/stubs/shared/stores/RootStore.qml index 98a3f4946c..4fe0e4656f 100644 --- a/storybook/stubs/shared/stores/RootStore.qml +++ b/storybook/stubs/shared/stores/RootStore.qml @@ -9,7 +9,20 @@ QtObject { property bool isWalletEnabled property var getSelectedTextWithFormationChars property var gifColumnA + property var currentCurrency property var currencyStore + property var getNetworkIcon + property var getFiatValue + property var getLatestBlockNumber + property var hex2Dec + property var getNetworkColor + property var getNetworkFullName + property var getNetworkShortName + property var formatCurrencyAmount + property var getNameForSavedWalletAddress + property var getNameForAddress + property var getEnsForSavedWalletAddress + property var getChainShortNamesForSavedWalletAddress } diff --git a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml index c3f87f6cba..6eacf9235d 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml @@ -137,7 +137,6 @@ Rectangle { MouseArea { id: sensor - z: 1 // Gives ability to hide siblings under the MouseArea anchors.fill: parent cursorShape: containsMouse ? Qt.PointingHandCursor : Qt.ArrowCursor acceptedButtons: Qt.NoButton @@ -289,7 +288,7 @@ Rectangle { objectName: "statusListItemSubTitle" Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: inlineTagModelRepeater.count > 0 ? contentWidth : parent.width + Layout.preferredWidth: inlineTagModelRepeater.count > 0 ? contentWidth : parent.width - subTitleBadgeLoader.width text: root.subTitle font.pixelSize: 15 diff --git a/ui/app/AppLayouts/Wallet/panels/WalletTxProgressBlock.qml b/ui/app/AppLayouts/Wallet/panels/WalletTxProgressBlock.qml index 98867d7be0..7d8ac69519 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletTxProgressBlock.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletTxProgressBlock.qml @@ -45,11 +45,6 @@ ColumnLayout { readonly property int progress: (Math.floor(Date.now() / 1000) - root.timeStamp) / 3600 } - Separator { - Layout.fillWidth: true - implicitHeight: 1 - } - StatusTxProgressBar { id: progressBar Layout.topMargin: 8 @@ -133,10 +128,4 @@ ColumnLayout { } } } - - Separator { - Layout.fillWidth: true - Layout.topMargin: 8 - implicitHeight: 1 - } } diff --git a/ui/app/AppLayouts/Wallet/popups/TransactionAddressMenu.qml b/ui/app/AppLayouts/Wallet/popups/TransactionAddressMenu.qml index c7201e4f06..ae32caabe9 100644 --- a/ui/app/AppLayouts/Wallet/popups/TransactionAddressMenu.qml +++ b/ui/app/AppLayouts/Wallet/popups/TransactionAddressMenu.qml @@ -68,7 +68,7 @@ StatusMenu { } function openMenu(delegate) { - const x = delegate.width - root.contentWidth / 2 + const x = delegate.width - 40 const y = delegate.height / 2 + 20 root.popup(delegate, x, y) } @@ -102,6 +102,7 @@ StatusMenu { function openEthAddressMenu(delegate, address) { d.selectedAddress = address + address = address.toLowerCase() const contactPubKey = "" // TODO retrive contact public key or contact data directly from address let contactData = Utils.getContactDetailsAsJson(contactPubKey) let isWalletAccount = false @@ -135,6 +136,7 @@ StatusMenu { function openTxMenu(delegate, address, chainShortName="") { d.addressType = TransactionAddressMenu.AddressType.Tx d.selectedAddress = address + chainShortName = chainShortName.toLowerCase() if (chainShortName === root.arbiscanShortChainName) { showOnArbiscanAction.enabled = true } else if (chainShortName === root.optimismShortChainName) { @@ -149,6 +151,7 @@ StatusMenu { d.addressType = TransactionAddressMenu.AddressType.Contract d.contractName = name d.selectedAddress = address + chainShortName = chainShortName.toLowerCase() if (chainShortName === root.arbiscanShortChainName) { showOnArbiscanAction.enabled = true } else if (chainShortName === root.optimismShortChainName) { @@ -232,8 +235,6 @@ StatusMenu { successText: { switch(d.addressType) { case TransactionAddressMenu.AddressType.Contract: - if (d.contractName.length > 0) - return qsTr("%1 contract address copied").arg(d.contractName) return qsTr("Contract address copied") case TransactionAddressMenu.AddressType.InputData: return qsTr("Input data copied") @@ -250,8 +251,6 @@ StatusMenu { defaultText: { switch(d.addressType) { case TransactionAddressMenu.AddressType.Contract: - if (d.contractName.length > 0) - return qsTr("Copy %1 contract address").arg(d.contractName) return qsTr("Copy contract address") case TransactionAddressMenu.AddressType.InputData: return qsTr("Copy input data") diff --git a/ui/app/AppLayouts/Wallet/views/TransactionDetailView.qml b/ui/app/AppLayouts/Wallet/views/TransactionDetailView.qml index 50ba797245..740f38ca3a 100644 --- a/ui/app/AppLayouts/Wallet/views/TransactionDetailView.qml +++ b/ui/app/AppLayouts/Wallet/views/TransactionDetailView.qml @@ -2,6 +2,7 @@ import QtQuick 2.13 import QtQuick.Layouts 1.13 import QtQuick.Controls 2.14 import QtQuick.Window 2.12 +import QtGraphicalEffects 1.15 import StatusQ.Components 0.1 import StatusQ.Core.Theme 0.1 @@ -10,6 +11,7 @@ import StatusQ.Controls 0.1 import StatusQ.Popups 0.1 import shared.controls 1.0 +import shared.panels 1.0 import utils 1.0 import shared.stores 1.0 @@ -38,96 +40,360 @@ Item { readonly property string to: root.isTransactionValid ? !!savedAddressNameTo ? savedAddressNameTo : Utils.compactAddress(transaction.to, 4): "" readonly property string savedAddressEns: root.isTransactionValid ? RootStore.getEnsForSavedWalletAddress(isIncoming ? transaction.from : transaction.to) : "" readonly property string savedAddressChains: root.isTransactionValid ? RootStore.getChainShortNamesForSavedWalletAddress(isIncoming ? transaction.from : transaction.to) : "" + readonly property string networkShortName: root.isTransactionValid ? RootStore.getNetworkShortName(transaction.chainId) : "" + readonly property string networkFullName: root.isTransactionValid ? RootStore.getNetworkFullName(transaction.chainId): "" + readonly property string networkIcon: root.isTransactionValid ? RootStore.getNetworkIcon(transaction.chainId): "" + readonly property int blockNumber: root.isTransactionValid ? RootStore.hex2Dec(root.transaction.blockNumber) : 0 + readonly property string bridgeNetworkIcon: "" // TODO fill when bridge data is implemented + readonly property string bridgeNetworkFullname: "" // TODO fill when bridge data is implemented + readonly property string bridgeNetworkShortName: "" // TODO fill when bridge data is implemented + readonly property int bridgeBlockNumber: 0 // TODO fill when bridge data is implemented + readonly property string swapSymbol: "" // TODO fill when swap data is implemented + readonly property string symbol: root.isTransactionValid ? transaction.symbol : "" + readonly property var multichainNetworks: [] // TODO fill icon for networks for multichain function getNameForSavedWalletAddress(address) { return RootStore.getNameForSavedWalletAddress(address) } + + function retryTransaction() { + // TODO handle failed transaction retry + } } - StatusScrollView { + id: scrollView anchors.top: parent.top anchors.left: parent.left width: parent.width height: parent.height contentHeight: column.height - contentWidth: parent.width Column { id: column - width: parent.width - Style.current.xlPadding + width: scrollView.availableWidth - Style.current.xlPadding - spacing: Style.current.bigPadding + spacing: Style.current.xlPadding + Style.current.halfPadding - TransactionDelegate { - id: transactionHeader - objectName: "transactionDetailHeader" + Column { width: parent.width - leftPadding: 0 + spacing: Style.current.bigPadding - modelData: transaction - transactionType: d.isIncoming ? TransactionDelegate.Receive : TransactionDelegate.Send - currentCurrency: RootStore.currentCurrency - cryptoValue: root.isTransactionValid ? transaction.value.amount: 0.0 - fiatValue: root.isTransactionValid ? RootStore.getFiatValue(cryptoValue, symbol, currentCurrency): 0.0 - networkIcon: root.isTransactionValid ? RootStore.getNetworkIcon(transaction.chainId): "" - networkColor: root.isTransactionValid ? RootStore.getNetworkColor(transaction.chainId): "" - networkName: root.isTransactionValid ? RootStore.getNetworkFullName(transaction.chainId): "" - symbol: root.isTransactionValid ? transaction.symbol : "" - transferStatus: root.isTransactionValid ? RootStore.hex2Dec(transaction.txStatus): "" - timeStampText: root.isTransactionValid ? qsTr("Signed at %1").arg(LocaleUtils.formatDateTime(transaction.timestamp * 1000, Locale.LongFormat)): "" - addressNameTo: root.isTransactionValid ? WalletStores.RootStore.getNameForAddress(transaction.to): "" - addressNameFrom: root.isTransactionValid ? WalletStores.RootStore.getNameForAddress(transaction.from): "" - sensor.enabled: false - formatCurrencyAmount: RootStore.formatCurrencyAmount - color: Theme.palette.transparent - state: "header" + TransactionDelegate { + id: transactionHeader + objectName: "transactionDetailHeader" + width: parent.width + leftPadding: 0 - onRetryClicked: { - // TODO handle failed transaction retry + modelData: transaction + transactionType: d.isIncoming ? TransactionDelegate.Receive : TransactionDelegate.Send + currentCurrency: RootStore.currentCurrency + cryptoValue: root.isTransactionValid ? transaction.value.amount: 0.0 + fiatValue: root.isTransactionValid ? RootStore.getFiatValue(cryptoValue, symbol, currentCurrency): 0.0 + networkIcon: d.networkIcon + networkColor: root.isTransactionValid ? RootStore.getNetworkColor(transaction.chainId): "" + networkName: d.networkFullName + swapSymbol: d.swapSymbol + bridgeNetworkName: d.bridgeNetworkFullname + symbol: d.symbol + transferStatus: root.isTransactionValid ? RootStore.hex2Dec(transaction.txStatus): "" + timeStampText: root.isTransactionValid ? qsTr("Signed at %1").arg(LocaleUtils.formatDateTime(transaction.timestamp * 1000, Locale.LongFormat)): "" + addressNameTo: root.isTransactionValid ? WalletStores.RootStore.getNameForAddress(transaction.to): "" + addressNameFrom: root.isTransactionValid ? WalletStores.RootStore.getNameForAddress(transaction.from): "" + sensor.enabled: false + formatCurrencyAmount: RootStore.formatCurrencyAmount + color: Theme.palette.transparent + state: "header" + onRetryClicked: d.retryTransaction() } + + Separator { } } WalletTxProgressBlock { + id: progressBlock width: Math.min(513, root.width) error: transactionHeader.transactionStatus === TransactionDelegate.TransactionStatus.Failed isLayer1: RootStore.getNetworkLayer(root.transaction.chainId) == 1 - confirmations: root.isTransactionValid ? Math.abs(WalletStores.RootStore.getLatestBlockNumber(root.transaction.chainId) - RootStore.hex2Dec(root.transaction.blockNumber)): 0 - chainName: root.isTransactionValid ? RootStore.getNetworkFullName(root.transaction.chainId): "" + confirmations: root.isTransactionValid ? Math.abs(WalletStores.RootStore.getLatestBlockNumber(root.transaction.chainId) - d.blockNumber): 0 + chainName: d.networkFullName timeStamp: root.isTransactionValid ? transaction.timestamp: "" } - SavedAddressesDelegate { - width: parent.width - - name: d.isIncoming ? d.savedAddressNameFrom : d.savedAddressNameTo - address: root.isTransactionValid ? d.isIncoming ? transaction.from : transaction.to : "" - ens: d.savedAddressEns - chainShortNames: d.savedAddressChains - title: d.isIncoming ? d.from : d.to - subTitle: root.isTransactionValid ? d.isIncoming ? !!d.savedAddressNameFrom ? Utils.compactAddress(transaction.from, 4) : "" : !!d.savedAddressNameTo ? Utils.compactAddress(transaction.to, 4) : "": "" - store: WalletStores.RootStore - contactsStore: root.contactsStore - onOpenSendModal: root.sendModal.open(address); - saveAddress: function(name, address, favourite, chainShortNames, ens) { - RootStore.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens) - } - deleteSavedAddress: function(address, ens) { - RootStore.deleteSavedAddress(address, ens) - } + Separator { + width: progressBlock.width } - StatusExpandableItem { - width: parent.width - anchors.horizontalCenter: parent.horizontalCenter + Column { + width: progressBlock.width + spacing: 0 - type: StatusExpandableItem.Type.Tertiary - expandable: true - primaryText: qsTr("Transaction summary") - expandableComponent: transactionSummary - separatorVisible: false - expanded: true + StatusBaseText { + width: parent.width + font.pixelSize: 15 + color: Theme.palette.directColor5 + text: qsTr("Transaction summary") + elide: Text.ElideRight + } + + Item { + width: parent.width + height: Style.current.smallPadding + } + + DetailsPanel { + RowLayout { + spacing: 0 + width: parent.width + height: opacity > 0 ? Math.max(implicitHeight, 85) : 0 + opacity: fromNetworkTile.visible || toNetworkTile.visible ? 1 : 0 + TransactionDataTile { + id: fromNetworkTile + Layout.fillWidth: true + Layout.fillHeight: true + title: qsTr("From") + subTitle: { + switch(transactionHeader.transactionType) { + case TransactionDelegate.Swap: + return d.symbol + case TransactionDelegate.Bridge: + return d.networkFullName + default: + return "" + } + } + asset.name: { + switch(transactionHeader.transactionType) { + case TransactionDelegate.Swap: + return !!d.symbol ? Style.png("tokens/%1".arg(d.symbol)) : "" + case TransactionDelegate.Bridge: + return !!d.networkIcon ? Style.svg(d.networkIcon) : "" + default: + return "" + } + } + visible: !!subTitle + } + TransactionDataTile { + id: toNetworkTile + Layout.fillWidth: true + Layout.fillHeight: true + title: qsTr("To") + subTitle: { + switch(transactionHeader.transactionType) { + case TransactionDelegate.Swap: + return d.swapSymbol + case TransactionDelegate.Bridge: + return d.bridgeNetworkFullname + default: + return "" + } + } + asset.name: { + switch(transactionHeader.transactionType) { + case TransactionDelegate.Swap: + return !!d.swapSymbol ? Style.png("tokens/%1".arg(d.swapSymbol)) : "" + case TransactionDelegate.Bridge: + return !!d.bridgeNetworkIcon ? Style.svg(d.bridgeNetworkIcon) : "" + default: + return "" + } + } + visible: !!subTitle + } + } + TransactionAddressTile { + width: parent.width + title: transactionHeader.transactionType === TransactionDelegate.Swap || transactionHeader.transactionType === TransactionDelegate.Bridge ? + qsTr("In") : qsTr("From") + addresses: root.isTransactionValid ? [root.transaction.from] : [] + contactsStore: root.contactsStore + rootStore: WalletStores.RootStore + onButtonClicked: { + if (transactionHeader.transactionType === TransactionDelegate.Swap || transactionHeader.transactionType === TransactionDelegate.Bridge) { + addressMenu.openEthAddressMenu(this, addresses[0]) + } else { + addressMenu.openSenderMenu(this, addresses[0]) + } + } + } + TransactionAddressTile { + width: parent.width + title: qsTr("To") + addresses: root.isTransactionValid ? [root.transaction.to] : [] + contactsStore: root.contactsStore + rootStore: WalletStores.RootStore + onButtonClicked: addressMenu.openReceiverMenu(this, addresses[0]) + visible: transactionHeader.transactionType !== TransactionDelegate.Swap && transactionHeader.transactionType !== TransactionDelegate.Bridge && transactionHeader.transactionType !== TransactionDelegate.Destroy + } + TransactionDataTile { + width: parent.width + title: qsTr("Using") + buttonIconName: "external" + subTitle: "" // TODO fill protocol name for Swap and Bridge + asset.name: "" // TODO fill protocol icon for Bridge and Swap e.g. Style.svg("network/Network=Arbitrum") + onButtonClicked: { + // TODO handle + } + visible: !!subTitle + } + TransactionDataTile { + width: parent.width + title: qsTr("%1 Tx hash").arg(d.networkFullName) + subTitle: root.isTransactionValid ? root.transaction.txHash : "" + visible: !!subTitle + buttonIconName: "more" + onButtonClicked: addressMenu.openTxMenu(this, subTitle, d.networkShortName) + } + TransactionDataTile { + width: parent.width + title: qsTr("%1 Tx hash").arg(d.bridgeNetworkFullname) + subTitle: "" // TODO fill tx hash for Bridge + visible: !!subTitle + buttonIconName: "more" + onButtonClicked: addressMenu.openTxMenu(this, subTitle, d.bridgeNetworkShortName) + } + TransactionContractTile { + // Used for Bridge and Swap to display 'From' network Protocol contract address + address: "" // TODO fill protocol contract address for 'from' network for Bridge and Swap + symbol: "" // TODO fill protocol name for Bridge and Swap + networkName: d.networkFullName + shortNetworkName: d.networkShortName + visible: !!subTitle && (transactionHeader.transactionType === TransactionDelegate.Bridge || transactionHeader.transactionType === TransactionDelegate.Swap) + } + TransactionContractTile { + // Used to display contract address for any network + address: root.isTransactionValid ? transaction.contract : "" + symbol: root.isTransactionValid ? transaction.value.symbol.toUpperCase() : "" + networkName: d.networkFullName + shortNetworkName: d.networkShortName + } + TransactionContractTile { + // Used for Bridge to display 'To' network Protocol contract address + address: "" // TODO fill protocol contract address for 'to' network for Bridge + symbol: "" // TODO fill protocol name for Bridge + networkName: d.bridgeNetworkFullname + shortNetworkName: d.bridgeNetworkShortName + visible: !!subTitle && transactionHeader.transactionType === TransactionDelegate.Bridge + } + TransactionContractTile { + // Used for Bridge and Swap to display 'To' network token contract address + address: { + if (!root.isTransactionValid) + return "" + switch(transactionHeader.transactionType) { + case TransactionDelegate.Swap: + return transaction.contract + case TransactionDelegate.Bridge: + return "" // TODO fill swap token's contract address for 'to' network for Bridge + default: + return "" + } + } + symbol: { + if (!root.isTransactionValid) + return "" + switch(transactionHeader.transactionType) { + case TransactionDelegate.Swap: + return d.swapSymbol + case TransactionDelegate.Bridge: + return transaction.value.symbol.toUpperCase() + default: + return "" + } + } + networkName: d.bridgeNetworkFullname + shortNetworkName: d.bridgeNetworkShortName + } + } + + Item { + width: parent.width + height: Style.current.bigPadding + } + + DetailsPanel { + width: progressBlock.width + RowLayout { + width: parent.width + height: Math.max(implicitHeight, 85) + spacing: 0 + TransactionDataTile { + id: multichainNetworksTile + Layout.fillHeight: true + Layout.fillWidth: true + title: qsTr("Networks") + visible: d.multichainNetworks.length > 0 + Row { + anchors { + top: parent.top + topMargin: multichainNetworksTile.statusListItemTitleArea.height + multichainNetworksTile.topPadding + left: parent.left + leftMargin: multichainNetworksTile.leftPadding + } + spacing: -4 + Repeater { + model: d.multichainNetworks + delegate: StatusRoundedImage { + width: 20 + height: 20 + visible: image.source !== "" + border.width: index === 0 ? 0 : 1 + border.color: Theme.palette.white + image.source: Style.svg("tiny/" + modelData) + z: index + 1 + } + } + } + } + TransactionDataTile { + Layout.fillHeight: true + Layout.fillWidth: true + title: qsTr("Network") + subTitle: d.networkFullName + asset.name: !!d.networkIcon ? Style.svg("%1".arg(d.networkIcon)) : "" + smallIcon: true + visible: transactionHeader.transactionType !== TransactionDelegate.Bridge + } + TransactionDataTile { + Layout.fillHeight: true + Layout.fillWidth: true + title: qsTr("Token format") + subTitle: root.isTransactionValid ? transaction.type.toUpperCase() : "" + visible: !!subTitle + } + TransactionDataTile { + Layout.fillHeight: true + Layout.fillWidth: true + title: qsTr("Nonce") + subTitle: root.isTransactionValid ? RootStore.hex2Dec(root.transaction.nonce) : "" + visible: !!subTitle + } + } + TransactionDataTile { + width: parent.width + title: qsTr("Input data") + subTitle: root.isTransactionValid ? root.transaction.input : "" + visible: !!subTitle + buttonIconName: "more" + onButtonClicked: addressMenu.openInputDataMenu(this, subTitle) + } + TransactionDataTile { + width: parent.width + title: !!d.networkFullName ? qsTr("Included in Block on %1").arg(d.networkFullName) : qsTr("Included on Block") + subTitle: d.blockNumber + tertiaryTitle: root.isTransactionValid ? LocaleUtils.formatDateTime(transaction.timestamp * 1000, Locale.LongFormat) : "" + visible: d.blockNumber > 0 + } + TransactionDataTile { + width: parent.width + title: !!d.bridgeNetworkFullname ? qsTr("Included in Block on %1").arg(d.bridgeNetworkFullname) : qsTr("Included on Block") + subTitle: d.bridgeBlockNumber + tertiaryTitle: root.isTransactionValid ? LocaleUtils.formatDateTime(transaction.timestamp * 1000, Locale.LongFormat) : "" + visible: d.bridgeBlockNumber > 0 + } + } } StatusExpandableItem { @@ -140,76 +406,6 @@ Item { expandableComponent: fees expanded: true } - - InformationTile { - maxWidth: parent.width - primaryText: qsTr("Data") - secondaryText: root.isTransactionValid ? root.transaction.input : "" - copy: true - onCopyClicked: RootStore.copyToClipboard(textToCopy) - } - } - } - - - Component { - id: transactionSummary - Column { - id: column - width: parent.width - spacing: 8 - TransactionDelegate { - width: parent.width - modelData: transaction - transactionType: d.isIncoming ? TransactionDelegate.Receive : TransactionDelegate.Send - currentCurrency: RootStore.currentCurrency - cryptoValue: root.isTransactionValid ? transaction.value.amount: 0.0 - fiatValue: root.isTransactionValid ? RootStore.getFiatValue(cryptoValue, symbol, currentCurrency): 0.0 - networkIcon: root.isTransactionValid ? RootStore.getNetworkIcon(transaction.chainId) : "" - networkColor: root.isTransactionValid ? RootStore.getNetworkColor(transaction.chainId): "" - networkName: root.isTransactionValid ? RootStore.getNetworkShortName(transaction.chainId): "" - symbol: root.isTransactionValid ? transaction.symbol : "" - transferStatus: root.isTransactionValid ? RootStore.hex2Dec(transaction.txStatus): "" - timeStampText: root.isTransactionValid ? LocaleUtils.formatTime(transaction.timestamp * 1000, Locale.ShortFormat): "" - addressNameTo: root.isTransactionValid ? RootStore.getNameForSavedWalletAddress(transaction.to): "" - addressNameFrom: root.isTransactionValid ? RootStore.getNameForSavedWalletAddress(transaction.from): "" - formatCurrencyAmount: RootStore.formatCurrencyAmount - sensor.enabled: false - color: Theme.palette.statusListItem.backgroundColor - border.width: 1 - border.color: Theme.palette.directColor8 - } - Row { - spacing: 8 - InformationTile { - maxWidth: parent.width - primaryText: qsTr("Time") - secondaryText: root.transaction !== undefined && !!root.transaction ? qsTr("%1 on %2"). - arg(LocaleUtils.formatTime(transaction.timestamp * 1000, Locale.ShortFormat)). - arg(LocaleUtils.formatDate(transaction.timestamp * 1000, Locale.ShortFormat)): "" - } - InformationTile { - maxWidth: parent.width - primaryText: qsTr("Confirmations") - secondaryText: { - if(root.isTransactionValid) - return Math.abs(WalletStores.RootStore.getLatestBlockNumber(root.transaction.chainId) - RootStore.hex2Dec(root.transaction.blockNumber)) - else - return "" - } - } - InformationTile { - maxWidth: parent.width - primaryText: qsTr("Nonce") - secondaryText: root.isTransactionValid ? RootStore.hex2Dec(root.transaction.nonce) : "" - } - InformationTile { - maxWidth: parent.width - primaryText: qsTr("TokenID") - secondaryText: root.isTransactionValid ? root.transaction.tokenID : "" - visible: root.isTransactionValid && d.isNFT - } - } } } @@ -258,4 +454,41 @@ Item { contactsStore: root.contactsStore onOpenSendModal: (address) => root.sendModal.open(address) } + + component DetailsPanel: Item { + width: parent.width + height: detailsColumn.childrenRect.height + default property alias content: detailsColumn.children + Rectangle { + id: tileBackground + anchors.fill: parent + radius: 8 + border.width: 1 + border.color: Style.current.separator + } + + Column { + id: detailsColumn + anchors.fill: parent + anchors.margins: 1 + spacing: 0 + layer.enabled: true + layer.effect: OpacityMask { + maskSource: tileBackground + } + } + } + + component TransactionContractTile: TransactionDataTile { + property string networkName: "" + property string symbol: "" + property string address: "" + property string shortNetworkName: "" + width: parent.width + title: qsTr("%1 %2 contract address").arg(networkName).arg(symbol) + subTitle: !!address && !/0x0+$/.test(address) ? address : "" + buttonIconName: "more" + visible: !!subTitle + onButtonClicked: addressMenu.openContractMenu(this, address, shortNetworkName, symbol) + } } diff --git a/ui/imports/shared/controls/TransactionAddress.qml b/ui/imports/shared/controls/TransactionAddress.qml index f74a85e24f..640fb4fcad 100644 --- a/ui/imports/shared/controls/TransactionAddress.qml +++ b/ui/imports/shared/controls/TransactionAddress.qml @@ -178,6 +178,7 @@ Item { font.pixelSize: 15 color: Theme.palette.directColor1 wrapMode: Text.WrapAnywhere + enabled: false // Set to false to disable hover for rich text text: { if(!!root.address == false) return "" diff --git a/ui/imports/shared/controls/TransactionAddressTile.qml b/ui/imports/shared/controls/TransactionAddressTile.qml index 6b1839f14b..512a9def10 100644 --- a/ui/imports/shared/controls/TransactionAddressTile.qml +++ b/ui/imports/shared/controls/TransactionAddressTile.qml @@ -9,7 +9,7 @@ import StatusQ.Core.Theme 0.1 /*! \qmltype TransactionAddressTile - \inherits StatusListItem + \inherits TransactionDataTile \inqmlmodule shared.controls \since shared.controls 1.0 \brief It displays list of addresses for wallet activity. @@ -21,7 +21,6 @@ import StatusQ.Core.Theme 0.1 title: qsTr("From") width: parent.width rootStore: WalletStores.RootStore - roundedCornersBottom: false addresses: [ "eth:arb:opt:0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35", "0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35", @@ -30,7 +29,7 @@ import StatusQ.Core.Theme 0.1 \endqml */ -StatusListItem { +TransactionDataTile { id: root /*! @@ -44,48 +43,11 @@ StatusListItem { */ property var rootStore - /*! - \qmlproperty int TransactionAddressTile::topPadding - This property holds spacing between top and content item in tile. - */ - property int topPadding: 12 - /*! - \qmlproperty int TransactionAddressTile::bottomPadding - This property holds spacing between bottom and content item in tile. - */ - property int bottomPadding: 12 - /* /internal Property hold reference to contacts store to refresh contact data on any change. */ property var contactsStore - signal showContextMenu() - - leftPadding: 12 - rightPadding: 12 - radius: 0 - - implicitHeight: transactionColumn.height + statusListItemTitleArea.height + root.topPadding + root.bottomPadding - statusListItemTitle.customColor: Theme.palette.directColor5 - statusListItemTitleArea.anchors { - top: statusListItemTitleArea.parent.top - topMargin: root.topPadding - right: statusListItemTitleArea.parent.right - verticalCenter: undefined - } - - components: [ - StatusRoundButton { - id: button - width: 32 - height: 32 - icon.color: hovered ? Theme.palette.directColor1 : Theme.palette.baseColor1 - icon.name: "more" - type: StatusRoundButton.Type.Quinary - radius: 8 - visible: root.sensor.containsMouse - onClicked: root.showContextMenu() - } - ] + implicitHeight: transactionColumn.height + transactionColumn.spacing + root.topPadding + root.bottomPadding + buttonIconName: "more" Column { id: transactionColumn @@ -93,14 +55,12 @@ StatusListItem { left: parent.left leftMargin: root.leftPadding right: parent.right - rightMargin: button.width + root.rightPadding * 2 + rightMargin: root.statusListItemComponentsSlot.width + root.rightPadding * 2 bottom: parent.bottom bottomMargin: root.bottomPadding } height: childrenRect.height spacing: 4 - // Moving it under sensor, because Rich Text steals hovering - z: root.sensor.z - 1 Repeater { model: root.addresses diff --git a/ui/imports/shared/controls/TransactionDataTile.qml b/ui/imports/shared/controls/TransactionDataTile.qml new file mode 100644 index 0000000000..cad825b3e9 --- /dev/null +++ b/ui/imports/shared/controls/TransactionDataTile.qml @@ -0,0 +1,122 @@ +import QtQuick 2.13 + +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 + +import shared.panels 1.0 + +/*! + \qmltype TransactionDataTile + \inherits StatusListItem + \inqmlmodule shared.controls + \since shared.controls 1.0 + \brief It displays data for wallet activity. + + The \c TransactionDataTile can display wallet activity data as a tile. + To show button fill \l{buttonIcon} property. + + \qml + TransactionDataTile { + width: parent.width + title: qsTr("From") + buttonIcon: "more" + } + \endqml +*/ + +StatusListItem { + id: root + + /*! + \qmlproperty int TransactionDataTile::topPadding + This property holds spacing between top and content item in tile. + */ + property int topPadding: 12 + /*! + \qmlproperty int TransactionDataTile::bottomPadding + This property holds spacing between bottom and content item in tile. + */ + property int bottomPadding: 12 + + /*! + \qmlproperty bool TransactionDataTile::smallIcon + This property holds information about icon state. Setting it to true will display small icon before subtitle. + + Default value is false. + */ + property bool smallIcon: false + /*! + \qmlproperty string TransactionDataTile::buttonIconName + This property holds button icon source string. + To show button icon source must be filled + */ + property string buttonIconName + + signal buttonClicked() + + leftPadding: 12 + rightPadding: 12 + height: implicitHeight + bottomPadding + radius: 0 + sensor.cursorShape: Qt.ArrowCursor + + // Title + statusListItemTitle.customColor: Theme.palette.directColor5 + statusListItemTitle.enabled: false + statusListItemTitleArea.anchors { + left: statusListItemTitleArea.parent.left + top: statusListItemTitleArea.parent.top + topMargin: topPadding + right: statusListItemTitleArea.parent.right + verticalCenter: undefined + } + + // Subtitle + statusListItemTagsRowLayout.anchors.topMargin: 8 + statusListItemTagsRowLayout.width: statusListItemTagsRowLayout.parent.width - (!!root.buttonIconName ? 36 : 0) + statusListItemSubTitle.customColor: Theme.palette.directColor1 + + // Tertiary title + statusListItemTertiaryTitle.anchors.topMargin: -statusListItemTertiaryTitle.height + statusListItemTertiaryTitle.horizontalAlignment: Qt.AlignRight + + // Icon + asset.isImage: false + statusListItemTagsRowLayout.spacing: 8 + subTitleBadgeComponent: !!asset.name ? iconComponent : null + statusListItemIcon.asset: StatusAssetSettings {} + + Component { + id: iconComponent + StatusRoundIcon { + asset: StatusAssetSettings { + name: root.asset.name + color: "transparent" + width: root.smallIcon ? 20 : 36 + height: root.smallIcon ? 20 : 36 + bgWidth: width + bgHeight: height + } + } + } + + components: Loader { + active: !!root.buttonIconName + sourceComponent: StatusRoundButton { + width: 32 + height: 32 + icon.color: hovered ? Theme.palette.directColor1 : Theme.palette.baseColor1 + icon.name: root.buttonIconName + type: StatusRoundButton.Type.Quinary + radius: 8 + visible: root.sensor.containsMouse + onClicked: root.buttonClicked() + } + } + + Separator { + anchors.bottom: parent.bottom + } +} diff --git a/ui/imports/shared/controls/TransactionDelegate.qml b/ui/imports/shared/controls/TransactionDelegate.qml index f78a8650af..13e9093db5 100644 --- a/ui/imports/shared/controls/TransactionDelegate.qml +++ b/ui/imports/shared/controls/TransactionDelegate.qml @@ -55,7 +55,7 @@ StatusListItem { property string symbol property string swapSymbol // TODO fill when swap data is implemented property int transactionType - property int transactionStatus: transferStatus === 0 ? TransactionDelegate.TransactionStatus.Failed : TransactionDelegate.TransactionStatus.Finished + property int transactionStatus: transferStatus === 0 ? TransactionDelegate.TransactionStatus.Pending : TransactionDelegate.TransactionStatus.Finished property string currentCurrency property int transferStatus property double cryptoValue diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index c633967691..00d1e71914 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -26,6 +26,7 @@ Timer 1.0 Timer.qml TransactionDelegate 1.0 TransactionDelegate.qml TransactionAddress 1.0 TransactionAddress.qml TransactionAddressTile 1.0 TransactionAddressTile.qml +TransactionDataTile 1.0 TransactionDataTile.qml TransactionFormGroup 1.0 TransactionFormGroup.qml EmojiHash 1.0 EmojiHash.qml InformationTile 1.0 InformationTile.qml