From ec04b8668fffb44fc6a63063dfc29cce82907429 Mon Sep 17 00:00:00 2001 From: Cuteivist Date: Wed, 10 May 2023 13:54:06 +0200 Subject: [PATCH] Feat/10473 Updated activity list delegate and activity details header (#10580) closes #10473 closes #10542 fixes #10408 --- .../main/wallet_section/accounts/model.nim | 6 + .../main/wallet_section/accounts/view.nim | 5 +- storybook/PagesModel.qml | 4 + storybook/pages/TransactionDelegatePage.qml | 149 +++++ .../src/StatusQ/Components/StatusListItem.qml | 1 + .../StatusQ/Components/StatusRoundIcon.qml | 3 +- .../Components/StatusSmartIdenticon.qml | 2 + .../Controls/StatusTextWithLoadingState.qml | 11 +- ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml | 9 +- .../src/StatusQ/Core/StatusAssetSettings.qml | 1 + ui/StatusQ/src/StatusQ/Core/StatusIcon.qml | 2 +- ui/StatusQ/src/assets.qrc | 1 + ui/StatusQ/src/assets/img/icons/destroy.svg | 10 + ui/app/AppLayouts/Wallet/stores/RootStore.qml | 8 + .../Wallet/views/TransactionDetailView.qml | 31 +- .../assets/icons/transaction/failed.svg | 4 + .../assets/icons/transaction/finished.svg | 8 + .../assets/icons/transaction/pending.svg | 6 + .../assets/icons/transaction/verified.svg | 8 + .../shared/controls/TransactionDelegate.qml | 571 ++++++++++++++---- ui/imports/shared/stores/RootStore.qml | 4 + ui/imports/shared/views/HistoryView.qml | 14 +- 22 files changed, 703 insertions(+), 155 deletions(-) create mode 100644 storybook/pages/TransactionDelegatePage.qml create mode 100644 ui/StatusQ/src/assets/img/icons/destroy.svg create mode 100644 ui/imports/assets/icons/transaction/failed.svg create mode 100644 ui/imports/assets/icons/transaction/finished.svg create mode 100644 ui/imports/assets/icons/transaction/pending.svg create mode 100644 ui/imports/assets/icons/transaction/verified.svg diff --git a/src/app/modules/main/wallet_section/accounts/model.nim b/src/app/modules/main/wallet_section/accounts/model.nim index 79d97877d7..4be211a2eb 100644 --- a/src/app/modules/main/wallet_section/accounts/model.nim +++ b/src/app/modules/main/wallet_section/accounts/model.nim @@ -97,3 +97,9 @@ QtObject: result = newQVariant(item.keyUid()) of ModelRole.AssetsLoading: result = newQVariant(item.assetsLoading()) + + proc getNameByAddress*(self: Model, address: string): string = + for item in self.items: + if(item.address() == address): + return item.name() + return "" \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/accounts/view.nim b/src/app/modules/main/wallet_section/accounts/view.nim index ccab7249d5..51d6b58d39 100644 --- a/src/app/modules/main/wallet_section/accounts/view.nim +++ b/src/app/modules/main/wallet_section/accounts/view.nim @@ -44,4 +44,7 @@ QtObject: self.delegate.deleteAccount(address) proc updateAccount(self: View, address: string, accountName: string, color: string, emoji: string) {.slot.} = - self.delegate.updateAccount(address, accountName, color, emoji) \ No newline at end of file + self.delegate.updateAccount(address, accountName, color, emoji) + + proc getNameByAddress(self: View, address: string): string {.slot.}= + return self.accounts.getNameByAddress(address) \ No newline at end of file diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index 8b5c351d33..96bca0d1f5 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -265,6 +265,10 @@ ListModel { title: "TokenItem" section: "Components" } + ListElement { + title: "TransactionDelegate" + section: "Components" + } ListElement { title: "CommunityPermissionsRow" section: "Components" diff --git a/storybook/pages/TransactionDelegatePage.qml b/storybook/pages/TransactionDelegatePage.qml new file mode 100644 index 0000000000..f374a3d15b --- /dev/null +++ b/storybook/pages/TransactionDelegatePage.qml @@ -0,0 +1,149 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import Storybook 1.0 +import utils 1.0 + +import shared.controls 1.0 + +SplitView { + id: root + + readonly property QtObject mockupModelData: QtObject { + property int timestamp: Date.now() / 1000 + property int txStatus: 0 + property string from: "0xfB8131c260749c7835a08ccBdb64728De432858E" + property string to: "0x3fb81384583b3910BB14Cc72582E8e8a56E83ae9" + property bool isNFT: false + property string tokenID: "4981676894159712808201908443964193325271219637660871887967796332739046670337" + property string nftName: "Happy Meow" + property string nftImageUrl: Style.png("collectibles/HappyMeow") + } + + SplitView { + orientation: Qt.Vertical + SplitView.fillWidth: true + Item { + SplitView.fillWidth: true + SplitView.fillHeight: true + + Rectangle { + anchors.fill: column + anchors.margins: -1 + border.color: "lightgray" + } + + ColumnLayout { + id: column + + anchors.centerIn: parent + + width: 600 + + TransactionDelegate { + id: delegate + Layout.fillWidth: true + modelData: root.mockupModelData + swapCryptoValue: 0.18 + swapFiatValue: 340 + swapSymbol: "SNT" + timeStampText: LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000) + cryptoValue: 0.1234 + fiatValue: 123123 + currentCurrency: "USD" + networkName: "Optimism" + symbol: "ETH" + bridgeNetworkName: "Mainnet" + feeFiatValue: 10.34 + feeCryptoValue: 0.013 + transactionStatus: TransactionDelegate.Pending + transactionType: TransactionDelegate.Send + formatCurrencyAmount: function(amount, symbol, options = null, locale = null) { + const currencyAmount = { + amount: amount, + symbol: symbol, + displayDecimals: 8, + stripTrailingZeroes: true + } + return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options) + } + } + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 200 + + SplitView.fillWidth: true + } + } + + Pane { + SplitView.minimumWidth: 300 + SplitView.preferredWidth: 300 + + ColumnLayout { + CheckBox { + text: "Is loading" + checked: delegate.loading + onToggled: delegate.loading = checked + } + CheckBox { + text: "Is activity details header" + readonly property string headerState: "header" + checked: delegate.state === headerState + onToggled: delegate.state = checked ? headerState : "" + } + + CheckBox { + text: "Is NFT" + checked: delegate.isNFT + onToggled: root.mockupModelData.isNFT = checked + } + + Label { + Layout.topMargin: 10 + Layout.fillWidth: true + text: "Transaction type:" + } + + ComboBox { + Layout.fillWidth: true + textRole: "name" + valueRole: "type" + model: ListModel { + ListElement { name: "Sent"; type: TransactionDelegate.Send } + ListElement { name: "Receive"; type: TransactionDelegate.Receive } + ListElement { name: "Buy"; type: TransactionDelegate.Buy } + ListElement { name: "Sell"; type: TransactionDelegate.Sell } + ListElement { name: "Destroy"; type: TransactionDelegate.Destroy } + ListElement { name: "Swap"; type: TransactionDelegate.Swap } + ListElement { name: "Bridge"; type: TransactionDelegate.Bridge } + } + onActivated: delegate.transactionType = model.get(currentIndex).type + } + + Label { + Layout.topMargin: 10 + Layout.fillWidth: true + text: "Transaction status:" + } + + ComboBox { + Layout.fillWidth: true + textRole: "name" + valueRole: "type" + model: ListModel { + ListElement { name: "Pending"; status: TransactionDelegate.Pending } + ListElement { name: "Failed"; status: TransactionDelegate.Failed } + ListElement { name: "Verified"; status: TransactionDelegate.Verified } + ListElement { name: "Finished"; status: TransactionDelegate.Finished } + } + onActivated: delegate.transactionStatus = model.get(currentIndex).status + } + } + } +} diff --git a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml index ca8d0cecf3..3a4567d1bd 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml @@ -81,6 +81,7 @@ Rectangle { property alias statusListItemLabel: statusListItemLabel property alias subTitleBadgeComponent: subTitleBadgeLoader.sourceComponent property alias errorIcon: errorIcon + property alias statusListItemTagsRowLayout: statusListItemSubtitleTagsRow signal clicked(string itemId, var mouse) signal titleClicked(string titleId) diff --git a/ui/StatusQ/src/StatusQ/Components/StatusRoundIcon.qml b/ui/StatusQ/src/StatusQ/Components/StatusRoundIcon.qml index 3c494e3a08..443bb8f68d 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusRoundIcon.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusRoundIcon.qml @@ -20,7 +20,8 @@ Rectangle { implicitWidth: asset.bgWidth implicitHeight: asset.bgHeight radius: asset.bgRadius - + border.width: asset.bgBorderWidth + border.color: asset.bgBorderColor StatusIcon { id: statusIcon diff --git a/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml b/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml index 3199caaf08..d6f825a535 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml @@ -80,6 +80,8 @@ Loader { asset.name: root.asset.name asset.rotation: root.asset.rotation asset.color: root.asset.color + asset.bgBorderWidth: root.asset.bgBorderWidth + asset.bgBorderColor: root.asset.bgBorderColor signal clicked(var mouse) diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusTextWithLoadingState.qml b/ui/StatusQ/src/StatusQ/Controls/StatusTextWithLoadingState.qml index 6f53c37206..265942ffff 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusTextWithLoadingState.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusTextWithLoadingState.qml @@ -35,12 +35,19 @@ StatusBaseText { */ property bool loading: false /*! - \qmlproperty bool StatusTextWithLoadingState::customColor + \qmlproperty color StatusTextWithLoadingState::customColor This property sets the user defined color for the text and handles transparency in loading state. */ property color customColor: Theme.palette.directColor1 + /*! + \qmlproperty int StatusTextWithLoadingState::maximumLoadingStateWidth + This property sets maximum width of loading component. + The default value is 140. + */ + property int maximumLoadingStateWidth: 140 + color: loading ? "transparent" : customColor Loader { @@ -51,7 +58,7 @@ StatusBaseText { anchors.centerIn: parent radius: textMetrics.font.pixelSize === 15 ? 4 : 8 height: textMetrics.tightBoundingRect.height - width: Math.min(root.width, 140) + width: Math.min(root.width, root.maximumLoadingStateWidth) } } diff --git a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml index 0112cf7d9d..67771a7d8c 100644 --- a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml @@ -301,10 +301,13 @@ QtObject { // otherwise var fullFormatString = d.fixupTimeFormatString(loc.dateTimeFormat(Locale.ShortFormat)) - if (now.getFullYear() === value.getFullYear()) - fullFormatString = fullFormatString.replace(/y/g, "") // strip year part, if current year -> "31 December 09:41" - else + if (now.getFullYear() === value.getFullYear()) { + // strip year part, if current year -> "31 December 09:41" + // It remove preceding dot or space + fullFormatString = fullFormatString.replace(/([.\s])?\b(y+)\b/g, "") + } else if (!fullFormatString.includes("yyyy")) { fullFormatString = fullFormatString.replace("yy", "yyyy") // different year -> "31 December 2022 09:41" + } return value.toLocaleString(loc, fullFormatString) } diff --git a/ui/StatusQ/src/StatusQ/Core/StatusAssetSettings.qml b/ui/StatusQ/src/StatusQ/Core/StatusAssetSettings.qml index 16e6e6fb62..24e3a896b4 100644 --- a/ui/StatusQ/src/StatusQ/Core/StatusAssetSettings.qml +++ b/ui/StatusQ/src/StatusQ/Core/StatusAssetSettings.qml @@ -30,6 +30,7 @@ QtObject { property int bgRadius property color bgColor: "transparent" property color bgBorderColor: "transparent" + property int bgBorderWidth: 0 //image property bool isImage: false diff --git a/ui/StatusQ/src/StatusQ/Core/StatusIcon.qml b/ui/StatusQ/src/StatusQ/Core/StatusIcon.qml index c36c65a980..ef1430b15e 100644 --- a/ui/StatusQ/src/StatusQ/Core/StatusIcon.qml +++ b/ui/StatusQ/src/StatusQ/Core/StatusIcon.qml @@ -13,7 +13,7 @@ Image { fillMode: Image.PreserveAspectFit onIconChanged: { - if(icon.startsWith("data:image/") || icon.startsWith("https://")) { + if(icon.startsWith("data:image/") || icon.startsWith("https://") || icon.startsWith("qrc:/") || icon.startsWith("file:/")) { //raw image data source = icon objectName = "custom-icon" diff --git a/ui/StatusQ/src/assets.qrc b/ui/StatusQ/src/assets.qrc index 3a371aa6f6..edff1b7ded 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -294,6 +294,7 @@ assets/img/icons/time.svg assets/img/icons/token-sale.svg assets/img/icons/token.svg + assets/img/icons/destroy.svg assets/img/icons/touch-id.svg assets/img/icons/travel-and-places.svg assets/img/icons/tributeToTalk.svg diff --git a/ui/StatusQ/src/assets/img/icons/destroy.svg b/ui/StatusQ/src/assets/img/icons/destroy.svg new file mode 100644 index 0000000000..9bbd5e9917 --- /dev/null +++ b/ui/StatusQ/src/assets/img/icons/destroy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index bebe2e766d..ac7a3e87f5 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -169,6 +169,14 @@ QtObject { return walletSectionSavedAddresses.getNameByAddress(address) } + function getNameForAddress(address) { + let name = getNameForSavedWalletAddress(address) + if (name.length === 0) { + name = walletSectionAccounts.getNameByAddress(address) + } + return name + } + function createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens) { return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens) } diff --git a/ui/app/AppLayouts/Wallet/views/TransactionDetailView.qml b/ui/app/AppLayouts/Wallet/views/TransactionDetailView.qml index 9265b72804..aa89f24bb2 100644 --- a/ui/app/AppLayouts/Wallet/views/TransactionDetailView.qml +++ b/ui/app/AppLayouts/Wallet/views/TransactionDetailView.qml @@ -28,7 +28,7 @@ Item { QtObject { id: d - readonly property bool isIncoming: root.isTransactionValid ? root.transaction.to === root.overview.mixedcaseAddress : false + readonly property bool isIncoming: root.isTransactionValid ? root.transaction.to.toLowerCase() === root.overview.mixedcaseAddress.toLowerCase() : false readonly property bool isNFT: root.isTransactionValid ? root.transaction.isNFT : false readonly property string savedAddressNameTo: root.isTransactionValid ? d.getNameForSavedWalletAddress(transaction.to) : "" readonly property string savedAddressNameFrom: root.isTransactionValid ? d.getNameForSavedWalletAddress(transaction.from): "" @@ -59,26 +59,31 @@ Item { spacing: Style.current.bigPadding TransactionDelegate { + id: transactionHeader objectName: "transactionDetailHeader" width: parent.width modelData: transaction - isIncoming: d.isIncoming + 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): "" + networkName: root.isTransactionValid ? RootStore.getNetworkFullName(transaction.chainId): "" symbol: root.isTransactionValid ? transaction.symbol : "" transferStatus: root.isTransactionValid ? RootStore.hex2Dec(transaction.txStatus): "" - shortTimeStamp: root.isTransactionValid ? LocaleUtils.formatTime(transaction.timestamp * 1000, Locale.ShortFormat): "" - savedAddressNameTo: root.isTransactionValid ? RootStore.getNameForSavedWalletAddress(transaction.to): "" - savedAddressNameFrom: root.isTransactionValid ? RootStore.getNameForSavedWalletAddress(transaction.from): "" - isSummary: false + 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.statusListItem.backgroundColor - state: "big" + state: "header" + + onRetryClicked: { + // TODO handle failed transaction retry + } } SavedAddressesDelegate { @@ -144,7 +149,7 @@ Item { TransactionDelegate { width: parent.width modelData: transaction - isIncoming: d.isIncoming + 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 @@ -153,10 +158,10 @@ Item { networkName: root.isTransactionValid ? RootStore.getNetworkShortName(transaction.chainId): "" symbol: root.isTransactionValid ? transaction.symbol : "" transferStatus: root.isTransactionValid ? RootStore.hex2Dec(transaction.txStatus): "" - shortTimeStamp: root.isTransactionValid ? LocaleUtils.formatTime(transaction.timestamp * 1000, Locale.ShortFormat): "" - savedAddressNameTo: root.isTransactionValid ? RootStore.getNameForSavedWalletAddress(transaction.to): "" - savedAddressNameFrom: root.isTransactionValid ? RootStore.getNameForSavedWalletAddress(transaction.from): "" - isSummary: false + 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 diff --git a/ui/imports/assets/icons/transaction/failed.svg b/ui/imports/assets/icons/transaction/failed.svg new file mode 100644 index 0000000000..95144d2f55 --- /dev/null +++ b/ui/imports/assets/icons/transaction/failed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/imports/assets/icons/transaction/finished.svg b/ui/imports/assets/icons/transaction/finished.svg new file mode 100644 index 0000000000..2a0c9698ca --- /dev/null +++ b/ui/imports/assets/icons/transaction/finished.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/imports/assets/icons/transaction/pending.svg b/ui/imports/assets/icons/transaction/pending.svg new file mode 100644 index 0000000000..ae6bea5352 --- /dev/null +++ b/ui/imports/assets/icons/transaction/pending.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/imports/assets/icons/transaction/verified.svg b/ui/imports/assets/icons/transaction/verified.svg new file mode 100644 index 0000000000..946358c3a3 --- /dev/null +++ b/ui/imports/assets/icons/transaction/verified.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/imports/shared/controls/TransactionDelegate.qml b/ui/imports/shared/controls/TransactionDelegate.qml index 53172690b4..4aff708ce7 100644 --- a/ui/imports/shared/controls/TransactionDelegate.qml +++ b/ui/imports/shared/controls/TransactionDelegate.qml @@ -9,187 +9,502 @@ import StatusQ.Controls 0.1 import utils 1.0 import shared 1.0 -import shared.stores 1.0 + +/*! + \qmltype TransactionDelegate + \inherits StatusListItem + \inqmlmodule shared.controls + \since shared.controls 1.0 + \brief Delegate for transaction activity list + + Delegate to display transaction activity data. + + \qml + TransactionDelegate { + id: delegate + width: ListView.view.width + modelData: model + swapCryptoValue: 0.18 + swapFiatValue: 340 + swapSymbol: "SNT" + timeStampText: LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000) + cryptoValue: 0.1234 + fiatValue: 123123 + currentCurrency: "USD" + networkName: "Optimism" + symbol: "ETH" + bridgeNetworkName: "Mainnet" + feeFiatValue: 10.34 + feeCryptoValue: 0.013 + transactionStatus: TransactionDelegate.Pending + transactionType: TransactionDelegate.Send + formatCurrencyAmount: RootStore.formatCurrencyAmount + loading: isModelDataValid && modelData.loadingTransaction + } + \endqml + + Additional usages should be handled using states. +*/ StatusListItem { id: root - property alias cryptoValueText: cryptoValueText - property alias fiatValueText: fiatValueText + signal retryClicked() property var modelData property string symbol - property bool isIncoming + 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 string currentCurrency property int transferStatus property double cryptoValue + property double swapCryptoValue // TODO fill when swap data is implemented property double fiatValue + property double swapFiatValue // TODO fill when swap data is implemented + property double feeCryptoValue // TODO fill when bridge data is implemented + property double feeFiatValue // TODO fill when bridge data is implemented property string networkIcon property string networkColor property string networkName - property string shortTimeStamp - property string savedAddressNameTo - property string savedAddressNameFrom - property bool isSummary: false + property string bridgeNetworkName // TODO fill when bridge data is implemented + property string timeStampText + property string addressNameTo + property string addressNameFrom + property var formatCurrencyAmount: function() {} readonly property bool isModelDataValid: modelData !== undefined && !!modelData readonly property bool isNFT: isModelDataValid && modelData.isNFT - readonly property string name: isModelDataValid ? - root.isNFT ? - modelData.nftName ? - modelData.nftName : - "#" + modelData.tokenID : - root.isSummary ? root.symbol : RootStore.formatCurrencyAmount(cryptoValue, symbol) : - "N/A" + readonly property string transactionValue: { + if (!isModelDataValid) + return qsTr("N/A") + if (root.isNFT) { + return modelData.nftName ? modelData.nftName : "#" + modelData.tokenID + } else { + return root.formatCurrencyAmount(cryptoValue, symbol) + } + } + readonly property string swapTransactionValue: { + if (!isModelDataValid) { + return qsTr("N/A") + } + return root.formatCurrencyAmount(swapCryptoValue, swapSymbol) + } - readonly property string image: isModelDataValid ? - root.isNFT ? - modelData.nftImageUrl ? - modelData.nftImageUrl : - "" : - root.symbol ? - Style.png("tokens/%1".arg(root.symbol)) : - "" : - "" + readonly property string tokenImage: { + if (!isModelDataValid) + return "" + if (root.isNFT) { + return modelData.nftImageUrl ? modelData.nftImageUrl : "" + } else { + return root.symbol ? Style.png("tokens/%1".arg(root.symbol)) : "" + } + } - readonly property string toAddress: !!savedAddressNameTo ? - savedAddressNameTo : + readonly property string swapTokenImage: { + if (!isModelDataValid) + return "" + return root.swapSymbol ? Style.png("tokens/%1".arg(root.swapSymbol)) : "" + } + + readonly property string toAddress: !!addressNameTo ? + addressNameTo : isModelDataValid ? Utils.compactAddress(modelData.to, 4) : "" - readonly property string fromAddress: !!savedAddressNameFrom ? - savedAddressNameFrom : + readonly property string fromAddress: !!addressNameFrom ? + addressNameFrom : isModelDataValid ? Utils.compactAddress(modelData.from, 4) : "" - state: "normal" - enabled: !loading - asset.isImage: !loading - asset.name: root.image - asset.isLetterIdenticon: loading - title: root.isModelDataValid ? - isIncoming ? - isSummary ? - qsTr("Receive %1").arg(root.name) : - qsTr("Received %1 from %2").arg(root.name).arg(root.fromAddress): - isSummary ? - qsTr("Send %1 to %2").arg(root.name).arg(root.toAddress) : - qsTr("Sent %1 to %2").arg(root.name).arg(root.toAddress) : - "" - subTitle: shortTimeStamp - inlineTagModel: 1 - inlineTagDelegate: InformationTag { - tagPrimaryLabel.text: networkName - tagPrimaryLabel.color: networkColor - image.source: !!networkIcon ? Style.svg("tiny/%1".arg(networkIcon)) : "" - customBackground: Component { - Rectangle { - color: "transparent" - border.width: 1 - border.color: Theme.palette.baseColor2 - radius: 36 + + property StatusAssetSettings statusIconAsset: StatusAssetSettings { + width: 12 + height: 12 + bgWidth: width + 2 + bgHeight: bgWidth + bgRadius: bgWidth / 2 + bgColor: root.color + color: "transparent" + name: { + switch(root.transactionStatus) { + case TransactionDelegate.TransactionStatus.Pending: + return Style.svg("transaction/pending") + case TransactionDelegate.TransactionStatus.Verified: + return Style.svg("transaction/verified") + case TransactionDelegate.TransactionStatus.Finished: + return Style.svg("transaction/finished") + case TransactionDelegate.TransactionStatus.Failed: + return Style.svg("transaction/failed") + default: + return "" } } - width: 51 - height: root.loading ? textMetrics.tightBoundingRect.height : 24 - rightComponent: transferStatus === Constants.TransactionStatus.Success ? completedIcon : loadingIndicator - loading: root.loading } - TextMetrics { - id: textMetrics - font: statusListItemSubTitle.font - text: statusListItemSubTitle.text + + property StatusAssetSettings tokenIconAsset: StatusAssetSettings { + width: 18 + height: 18 + bgWidth: width + bgHeight: height + bgColor: "transparent" + color: "transparent" + isImage: !loading + name: root.tokenImage + isLetterIdenticon: loading } - components: [ - ColumnLayout { - visible: !root.isNFT - Row { - Layout.alignment: Qt.AlignRight - spacing: 4 - StatusIcon { - color: isIncoming ? Theme.palette.successColor1 : Theme.palette.dangerColor1 - icon: "arrow-up" - rotation: isIncoming ? 135 : 45 - height: 18 - visible: !root.loading + + enum TransactionType { + Send, + Receive, + Buy, + Sell, + Destroy, + Swap, + Bridge + } + + enum TransactionStatus { + Pending, + Failed, + Verified, + Finished + } + + QtObject { + id: d + + property int loadingPixelSize: 13 + property int datePixelSize: 12 + property int titlePixelSize: 15 + property int subtitlePixelSize: 13 + } + + rightPadding: 16 + enabled: !loading + color: sensor.containsMouse ? Theme.palette.baseColor5 : Theme.palette.statusListItem.backgroundColor + + statusListItemIcon.active: (loading || root.asset.name) + asset { + width: 24 + height: 24 + isImage: false + imgIsIdenticon: true + isLetterIdenticon: loading + name: { + switch(root.transactionType) { + case TransactionDelegate.TransactionType.Send: + return "receive" + case TransactionDelegate.TransactionType.Receive: + return "send" + case TransactionDelegate.TransactionType.Buy: + case TransactionDelegate.TransactionType.Sell: + return "token" + case TransactionDelegate.TransactionType.Destroy: + return "destroy" + case TransactionDelegate.TransactionType.Swap: + return "swap" + case TransactionDelegate.TransactionType.Bridge: + return "bridge" + default: + return "" + } + } + bgColor: "transparent" + color: Theme.palette.black + bgBorderWidth: 1 + bgBorderColor: Theme.palette.primaryColor3 + } + + sensor.children: [ + StatusRoundIcon { + id: leftIconStatusIcon + visible: !root.loading + anchors { + right: root.statusListItemIcon.right + bottom: root.statusListItemIcon.bottom + } + asset: root.statusIconAsset + } + ] + + // Title + title: { + if (root.loading) { + return "dummmy" + } else if (!root.isModelDataValid) { + return "" + } + + const isPending = root.transactionStatus === TransactionDelegate.TransactionStatus.Pending + const failed = root.transactionStatus === TransactionDelegate.TransactionStatus.Failed + switch(root.transactionType) { + case TransactionDelegate.TransactionType.Send: + return failed ? qsTr("Send failed") : (isPending ? qsTr("Sending") : qsTr("Sent")) + case TransactionDelegate.TransactionType.Receive: + return failed ? qsTr("Receive failed") : (isPending ? qsTr("Receiving") : qsTr("Received")) + case TransactionDelegate.TransactionType.Buy: + return failed ? qsTr("Buy failed") : (isPending ? qsTr("Buying") : qsTr("Bought")) + case TransactionDelegate.TransactionType.Sell: + return failed ? qsTr("Sell failed") : (isPending ? qsTr("Selling") : qsTr("Sold")) + case TransactionDelegate.TransactionType.Destroy: + return failed ? qsTr("Destroy failed") : (isPending ? qsTr("Destroying") : qsTr("Destroyed")) + case TransactionDelegate.TransactionType.Swap: + return failed ? qsTr("Swap failed") : (isPending ? qsTr("Swapping") : qsTr("Swapped")) + case TransactionDelegate.TransactionType.Bridge: + return failed ? qsTr("Bridge failed") : (isPending ? qsTr("Bridging") : qsTr("Bridged")) + default: + return "" + } + } + statusListItemTitleArea.anchors.rightMargin: root.rightPadding + statusListItemTitle.font.weight: Font.DemiBold + statusListItemTitle.font.pixelSize: root.loading ? d.loadingPixelSize : d.titlePixelSize + + // title icons and date + statusListItemTitleIcons.sourceComponent: Row { + spacing: 8 + Row { + visible: !root.loading + spacing: swapTokenImage.visible ? -tokenImage.width * 0.2 : 0 + StatusRoundIcon { + id: tokenImage + anchors.verticalCenter: parent.verticalCenter + asset: root.tokenIconAsset + } + StatusRoundIcon { + id: swapTokenImage + visible: !root.isNFT && !!root.swapTokenImage && root.transactionType === TransactionDelegate.TransactionType.Swap + anchors.verticalCenter: parent.verticalCenter + asset: StatusAssetSettings { + width: root.tokenIconAsset.width + height: root.tokenIconAsset.height + bgWidth: width + 2 + bgHeight: height + 2 + bgRadius: bgWidth / 2 + bgColor: root.color + isImage:root.tokenIconAsset.isImage + color: root.tokenIconAsset.color + name: root.swapTokenImage + isLetterIdenticon: root.tokenIconAsset.isLetterIdenticon } + } + } + StatusTextWithLoadingState { + anchors.verticalCenter: parent.verticalCenter + text: root.loading ? root.title : root.timeStampText + verticalAlignment: Qt.AlignVCenter + font.pixelSize: root.loading ? d.loadingPixelSize : d.datePixelSize + visible: !!text + loading: root.loading + customColor: Theme.palette.baseColor1 + } + } + + // subtitle + subTitle: { + if (!root.isModelDataValid) { + return "" + } + switch(root.transactionType) { + case TransactionDelegate.TransactionType.Receive: + return qsTr("%1 from %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName) + case TransactionDelegate.TransactionType.Buy: + case TransactionDelegate.TransactionType.Sell: + return qsTr("%1 on %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName) + case TransactionDelegate.TransactionType.Destroy: + return qsTr("%1 at %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName) + case TransactionDelegate.TransactionType.Swap: + return qsTr("%1 to %2 via %3").arg(transactionValue).arg(swapTransactionValue).arg(networkName) + case TransactionDelegate.TransactionType.Bridge: + return qsTr("%1 from %2 to %3").arg(transactionValue).arg(bridgeNetworkName).arg(networkName) + default: + return qsTr("%1 to %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName) + } + } + statusListItemSubTitle.maximumLoadingStateWidth: 300 + statusListItemSubTitle.customColor: Theme.palette.directColor1 + statusListItemSubTitle.font.pixelSize: root.loading ? d.loadingPixelSize : d.subtitlePixelSize + statusListItemTagsRowLayout.anchors.topMargin: 4 // Spacing between title row nad subtitle row + + // Right side components + components: [ + Loader { + active: !headerStatusLoader.active + visible: active + sourceComponent: ColumnLayout { StatusTextWithLoadingState { id: cryptoValueText - text: RootStore.formatCurrencyAmount(cryptoValue, root.symbol) - Binding on width { - when: root.loading - value: 111 - restoreMode: Binding.RestoreBindingOrValue + text: { + if (root.loading) { + return "dummy text" + } else if (!root.isModelDataValid || root.isNFT) { + return "" + } + + switch(root.transactionType) { + case TransactionDelegate.TransactionType.Send: + case TransactionDelegate.TransactionType.Sell: + return "-" + root.transactionValue + case TransactionDelegate.TransactionType.Buy: + case TransactionDelegate.TransactionType.Receive: + return "+" + root.transactionValue + case TransactionDelegate.TransactionType.Swap: + return "-%2 / +%5" + .arg(Theme.palette.directColor1) + .arg(root.transactionValue) + .arg(Theme.palette.baseColor1) + .arg(Theme.palette.successColor1) + .arg(root.swapTransactionValue) + case TransactionDelegate.TransactionType.Bridge: + return "-" + root.formatCurrencyAmount(feeCryptoValue, root.symbol) + default: + return "" + } + } + horizontalAlignment: Qt.AlignRight + Layout.alignment: Qt.AlignRight + font.pixelSize: root.loading ? d.loadingPixelSize : 13 + customColor: { + switch(root.transactionType) { + case TransactionDelegate.TransactionType.Receive: + case TransactionDelegate.TransactionType.Buy: + case TransactionDelegate.TransactionType.Swap: + return Theme.palette.successColor1 + default: + return Theme.palette.directColor1 + } } - customColor: Theme.palette.directColor1 loading: root.loading } + StatusTextWithLoadingState { + id: fiatValueText + Layout.alignment: Qt.AlignRight + horizontalAlignment: Qt.AlignRight + text: { + if (root.loading) { + return "dummy text" + } else if (!root.isModelDataValid || root.isNFT) { + return "" + } + switch(root.transactionType) { + case TransactionDelegate.TransactionType.Send: + case TransactionDelegate.TransactionType.Sell: + case TransactionDelegate.TransactionType.Buy: + return "-" + root.formatCurrencyAmount(root.fiatValue, root.currentCurrency) + case TransactionDelegate.TransactionType.Receive: + return "+" + root.formatCurrencyAmount(root.fiatValue, root.currentCurrency) + case TransactionDelegate.TransactionType.Swap: + return "-%1 / +%2".arg(root.formatCurrencyAmount(root.fiatValue, root.currentCurrency)) + .arg(root.formatCurrencyAmount(root.swapFiatValue, root.currentCurrency)) + case TransactionDelegate.TransactionType.Bridge: + return "-" + root.formatCurrencyAmount(root.feeFiatValue, root.currentCurrency) + default: + return "" + } + } + font.pixelSize: root.loading ? d.loadingPixelSize : 12 + customColor: Theme.palette.baseColor1 + loading: root.loading + } } - StatusTextWithLoadingState { - id: fiatValueText - Layout.alignment: Qt.AlignRight - text: RootStore.formatCurrencyAmount(fiatValue, root.currentCurrency) - font.pixelSize: 15 - customColor: Theme.palette.baseColor1 - loading: root.loading + }, + Loader { + id: headerStatusLoader + active: false + visible: active + sourceComponent: Rectangle { + id: statusRect + width: transactionTypeIcon.width + (retryButton.visible ? retryButton.width + 5 : 0) + height: transactionTypeIcon.height + anchors.verticalCenter: parent.verticalCenter + color: "transparent" + radius: 100 + border { + width: retryButton.visible ? 1 : 0 + color: root.asset.bgBorderColor + } + + StatusButton { + id: retryButton + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 10 + radius: height / 2 + height: parent.height * 0.7 + verticalPadding: 0 + horizontalPadding: radius + textFillWidth: true + text: qsTr("Retry") + size: StatusButton.Small + type: StatusButton.Primary + visible: !root.loading && root.transactionStatus === TransactionDelegate.Failed + onClicked: root.retryClicked() + } + + StatusSmartIdenticon { + id: transactionTypeIcon + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + enabled: false + asset: root.asset + active: !!root.asset.name + loading: root.loading + name: root.title + } + StatusRoundIcon { + visible: !root.loading + anchors { + right: transactionTypeIcon.right + bottom: transactionTypeIcon.bottom + } + asset: root.statusIconAsset + } } } ] - Component { - id: loadingIndicator - StatusLoadingIndicator { - height: 10 - width: 10 - } - } - - Component { - id: completedIcon - StatusIcon { - visible: icon !== "" - icon: "checkmark" - color: Theme.palette.baseColor1 - width: 10 - height: 10 - } - } - states: [ State { - name: "normal" + name: "header" PropertyChanges { - target: asset - width: 40 - height: 40 + target: headerStatusLoader + active: true } PropertyChanges { - target: statusListItemTitle - font.weight: Font.Medium - font.pixelSize: 15 + target: leftIconStatusIcon + visible: false } PropertyChanges { - target: cryptoValueText - font.pixelSize: 15 - } - }, - State { - name: "big" - PropertyChanges { - target: asset - width: 50 - height: 50 + target: root.statusListItemIcon + active: false } PropertyChanges { - target: statusListItemTitle - font.weight: Font.Bold - font.pixelSize: 17 + target: root.asset + bgBorderWidth: root.transactionStatus === TransactionDelegate.Failed ? 0 : 1 + width: 34 + height: 34 + bgWidth: 56 + bgHeight: 56 } PropertyChanges { - target: cryptoValueText - font.pixelSize: 17 + target: root.statusIconAsset + width: 17 + height: 17 + } + PropertyChanges { + target: root.tokenIconAsset + width: 20 + height: 20 + } + PropertyChanges { + target: d + titlePixelSize: 17 + datePixelSize: 13 + subtitlePixelSize: 15 + loadingPixelSize: 14 } } ] diff --git a/ui/imports/shared/stores/RootStore.qml b/ui/imports/shared/stores/RootStore.qml index f0cac70692..8a02669965 100644 --- a/ui/imports/shared/stores/RootStore.qml +++ b/ui/imports/shared/stores/RootStore.qml @@ -54,6 +54,10 @@ QtObject { return networksModule.all.getNetworkShortName(chainId) } + function getNetworkFullName(chainId) { + return networksModule.all.getNetworkFullName(chainId) + } + function getNetworkIconUrl(symbol) { return networksModule.all.getNetworkIconUrl(symbol) } diff --git a/ui/imports/shared/views/HistoryView.qml b/ui/imports/shared/views/HistoryView.qml index 863a9741cf..d95caadb9c 100644 --- a/ui/imports/shared/views/HistoryView.qml +++ b/ui/imports/shared/views/HistoryView.qml @@ -17,6 +17,8 @@ import "../popups" import "../stores" import "../controls" +import AppLayouts.Wallet.stores 1.0 as WalletStores + ColumnLayout { id: root @@ -193,19 +195,19 @@ ColumnLayout { TransactionDelegate { width: ListView.view.width modelData: model - isIncoming: isModelDataValid ? modelData.to === root.overview.mixedcaseAddress: false + transactionType: isModelDataValid && modelData.to.toLowerCase() === root.overview.mixedcaseAddress.toLowerCase() ? TransactionDelegate.Receive : TransactionDelegate.Send currentCurrency: RootStore.currentCurrency cryptoValue: isModelDataValid ? modelData.value.amount : 0.0 fiatValue: isModelDataValid ? RootStore.getFiatValue(cryptoValue, symbol, currentCurrency): 0.0 networkIcon: isModelDataValid ? RootStore.getNetworkIcon(modelData.chainId) : "" networkColor: isModelDataValid ? RootStore.getNetworkColor(modelData.chainId) : "" - networkName: isModelDataValid ? RootStore.getNetworkShortName(modelData.chainId) : "" + networkName: isModelDataValid ? RootStore.getNetworkFullName(modelData.chainId) : "" symbol: isModelDataValid && !!modelData.symbol ? modelData.symbol : "" transferStatus: isModelDataValid ? RootStore.hex2Dec(modelData.txStatus) : "" - shortTimeStamp: isModelDataValid ? LocaleUtils.formatTime(modelData.timestamp * 1000, Locale.ShortFormat) : "" - savedAddressNameTo: isModelDataValid ? RootStore.getNameForSavedWalletAddress(modelData.to) : "" - savedAddressNameFrom: isModelDataValid ? RootStore.getNameForSavedWalletAddress(modelData.from) : "" - isSummary: true + timeStampText: isModelDataValid ? LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000) : "" + addressNameTo: isModelDataValid ? WalletStores.RootStore.getNameForAddress(modelData.to) : "" + addressNameFrom: isModelDataValid ? WalletStores.RootStore.getNameForAddress(modelData.from) : "" + formatCurrencyAmount: RootStore.formatCurrencyAmount onClicked: launchTransactionDetail(modelData) loading: isModelDataValid ? modelData.loadingTransaction : false