From 6ecb636ec2144a5d1b0e23fc3f57b101505e91b3 Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Sat, 7 Dec 2024 00:38:38 +0100 Subject: [PATCH] feat(@desktop/wallet): Add a temporary recipient selector so that we can quickly unblock other peoples work --- storybook/pages/SimpleSendModalPage.qml | 75 +++++-- .../Wallet/panels/RecipientSelectorPanel.qml | 187 +++++++++++++++++ ui/app/AppLayouts/Wallet/panels/qmldir | 1 + .../popups/simpleSend/SimpleSendModal.qml | 37 +++- .../AppLayouts/Wallet/views/RecipientView.qml | 193 ++++++++++++++++++ ui/app/AppLayouts/Wallet/views/qmldir | 1 + ui/app/mainui/AppMain.qml | 2 + ui/app/mainui/SendModalHandler.qml | 5 + 8 files changed, 487 insertions(+), 14 deletions(-) create mode 100644 ui/app/AppLayouts/Wallet/panels/RecipientSelectorPanel.qml create mode 100644 ui/app/AppLayouts/Wallet/views/RecipientView.qml diff --git a/storybook/pages/SimpleSendModalPage.qml b/storybook/pages/SimpleSendModalPage.qml index cba06af5ea..041f95c944 100644 --- a/storybook/pages/SimpleSendModalPage.qml +++ b/storybook/pages/SimpleSendModalPage.qml @@ -6,6 +6,7 @@ import SortFilterProxyModel 0.2 import StatusQ 0.1 import StatusQ.Core 0.1 +import StatusQ.Core.Backpressure 0.1 import Models 1.0 import Storybook 1.0 @@ -43,6 +44,22 @@ SplitView { stripTrailingZeroes: false }) } + + readonly property var savedAddressesModel: ListModel { + Component.onCompleted: { + for (let i = 0; i < 10; i++) + append({ + name: "some saved addr name " + i, + ens: [], + address: "0x2B748A02e06B159C7C3E98F5064577B96E55A7b4", + }) + append({ + name: "some saved ENS name ", + ens: ["me@status.eth"], + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4", + }) + } + } } PopupBackground { @@ -77,6 +94,9 @@ SplitView { collectiblesModel: collectiblesSelectionAdaptor.model networksModel: d.filteredNetworksModel + savedAddressesModel: d.savedAddressesModel + recentRecipientsModel: WalletTransactionsModel{} + currentCurrency: "USD" fnFormatCurrencyAmount: function(amount, symbol, options = null, locale = null) { if (isNaN(amount)) { @@ -86,6 +106,15 @@ SplitView { return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale) } + fnResolveENS: Backpressure.debounce(root, 500, function (ensName, uuid) { + if (!!ensName && ensName.endsWith(".eth")) { + // return some valid address + simpleSend.ensNameResolved(ensName, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4", uuid) + } else { + simpleSend.ensNameResolved(ensName, "", uuid) // invalid + } + }) + Binding on selectedAccountAddress { value: accountsCombobox.currentValue ?? "" } @@ -462,7 +491,7 @@ SplitView { } Text { - text: "Accounts Selection" + text: "Select an accounts" } ComboBox { id: accountsCombobox @@ -478,17 +507,13 @@ SplitView { valueRole: "address" currentIndex: 0 } - Text { - text: "account selected is: \n" - + simpleSend.selectedAccountAddress - } CheckBox { id: testNetworksCheckbox text: "are test networks enabled" } Text { - text: "Networks Selection" + text: "Select a network" } ComboBox { id: networksCombobox @@ -497,12 +522,9 @@ SplitView { valueRole: "chainId" currentIndex: 0 } - Text { - text: "network selected is: " + simpleSend.selectedChainId - } Text { - text: "Tokens selection" + text: "Select a token" } ComboBox { id: tokensCombobox @@ -524,9 +546,6 @@ SplitView { textRole: "name" valueRole: "tokensKey" } - Text { - text: "token selected is: " + simpleSend.selectedTokenKey - } RowLayout { Layout.fillWidth: true @@ -546,9 +565,39 @@ SplitView { Text { text: "amount selected in base unit: " + simpleSend.selectedAmountInBaseUnit } + + Text { + text: "Select a recipient" + } + RowLayout { + Layout.fillWidth: true + TextField { + id: recipientInput + Layout.preferredWidth: 200 + Layout.preferredHeight: 50 + } + Button { + text: "update in modal" + onClicked: simpleSend.selectedRecipientAddress = recipientInput.text + } + } + + Text { + text: "account selected is: \n" + + simpleSend.selectedAccountAddress + } + Text { + text: "network selected is: " + simpleSend.selectedChainId + } + Text { + text: "token selected is: " + simpleSend.selectedTokenKey + } Text { text: "amount entered is: " + simpleSend.selectedAmount } + Text { + text: "selected recipient is: \n" + simpleSend.selectedRecipientAddress + } RolesRenamingModel { id: collectiblesKeyModel diff --git a/ui/app/AppLayouts/Wallet/panels/RecipientSelectorPanel.qml b/ui/app/AppLayouts/Wallet/panels/RecipientSelectorPanel.qml new file mode 100644 index 0000000000..b02ed6236c --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/RecipientSelectorPanel.qml @@ -0,0 +1,187 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.1 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 + +import shared.controls 1.0 as SharedControls +// TODO: remove all files and dependecies with this location once old send modal is removed +import shared.popups.send.controls 1.0 +import shared.popups.send 1.0 + +import utils 1.0 + +import AppLayouts.Wallet.views 1.0 + +Rectangle { + id: root + + required property var savedAddressesModel + required property var myAccountsModel + required property var recentRecipientsModel + + property alias selectedRecipientAddress: recipientInputLoader.selectedRecipientAddress + property alias selectedRecipientType: recipientInputLoader.selectedRecipientType + + signal resolveENS(string ensName, string uuid) + + function ensNameResolved(resolvedPubKey, resolvedAddress, uuid) { + recipientInputLoader.ensNameResolved(resolvedPubKey, resolvedAddress, uuid) + } + + implicitHeight: childrenRect.height + color: Theme.palette.indirectColor1 + radius: 8 + + ColumnLayout { + id: layout + + width: parent.width + spacing: 0 + + RecipientView { + id: recipientInputLoader + + Layout.fillWidth: true + + savedAddressesModel: root.savedAddressesModel + myAccountsModel: root.myAccountsModel + + onResolveENS: root.resolveENS(ensName, uuid) + } + + StatusTabBar { + id: recipientTypeTabBar + + objectName: "recipientTypeTabBar" + + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + Layout.topMargin: 12 + + StatusTabButton { + width: implicitWidth + objectName: "recentAddressesTab" + text: qsTr("Recent") + } + StatusTabButton { + width: implicitWidth + objectName: "savedAddressesTab" + text: qsTr("Saved") + } + StatusTabButton { + width: implicitWidth + objectName: "myAccountsTab" + text: qsTr("My Accounts") + } + + visible: !root.selectedRecipientAddress + } + + Repeater { + id: repeater + + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + model: { + switch(recipientTypeTabBar.currentIndex) { + case 0: + return recentsObjModel + case 1: + return savedObjModel + case 2: + return myAccountsObjModel + } + } + } + } + + DelegateModel { + id: recentsObjModel + + model: root.recentRecipientsModel + delegate: StatusListItem { + id: listItem + + property var entry: model.activityEntry + property bool isIncoming: entry.txType === Constants.TransactionType.Receive + + Layout.fillWidth: true + title: isIncoming ? StatusQUtils.Utils.elideText(entry.sender,6,4) : StatusQUtils.Utils.elideText(entry.recipient,6,4) + subTitle: LocaleUtils.getTimeDifference(new Date(parseInt(entry.timestamp) * 1000), new Date()) + statusListItemTitle.elide: Text.ElideMiddle + statusListItemTitle.wrapMode: Text.NoWrap + radius: 0 + color: sensor.containsMouse || highlighted ? Theme.palette.baseColor2 : "transparent" + statusListItemComponentsSlot.spacing: 5 + components: [ + StatusIcon { + id: transferIcon + height: 15 + width: 15 + color: listItem.isIncoming ? Theme.palette.successColor1 : Theme.palette.dangerColor1 + icon: listItem.isIncoming ? "arrow-down" : "arrow-up" + rotation: 45 + }, + StatusTextWithLoadingState { + font.pixelSize: 15 + customColor: Theme.palette.directColor1 + text: LocaleUtils.currencyAmountToLocaleString(entry.amountCurrency) + } + ] + onClicked: { + root.selectedRecipientType = Helpers.RecipientAddressObjectType.RecentsAddress + let isIncoming = entry.txType === Constants.TransactionType.Receive + let selectedAddress = isIncoming ? entry.sender : entry.recipient + root.selectedRecipientAddress = selectedAddress + } + visible: !root.selectedRecipientAddress + } + } + + DelegateModel { + id: savedObjModel + + model: root.savedAddressesModel + delegate: SavedAddressListItem { + Layout.fillWidth: true + modelData: model + onClicked: { + root.selectedRecipientType = Helpers.RecipientAddressObjectType.SavedAddress + root.selectedRecipientAddress = modelData.address + } + visible: !root.selectedRecipientAddress + } + } + + DelegateModel { + id: myAccountsObjModel + + model: root.myAccountsModel + delegate: SharedControls.WalletAccountListItem { + required property var model + + Layout.fillWidth: true + + name: model.name + address: model.address + emoji: model.emoji + walletColor: Utils.getColorForId(model.colorId) + currencyBalance: model.currencyBalance + walletType: model.walletType + migratedToKeycard: model.migratedToKeycard ?? false + accountBalance: model.accountBalance ?? null + onClicked: { + root.selectedRecipientType = Helpers.RecipientAddressObjectType.Account + root.selectedRecipientAddress = model.address + } + visible: !root.selectedRecipientAddress + } + } +} diff --git a/ui/app/AppLayouts/Wallet/panels/qmldir b/ui/app/AppLayouts/Wallet/panels/qmldir index 01221de437..cb8395528c 100644 --- a/ui/app/AppLayouts/Wallet/panels/qmldir +++ b/ui/app/AppLayouts/Wallet/panels/qmldir @@ -14,3 +14,4 @@ TokenSelectorPanel 1.0 TokenSelectorPanel.qml WalletHeader 1.0 WalletHeader.qml SendModalHeader 1.0 SendModalHeader.qml StickySendModalHeader 1.0 StickySendModalHeader.qml +RecipientSelectorPanel 1.0 RecipientSelectorPanel.qml diff --git a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml index 9b4636d689..8061906be1 100644 --- a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml @@ -71,6 +71,8 @@ StatusDialog { Only networks valid as per mainnet/testnet selection **/ required property var networksModel + required property var savedAddressesModel + required property var recentRecipientsModel /** Input property holds currently selected Fiat currency **/ required property string currentCurrency /** Input function to format currency amount to locale string **/ @@ -92,6 +94,14 @@ StatusDialog { e.g. 1000000000000000000 for 1 ETH **/ readonly property string selectedAmountInBaseUnit: amountToSend.amount + /** TODO: replace with new and improved recipient selector StatusDateRangePicker + TBD under https://github.com/status-im/status-desktop/issues/16916 **/ + property alias selectedRecipientAddress: recipientsPanel.selectedRecipientAddress + required property var fnResolveENS + function ensNameResolved(resolvedPubKey, resolvedAddress, uuid) { + recipientsPanel.ensNameResolved(resolvedPubKey, resolvedAddress, uuid) + } + QtObject { id: d @@ -172,7 +182,6 @@ StatusDialog { padding: 0 leftPadding: Theme.xlPadding rightPadding: Theme.xlPadding - bottomPadding: Theme.xlPadding topMargin: margins + accountSelector.height + Theme.padding background: StatusDialogBackground { @@ -345,6 +354,32 @@ StatusDialog { disabledTextColor: type === StatusBaseButton.Type.Danger ? Theme.palette.dangerColor1 : Theme.palette.primaryColor1 } } + + /** TODO: replace with new and improved recipient selector TBD under + https://github.com/status-im/status-desktop/issues/16916 **/ + ColumnLayout { + spacing: Theme.halfPadding + Layout.fillWidth: true + StatusBaseText { + elide: Text.ElideRight + text: qsTr("To") + font.pixelSize: 15 + color: Theme.palette.directColor1 + } + RecipientSelectorPanel { + id: recipientsPanel + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.bottomMargin: Theme.xlPadding + + savedAddressesModel: root.savedAddressesModel + myAccountsModel: root.accountsModel + recentRecipientsModel: root.recentRecipientsModel + + onResolveENS: root.fnResolveENS(ensName, uuid) + } + } } } } diff --git a/ui/app/AppLayouts/Wallet/views/RecipientView.qml b/ui/app/AppLayouts/Wallet/views/RecipientView.qml new file mode 100644 index 0000000000..79c4ec15a6 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/views/RecipientView.qml @@ -0,0 +1,193 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Backpressure 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils + +import AppLayouts.Wallet 1.0 + +import shared.controls 1.0 as SharedControls +import shared.stores.send 1.0 +import shared.popups.send.panels 1.0 +import shared.popups.send 1.0 +import shared.popups.send.controls 1.0 + +import utils 1.0 + +Loader { + id: root + + required property var savedAddressesModel + required property var myAccountsModel + + property string selectedRecipientAddress + property int selectedRecipientType: Helpers.RecipientAddressObjectType.Address + property bool interactive: true + + signal resolveENS(string ensName, string uuid) + + function ensNameResolved(resolvedPubKey, resolvedAddress, uuid) { + if(uuid !== d.uuid) { + return + } + root.selectedRecipientAddress = resolvedAddress + } + + QtObject { + id: d + + property bool isValidAddress: true + property bool isBeingEvaluated: false + + property string uuid + + readonly property var validateInput: Backpressure.debounce(root, 500, function (address) { + d.isValidAddress = Utils.isValidAddress(address) + const isENSName = Utils.isValidEns(address) + + if(d.isValidAddress) { + root.selectedRecipientAddress = address + d.isBeingEvaluated = false + } + else if(isENSName) { + d.uuid = Utils.uuid() + return root.resolveENS(address, uuid) + } else { + root.selectedRecipientAddress = "" + d.isBeingEvaluated = false + } + }) + + readonly property var accountsSelectedEntry: ModelEntry { + sourceModel: root.myAccountsModel + key: "address" + value: root.selectedRecipientAddress + } + + readonly property var savedAddrSelectedEntry: ModelEntry { + sourceModel: root.savedAddressesModel + key: "address" + value: root.selectedRecipientAddress + + } + + function clearValues() { + root.selectedRecipientAddress = "" + root.selectedRecipientType = Helpers.RecipientAddressObjectType.Address + } + } + + sourceComponent: root.selectedRecipientType === Helpers.RecipientAddressObjectType.SavedAddress ? + savedAddressRecipient: + root.selectedRecipientType === Helpers.RecipientAddressObjectType.Account ? + myAccountRecipient: + root.selectedRecipientType === Helpers.RecipientAddressObjectType.RecentsAddress ? + recentsRecipient : addressRecipient + + Component { + id: savedAddressRecipient + SavedAddressListItem { + implicitWidth: parent.width + modelData: d.savedAddrSelectedEntry.item + radius: 8 + clearVisible: true + color: Theme.palette.indirectColor1 + sensor.enabled: false + subTitle: { + if(!!modelData) { + if (!!modelData && !!modelData.ens && modelData.ens.length > 0) + return Utils.richColorText(modelData.ens, Theme.palette.directColor1) + else + return StatusQUtils.Utils.elideText(modelData.address,6,4) + } + return "" + } + onCleared: d.clearValues() + } + } + + Component { + id: myAccountRecipient + SharedControls.WalletAccountListItem { + id: accountItem + readonly property var modelData: d.accountsSelectedEntry.item + + name: !!modelData ? modelData.name : "" + address: !!modelData ? modelData.address : "" + emoji: !!modelData ? modelData.emoji : "" + walletColor: !!modelData ? Utils.getColorForId(modelData.colorId): "" + currencyBalance: !!modelData ? modelData.currencyBalance : "" + walletType: !!modelData ? modelData.walletType : "" + migratedToKeycard: !!modelData ? modelData.migratedToKeycard ?? false : false + accountBalance: !!modelData ? modelData.accountBalance : null + + width: parent.width + radius: 8 + clearVisible: true + color: Theme.palette.indirectColor1 + sensor.enabled: false + subTitle: { + if(!!modelData) { + return StatusQUtils.Utils.elideAndFormatWalletAddress(modelData.address) + } + return "" + } + onCleared: d.clearValues() + } + } + + Component { + id: recentsRecipient + + SendRecipientInput { + width: parent.width + height: visible ? implicitHeight: 0 + + interactive: root.interactive + input.edit.enabled: false + input.edit.textFormat: Text.AutoText + text: root.selectedRecipientAddress + + onClearClicked: d.clearValues() + } + } + + Component { + id: addressRecipient + + SendRecipientInput { + function validateInput() { + const plainText = StatusQUtils.StringUtils.plainText(text) + d.isBeingEvaluated = true + d.validateInput(plainText) + } + + width: parent.width + height: visible ? implicitHeight: 0 + + interactive: root.interactive + checkMarkVisible: !d.isBeingEvaluated && d.isValidAddress + loading: d.isBeingEvaluated + input.edit.textFormat: Text.AutoText + + text: { + if(!!root.selectedRecipientAddress ) { + return root.selectedRecipientAddress + } + return text + } + + onTextChanged: Qt.callLater(() => validateInput()) + onClearClicked: { + text = "" + d.clearValues() + } + onValidateInputRequested: Qt.callLater(() => validateInput()) + } + } +} + diff --git a/ui/app/AppLayouts/Wallet/views/qmldir b/ui/app/AppLayouts/Wallet/views/qmldir index e578333df5..54c55397ed 100644 --- a/ui/app/AppLayouts/Wallet/views/qmldir +++ b/ui/app/AppLayouts/Wallet/views/qmldir @@ -6,3 +6,4 @@ TokenSelectorAssetDelegate 1.0 TokenSelectorAssetDelegate.qml TokenSelectorCollectibleDelegate 1.0 TokenSelectorCollectibleDelegate.qml TokenSelectorSectionDelegate 1.0 TokenSelectorSectionDelegate.qml AccountContextMenu 1.0 AccountContextMenu.qml +RecipientView 1.0 RecipientView.qml diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index a351e7f5ee..13933e7e14 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -671,6 +671,8 @@ Item { fnFormatCurrencyAmount: function(amount, symbol, options = null, locale = null) { return appMain.currencyStore.formatCurrencyAmount(amount, symbol) } + savedAddressesModel: WalletStores.RootStore.savedAddresses + recentRecipientsModel: appMain.transactionStore.tempActivityController1Model Component.onCompleted: { // It's requested from many nested places, so as a workaround we use diff --git a/ui/app/mainui/SendModalHandler.qml b/ui/app/mainui/SendModalHandler.qml index b6223898bd..b69c1346ff 100644 --- a/ui/app/mainui/SendModalHandler.qml +++ b/ui/app/mainui/SendModalHandler.qml @@ -88,6 +88,8 @@ QtObject { required property var showCommunityAssetsInSend /** required function to format currency amount to locale string **/ required property var fnFormatCurrencyAmount + required property var savedAddressesModel + required property var recentRecipientsModel function openSend(params = {}) { // TODO remove once simple send is feature complete @@ -221,6 +223,9 @@ QtObject { collectiblesModel: collectiblesSelectionAdaptor.model networksModel: root.filteredFlatNetworksModel + savedAddressesModel: root.savedAddressesModel + recentRecipientsModel: root.recentRecipientsModel + currentCurrency: root.currentCurrency fnFormatCurrencyAmount: root.fnFormatCurrencyAmount