diff --git a/storybook/pages/RequestPaymentModalPage.qml b/storybook/pages/RequestPaymentModalPage.qml new file mode 100644 index 0000000000..dc3935f3b9 --- /dev/null +++ b/storybook/pages/RequestPaymentModalPage.qml @@ -0,0 +1,187 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import SortFilterProxyModel 0.2 +import QtTest 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.Utils 0.1 + +import utils 1.0 +import Storybook 1.0 +import Models 1.0 + +import mainui 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStores +import AppLayouts.Chat.popups 1.0 +import AppLayouts.stores 1.0 as AppLayoutStores +import shared.stores 1.0 as SharedStores + +// TODO_ES remove unneeded imports + +SplitView { + id: root + + Logs { id: logs } + + orientation: Qt.Horizontal + + ListModel { + id: plainTokensModel + ListElement { + key: "aave" + name: "Aave" + symbol: "AAVE" + image: "https://cryptologos.cc/logos/aave-aave-logo.png" + communityId: "" + } + ListElement { + key: "usdc" + name: "USDC" + symbol: "USDC" + image: "" + communityId: "" + } + ListElement { + key: "hst" + name: "Decision Token" + symbol: "HST" + image: "https://etherscan.io/token/images/horizonstate2_28.png" + communityId: "" + } + } + + QtObject { + id: d + readonly property var tokenBySymbolModel: TokensBySymbolModel {} + + function launchPopup() { + requestPaymentModalComponent.createObject(root) + } + + readonly property var accounts: WalletAccountsModel {} + readonly property SharedStores.CurrenciesStore currencyStore: SharedStores.CurrenciesStore {} + readonly property var flatNetworks: NetworksModel.flatNetworks + readonly property var walletAssetsStore: WalletStores.WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: WalletStores.TokensStore { + plainTokensBySymbolModel: TokensBySymbolModel {} + } + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel + } + + readonly property string selectedAccountAddress: ctrlAccount.currentValue ?? "" + readonly property int selectedNetworkChainId: ctrlSelectedNetworkChainId.currentValue ?? -1 + + readonly property SharedStores.RequestPaymentStore requestPaymentStore: SharedStores.RequestPaymentStore { + currencyStore: d.currencyStore + flatNetworksModel: d.flatNetworks + processedAssetsModel: d.walletAssetsStore.renamedTokensBySymbolModel + accountsModel: d.accounts + } + } + + PopupBackground { + id: popupBg + + SplitView.fillWidth: true + SplitView.fillHeight: true + + Button { + id: reopenButton + anchors.centerIn: parent + text: "Reopen" + enabled: !requestPaymentModalComponent.visible + + onClicked: d.launchPopup() + } + + Component.onCompleted: d.launchPopup() + + Component { + id: requestPaymentModalComponent + RequestPaymentModal { + id: requestPaymentModal + visible: true + modal: false + closePolicy: Popup.CloseOnEscape + destroyOnClose: true + + store: d.requestPaymentStore + + Connections { + target: d + function onSelectedNetworkChainIdChanged() { + requestPaymentModal.selectedNetworkChainId = d.selectedNetworkChainId + } + function onSelectedAccountAddressChanged() { + requestPaymentModal.selectedAccountAddress = d.selectedAccountAddress + } + } + Component.onCompleted: { + if (d.selectedNetworkChainId > -1) + requestPaymentModal.selectedNetworkChainId = d.selectedNetworkChainId + if (!!d.selectedAccountAddress) + requestPaymentModal.selectedAccountAddress = d.selectedAccountAddress + } + } + } + } + + ScrollView { + id: rightPanel + SplitView.minimumWidth: 300 + SplitView.preferredWidth: 300 + SplitView.minimumHeight: 300 + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: 10 + spacing: 10 + + Label { + text: "pre-selection:" + } + + RowLayout { + Layout.fillWidth: true + Label { + text: "Chain:" + } + ComboBox { + Layout.fillWidth: true + id: ctrlSelectedNetworkChainId + model: d.flatNetworks + textRole: "chainName" + valueRole: "chainId" + displayText: currentIndex === -1 ? "All chains" : currentText + currentIndex: -1 // all chains + } + } + + RowLayout { + Layout.fillWidth: true + Label { text: "Account:" } + ComboBox { + Layout.fillWidth: true + id: ctrlAccount + textRole: "name" + valueRole: "address" + displayText: currentText || "----" + model: SortFilterProxyModel { + sourceModel: d.accounts + sorters: RoleSorter { roleName: "position" } + } + currentIndex: -1 + } + } + } + } +} + +// category: Popups diff --git a/storybook/pages/StatusChatInputPage.qml b/storybook/pages/StatusChatInputPage.qml index d4fadb6c2e..358a96d043 100644 --- a/storybook/pages/StatusChatInputPage.qml +++ b/storybook/pages/StatusChatInputPage.qml @@ -11,6 +11,7 @@ import shared.stores 1.0 as SharedStores import StatusQ.Core.Utils 0.1 as SQUtils +import AppLayouts.Wallet.stores 1.0 as WalletStores import AppLayouts.Chat.stores 1.0 as ChatStores SplitView { @@ -99,6 +100,8 @@ SplitView { } } + requestPaymentStore: d.requestPaymentStore + onSendMessage: { logs.logEvent("StatusChatInput::sendMessage", ["MessageWithPk"], [chatInput.getTextWithPublicKeys()]) logs.logEvent("StatusChatInput::sendMessage", ["PlainText"], [SQUtils.StringUtils.plainText(chatInput.getTextWithPublicKeys())]) @@ -138,6 +141,23 @@ SplitView { QtObject { id: d + + readonly property var walletAssetsStore: WalletStores.WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: WalletStores.TokensStore { + plainTokensBySymbolModel: TokensBySymbolModel {} + } + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel + } + + readonly property SharedStores.RequestPaymentStore requestPaymentStore: SharedStores.RequestPaymentStore { + currencyStore: SharedStores.CurrenciesStore {} + flatNetworksModel: NetworksModel.flatNetworks + processedAssetsModel: d.walletAssetsStore.renamedTokensBySymbolModel + accountsModel: WalletAccountsModel {} + } + property bool linkPreviewsEnabled: linkPreviewSwitch.checked && !askToEnableLinkPreviewSwitch.checked onLinkPreviewsEnabledChanged: { loadLinkPreviews(chatInputLoader.item ? chatInputLoader.item.unformattedText : "") diff --git a/storybook/stubs/shared/stores/RequestPaymentStore.qml b/storybook/stubs/shared/stores/RequestPaymentStore.qml new file mode 100644 index 0000000000..d1a100d5af --- /dev/null +++ b/storybook/stubs/shared/stores/RequestPaymentStore.qml @@ -0,0 +1,8 @@ +import QtQuick 2.15 + +QtObject { + required property CurrenciesStore currencyStore + required property var flatNetworksModel + required property var processedAssetsModel + required property var accountsModel +} diff --git a/storybook/stubs/shared/stores/qmldir b/storybook/stubs/shared/stores/qmldir index d7a633428c..0a8d1cecc7 100644 --- a/storybook/stubs/shared/stores/qmldir +++ b/storybook/stubs/shared/stores/qmldir @@ -6,5 +6,6 @@ GifStore 1.0 GifStore.qml NetworkConnectionStore 1.0 NetworkConnectionStore.qml PermissionsStore 1.0 PermissionsStore.qml ProfileStore 1.0 ProfileStore.qml +RequestPaymentStore 1.0 RequestPaymentStore.qml RootStore 1.0 RootStore.qml UtilsStore 1.0 UtilsStore.qml diff --git a/ui/app/AppLayouts/Chat/popups/RequestPaymentModal.qml b/ui/app/AppLayouts/Chat/popups/RequestPaymentModal.qml new file mode 100644 index 0000000000..b6ed7a67d1 --- /dev/null +++ b/ui/app/AppLayouts/Chat/popups/RequestPaymentModal.qml @@ -0,0 +1,265 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.15 +import QtGraphicalEffects 1.15 + +import StatusQ 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils +import StatusQ.Components.private 0.1 as SQP +import StatusQ.Components 0.1 +import StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Wallet.controls 1.0 +import AppLayouts.Wallet.adaptors 1.0 + +import shared.popups.send.views 1.0 +import shared.controls 1.0 +import shared.stores 1.0 as SharedStores +import utils 1.0 + +StatusDialog { + id: root + + required property SharedStores.RequestPaymentStore store + + property int selectedNetworkChainId: Constants.chains.mainnetChainId + property string selectedAccountAddress + property string selectedTokenKey: Constants.ethToken + onSelectedTokenKeyChanged: Qt.callLater(d.reevaluateSelectedId) + + readonly property string amount: { + if (!d.isSelectedHoldingValidAsset || !d.selectedHolding.marketDetails || !d.selectedHolding.marketDetails.currencyPrice) { + return "0" + } + return amountToSendInput.text + } + + objectName: "requestPaymentModal" + + implicitWidth: 480 + implicitHeight: 470 + + modal: true + padding: 0 + backgroundColor: Theme.palette.statusModal.backgroundColor + + title: qsTr("Payment request") + + QtObject { + id: d + + // FIXME use ModelEntry + property var selectedHolding: SQUtils.ModelUtils.getByKey(holdingSelector.model, "tokensKey", root.selectedTokenKey) + readonly property bool isSelectedHoldingValidAsset: !!selectedHolding + + readonly property var adaptor: TokenSelectorViewAdaptor { + assetsModel: root.store.processedAssetsModel + flatNetworksModel: root.flatNetworksModel + currentCurrency: root.store.currencyStore.currentCurrency + showAllTokens: true + } + + // FIXME drop after using ModelEntry, shouldn't be needed + function reevaluateSelectedId() { + const entry = SQUtils.ModelUtils.getByKey(holdingSelector.model, "tokensKey", root.selectedTokenKey) + + if (entry) { + holdingSelector.setSelection(entry.symbol, entry.iconSource, entry.tokensKey) + } else { + root.selectedTokenKey = "" + holdingSelector.reset() + } + + d.selectedHolding = entry + } + } + + footer: StatusDialogFooter { + StatusDialogDivider { + anchors.top: parent.top + width: parent.width + } + rightButtons: ObjectModel { + StatusButton { + objectName: "sendButton" + text: qsTr("Add to message") + disabledColor: Theme.palette.directColor8 + enabled: amountToSendInput.valid && !amountToSendInput.empty && amountToSendInput.asNumber > 0 + interactive: true + onClicked: { + // TODO_ES handle + root.accept() + } + } + } + } + + ColumnLayout { + anchors.top: parent.top + anchors.topMargin: Theme.bigPadding + anchors.left: parent.left + anchors.leftMargin: Theme.padding + anchors.right: parent.right + anchors.rightMargin: Theme.padding + + spacing: Theme.padding + + AmountToSend { + id: amountToSendInput + Layout.fillWidth: true + + readonly property bool ready: valid && !empty + + readonly property string selectedSymbol: root.selectedTokenKey + + // For backward compatibility. To be removed when + // dependent components (NetworkSelector, AmountToReceive) + // are refactored. + readonly property double asNumber: { + if (!valid) + return 0 + + return parseFloat(text.replace(LocaleUtils.userInputLocale.decimalPoint, ".")) + } + readonly property int minSendCryptoDecimals: + !fiatMode ? LocaleUtils.fractionalPartLength(asNumber) : 0 + readonly property int minReceiveCryptoDecimals: + !fiatMode ? minSendCryptoDecimals + 1 : 0 + readonly property int minSendFiatDecimals: + fiatMode ? LocaleUtils.fractionalPartLength(asNumber) : 0 + readonly property int minReceiveFiatDecimals: + fiatMode ? minSendFiatDecimals + 1 : 0 + // End of to-be-removed part + + multiplierIndex: 9 + // !!holdingSelector.selectedItem + // && !!holdingSelector.selectedItem.decimals + // ? holdingSelector.selectedItem.decimals : 0 + + // price: d.isSelectedHoldingValidAsset + // ? (d.selectedHolding ? + // d.selectedHolding.marketDetails.currencyPrice.amount : 1) + // : 1 + price: 1 + + formatFiat: amount => root.store.currencyStore.formatCurrencyAmount( + amount, root.store.currencyStore.currentCurrency) + formatBalance: amount => root.store.currencyStore.formatCurrencyAmount( + amount, selectedSymbol) + + showSeparator: true + onValidChanged: { + + } + onAmountChanged: { + + } + + AssetSelector { + id: holdingSelector + + anchors.top: parent.top + anchors.right: parent.right + + model: d.adaptor.outputAssetsModel + onSelected: root.selectedTokenKey = key + } + } + + StatusBaseText { + text: qsTr("Into") + color: Theme.palette.directColor5 + font.weight: Font.Medium + } + + AccountSelector { + id: accountSelector + model: root.store.accountsModel + Layout.fillWidth: true + Layout.preferredHeight: 64 + + size: StatusComboBox.Size.Large + + control.background: SQP.StatusComboboxBackground { + active: accountSelector.control.down || accountSelector.control.hovered + } + + popup.verticalPadding: 0 + popup.width: accountSelector.width + control.contentItem: WalletAccountListItem { + readonly property var account: accountSelector.currentAccount + width: accountSelector.width + height: accountSelector.height + name: !!account ? account.name : "" + address: !!account ? account.address : "" + emoji: !!account ? account.emoji : "" + walletColor: !!account ? account.color : "" + + leftPadding: 0 + rightPadding: 0 + statusListItemTitle.customColor: Theme.palette.directColor1 + enabled: false + } + } + + StatusBaseText { + text: qsTr("On") + color: Theme.palette.directColor5 + font.weight: Font.Medium + } + + StatusComboBox { + id: networkSelector + objectName: "networkSelector" + Layout.fillWidth: true + Layout.preferredHeight: 64 + + readonly property ModelEntry singleSelectionItem: ModelEntry { + sourceModel: root.store.flatNetworksModel + key: "chainId" + value: root.selectedNetworkChainId ?? -1 + } + + model: root.store.flatNetworksModel + + control.background: SQP.StatusComboboxBackground { + active: networkSelector.control.down || networkSelector.control.hovered + } + + component NetworkDelegate: StatusListItem { + required property var network + width: parent.width + title: network.chainName + asset.height: 36 + asset.width: 36 + asset.isImage: true + asset.name: Theme.svg(network.iconUrl) + subTitle: qsTr("Only") + } + + control.contentItem: NetworkDelegate { + required property var model + network: networkSelector.singleSelectionItem.item + leftPadding: 0 + rightPadding: 0 + statusListItemTitle.customColor: Theme.palette.directColor1 + bgColor: "transparent" + enabled: false + } + + popup.verticalPadding: 0 + delegate: NetworkDelegate { + required property var model + network: model + onClicked: { + root.selectedNetworkChainId = model.chainId + networkSelector.popup.close() + } + } + } + } +} diff --git a/ui/app/AppLayouts/Chat/popups/qmldir b/ui/app/AppLayouts/Chat/popups/qmldir index 83c4f4d969..0cebda8ea7 100644 --- a/ui/app/AppLayouts/Chat/popups/qmldir +++ b/ui/app/AppLayouts/Chat/popups/qmldir @@ -1 +1,2 @@ PinnedMessagesPopup 1.0 PinnedMessagesPopup.qml +RequestPaymentModal 1.0 RequestPaymentModal.qml diff --git a/ui/imports/shared/popups/send/views/AmountToSend.qml b/ui/imports/shared/popups/send/views/AmountToSend.qml index 17a49cda78..0cd68fef73 100644 --- a/ui/imports/shared/popups/send/views/AmountToSend.qml +++ b/ui/imports/shared/popups/send/views/AmountToSend.qml @@ -11,6 +11,7 @@ import StatusQ.Validators 0.1 import utils 1.0 import shared.controls 1.0 +import shared.panels 1.0 Control { id: root @@ -73,6 +74,9 @@ Control { property var formatBalance: balance => `${balance.toLocaleString(Qt.locale())} CRYPTO` + /* Shows separator between top and bottom text */ + property bool showSeparator: false + /* Allows to set value to be displayed. The value is expected to be a not localized string like "1", "1.1" or "0.000000023400234222". Provided value will be formatted and displayed. Depending on the fiatMode flag @@ -217,6 +221,12 @@ Control { } } + Separator { + Layout.fillWidth: true + Layout.preferredHeight: 1 + visible: root.showSeparator + } + StatusBaseText { id: bottomItem diff --git a/ui/imports/shared/status/StatusChatInput.qml b/ui/imports/shared/status/StatusChatInput.qml index 735fbedb23..fa365cc993 100644 --- a/ui/imports/shared/status/StatusChatInput.qml +++ b/ui/imports/shared/status/StatusChatInput.qml @@ -15,11 +15,13 @@ import mainui 1.0 //TODO remove this dependency import AppLayouts.Chat.panels 1.0 +import AppLayouts.Chat.popups 1.0 import AppLayouts.Chat.stores 1.0 as ChatStores import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 import StatusQ.Core.Utils 0.1 as StatusQUtils import StatusQ.Components 0.1 import StatusQ.Controls 0.1 as StatusQ @@ -40,6 +42,7 @@ Rectangle { property var usersModel property SharedStores.RootStore sharedStore + property SharedStores.RequestPaymentStore requestPaymentStore property var emojiPopup: null property var stickersPopup: null @@ -154,6 +157,9 @@ Rectangle { property bool emojiPopupOpened: false property bool stickersPopupOpened: false + property var imageDialog: null + property var requestPaymentPopup: null + // common popups are emoji, jif and stickers // Put controlWidth as argument with default value for binding function getCommonPopupRelativePosition(popup, popupParent, controlWidth = control.width) { @@ -355,7 +361,7 @@ Rectangle { property var mentionsPos: [] function isUploadFilePressed(event) { - return (event.key === Qt.Key_U) && (event.modifiers & Qt.ControlModifier) && imageBtn.visible && !imageBtn.highlighted + return (event.key === Qt.Key_U) && (event.modifiers & Qt.ControlModifier) && !d.imageDialog } function checkTextInsert() { @@ -535,7 +541,7 @@ Rectangle { // ⌘⇧U if (isUploadFilePressed(event)) { - imageBtn.clicked(null) + openImageDialog() event.accepted = true } @@ -949,6 +955,16 @@ Rectangle { messageInputField.forceActiveFocus(); } + function openImageDialog() { + d.imageDialog = imageDialogComponent.createObject(control) + d.imageDialog.open() + } + + function openPaymentRequestPopup() { + d.requestPaymentPopup = requestPaymentPopupComponent.createObject(control) + d.requestPaymentPopup.open() + } + DropAreaPanel { enabled: control.visible && control.enabled parent: Overlay.overlay @@ -985,16 +1001,48 @@ Rectangle { qsTr("Image files (%1)").arg(UrlUtils.validImageNameFilters) ] onAccepted: { - imageBtn.highlighted = false validateImagesAndShowImageArea(fileUrls) messageInputField.forceActiveFocus() + destroy() } - onRejected: { - imageBtn.highlighted = false + onRejected: destroy() + Component.onDestruction: d.imageDialog = null + } + } + + Component { + id: requestPaymentPopupComponent + RequestPaymentModal { + store: control.requestPaymentStore + + onAccepted: { + control.requestPaymentStore.addPaymentRequest(selectedTokenKey, amount, selectedAccountAddress, selectedNetworkChainId) } } } + Component { + id: chatCommandMenuComponent + + StatusMenu { + StatusAction { + text: qsTr("Add image") + icon.name: "image" + onTriggered: control.openImageDialog() + } + + StatusAction { + text: qsTr("Add payment request") + icon.name: "wallet" + // TODO_ES disable for testnet (only production) + // TODO_ES error message when disabled on testnet (only production) + onTriggered: control.openPaymentRequestPopup() + } + + closeHandler: () => commandBtn.highlighted = false + } + } + StatusEmojiSuggestionPopup { id: emojiSuggestions messageInput: messageInput @@ -1068,18 +1116,19 @@ Rectangle { spacing: 4 StatusQ.StatusFlatRoundButton { - id: imageBtn + id: commandBtn Layout.preferredWidth: 32 Layout.preferredHeight: 32 Layout.alignment: Qt.AlignBottom Layout.bottomMargin: 4 - icon.name: "image" + icon.name: "chat-commands" type: StatusQ.StatusFlatRoundButton.Type.Tertiary visible: !isEdit onClicked: { highlighted = true - const popup = imageDialogComponent.createObject(control) - popup.open() + let menu = chatCommandMenuComponent.createObject(commandBtn) + menu.y = -menu.height // Show above button + menu.open() } } @@ -1224,6 +1273,7 @@ Rectangle { topPadding: 12 imagePreviewArray: control.fileUrlsAndSources linkPreviewModel: control.linkPreviewModel + requestPaymentModel: control.requestPaymentStore.requestPaymentModel showLinkPreviewSettings: control.askToEnableLinkPreview onImageRemoved: (index) => { //Just do a copy and replace the whole thing because it's a plain JS array and thre's no signal when a single item is removed @@ -1242,6 +1292,7 @@ Rectangle { onDisableLinkPreview: () => control.disableLinkPreview() onDismissLinkPreviewSettings: () => control.dismissLinkPreviewSettings() onDismissLinkPreview: (index) => control.dismissLinkPreview(index) + onPaymentRequestRemoved: (index) => control.requestPaymentStore.removePaymentRequest(index) } RowLayout { diff --git a/ui/imports/shared/stores/RequestPaymentStore.qml b/ui/imports/shared/stores/RequestPaymentStore.qml new file mode 100644 index 0000000000..d1a100d5af --- /dev/null +++ b/ui/imports/shared/stores/RequestPaymentStore.qml @@ -0,0 +1,8 @@ +import QtQuick 2.15 + +QtObject { + required property CurrenciesStore currencyStore + required property var flatNetworksModel + required property var processedAssetsModel + required property var accountsModel +} diff --git a/ui/imports/shared/stores/qmldir b/ui/imports/shared/stores/qmldir index c57acc564e..a3974cab0a 100644 --- a/ui/imports/shared/stores/qmldir +++ b/ui/imports/shared/stores/qmldir @@ -6,5 +6,6 @@ GifStore 1.0 GifStore.qml MetricsStore 1.0 MetricsStore.qml NetworkConnectionStore 1.0 NetworkConnectionStore.qml PermissionsStore 1.0 PermissionsStore.qml +RequestPaymentStore 1.0 RequestPaymentStore.qml RootStore 1.0 RootStore.qml UtilsStore 1.0 UtilsStore.qml