diff --git a/storybook/pages/ChatInputLinksPreviewAreaPage.qml b/storybook/pages/ChatInputLinksPreviewAreaPage.qml index adde059d23..7c9adbba35 100644 --- a/storybook/pages/ChatInputLinksPreviewAreaPage.qml +++ b/storybook/pages/ChatInputLinksPreviewAreaPage.qml @@ -32,6 +32,7 @@ SplitView { width: parent.width imagePreviewArray: ["https://picsum.photos/200/300?random=1", "https://picsum.photos/200/300?random=1"] linkPreviewModel: showLinkPreviewSettings ? emptyModel : mockedLinkPreviewModel + paymentRequestModel: mockedPaymentRequestModel showLinkPreviewSettings: !linkPreviewEnabledSwitch.checked visible: hasContent @@ -82,6 +83,10 @@ SplitView { LinkPreviewModel { id: mockedLinkPreviewModel } + + PaymentRequestModel { + id: mockedPaymentRequestModel + } } // category: Panels diff --git a/storybook/pages/LinksMessageViewPage.qml b/storybook/pages/LinksMessageViewPage.qml index cf5c540548..d2585a1706 100644 --- a/storybook/pages/LinksMessageViewPage.qml +++ b/storybook/pages/LinksMessageViewPage.qml @@ -13,6 +13,10 @@ SplitView { id: mockedLinkPreviewModel } + PaymentRequestModel { + id: mockedPaymentRequestModel + } + Pane { id: messageViewWrapper SplitView.fillWidth: true @@ -27,6 +31,10 @@ SplitView { playAnimations: true linkPreviewModel: mockedLinkPreviewModel gifLinks: [ "https://media.tenor.com/qN_ytiwLh24AAAAC/cold.gif" ] + paymentRequestModel: mockedPaymentRequestModel + areTestNetworksEnabled: false + + senderName: "Alice" gifUnfurlingEnabled: false canAskToUnfurlGifs: true @@ -70,6 +78,11 @@ SplitView { checked: linksMessageView.isOnline onToggled: linksMessageView.isOnline = !linksMessageView.isOnline } + CheckBox { + text: qsTr("Testnet enabled") + checked: linksMessageView.areTestNetworksEnabled + onToggled: linksMessageView.areTestNetworksEnabled = !linksMessageView.areTestNetworksEnabled + } } } } diff --git a/storybook/pages/StatusMessagePage.qml b/storybook/pages/StatusMessagePage.qml index 8481b6cd65..ddf8e45234 100644 --- a/storybook/pages/StatusMessagePage.qml +++ b/storybook/pages/StatusMessagePage.qml @@ -17,6 +17,8 @@ SplitView { QtObject { id: d + readonly property var exampleAlbum: [ModelsData.banners.coinbase, ModelsData.icons.status] + readonly property var messagesModel: ListModel { ListElement { timestamp: 1656937930123 @@ -152,6 +154,17 @@ SplitView { trustIndicator: StatusContactVerificationIcons.TrustedType.None outgoingStatus: StatusMessage.OutgoingStatus.Delivered } + ListElement { + timestamp: 1667937830123 + senderId: "zq123456790" + senderDisplayName: "Alice" + contentType: StatusMessage.ContentType.Image + message: "This message contains images" + isContact: true + isAReply: false + trustIndicator: StatusContactVerificationIcons.TrustedType.None + outgoingStatus: StatusMessage.OutgoingStatus.Delivered + } } readonly property var colorHash: ListModel { ListElement { colorId: 13; segmentLength: 5 } @@ -202,6 +215,8 @@ SplitView { colorId: index colorHash: d.colorHash } + album: model.contentType === StatusMessage.ContentType.Image ? d.exampleAlbum : [] + albumCount: model.contentType === StatusMessage.ContentType.Image ? d.exampleAlbum.length : 0 } replyDetails { @@ -222,6 +237,7 @@ SplitView { onReplyMessageClicked: logs.logEvent("StatusMessage::replyMessageClicked") onResendClicked: logs.logEvent("StatusMessage::resendClicked") onLinkActivated: logs.logEvent("StatusMessage::linkActivated" + link) + onImageClicked: logs.logEvent("StatusMessage::imageClicked") } } } diff --git a/storybook/qmlTests/tests/tst_StatusMessage.qml b/storybook/qmlTests/tests/tst_StatusMessage.qml index af66388891..09c388b447 100644 --- a/storybook/qmlTests/tests/tst_StatusMessage.qml +++ b/storybook/qmlTests/tests/tst_StatusMessage.qml @@ -35,7 +35,7 @@ Item { property StatusMessage controlUnderTest: null TestCase { - name: "TokenSelectorView" + name: "StatusMessage" when: windowShown function init() { diff --git a/storybook/src/Models/PaymentRequestModel.qml b/storybook/src/Models/PaymentRequestModel.qml new file mode 100644 index 0000000000..2e6128ae19 --- /dev/null +++ b/storybook/src/Models/PaymentRequestModel.qml @@ -0,0 +1,18 @@ +import QtQuick 2.15 + +ListModel { + id: root + + ListElement { + symbol: "WBTC" + amount: "0.00017" + address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" + chainId: 1 // main + } + ListElement { + symbol: "ETH" + amount: "12345.6789" + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881" + chainId: 10 // Opti + } +} diff --git a/storybook/src/Models/qmldir b/storybook/src/Models/qmldir index 04d623fc7e..d72574bb7b 100644 --- a/storybook/src/Models/qmldir +++ b/storybook/src/Models/qmldir @@ -23,6 +23,7 @@ TokensBySymbolModel 1.0 TokensBySymbolModel.qml CommunitiesModel 1.0 CommunitiesModel.qml OnRampProvidersModel 1.0 OnRampProvidersModel.qml SwapTransactionRoutes 1.0 SwapTransactionRoutes.qml +PaymentRequestModel 1.0 PaymentRequestModel.qml singleton ModelsData 1.0 ModelsData.qml singleton NetworksModel 1.0 NetworksModel.qml diff --git a/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml b/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml index 77d9650bc1..3258579d69 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml @@ -49,6 +49,7 @@ Control { property string messageAttachments: "" property var reactionIcons: [] property var linkPreviewModel + property var paymentRequestModel property var gifLinks property string messageId: "" @@ -312,6 +313,7 @@ Control { anchors.right: parent.right visible: active sourceComponent: StatusTextMessage { + objectName: "StatusMessage_textMessage" messageDetails: root.messageDetails isEdited: root.isEdited allowShowMore: !root.isInPinnedPopup @@ -326,6 +328,7 @@ Control { Loader { active: true sourceComponent: StatusMessageImageAlbum { + objectName: "StatusMessage_imageAlbum" width: messageLayout.width album: root.messageDetails.albumCount > 0 ? root.messageDetails.album : [root.messageDetails.messageContent] albumCount: root.messageDetails.albumCount > 0 ? root.messageDetails.albumCount : 1 @@ -375,7 +378,8 @@ Control { Layout.preferredHeight: implicitHeight active: !root.editMode && ((!!root.linkPreviewModel && root.linkPreviewModel.count > 0) - || (!!root.gifLinks && root.gifLinks.length > 0)) + || (!!root.gifLinks && root.gifLinks.length > 0) + || (!!root.paymentRequestModel && root.paymentRequestModel.ModelCount.count > 0)) visible: active } Loader { diff --git a/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusMessageImageAlbum.qml b/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusMessageImageAlbum.qml index 24bdb9728e..a6159859fa 100644 --- a/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusMessageImageAlbum.qml +++ b/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusMessageImageAlbum.qml @@ -24,6 +24,7 @@ RowLayout { delegate: Loader { active: true + objectName: "album_image_loader_" + index readonly property bool imageLoaded: index < root.album.length readonly property string imagePath: imageLoaded ? root.album[index] : "" sourceComponent: imageLoaded ? imageComponent : imagePlaceholderComponent diff --git a/ui/StatusQ/src/assets.qrc b/ui/StatusQ/src/assets.qrc index eb1dc51287..5b4a8438d7 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -8043,6 +8043,7 @@ assets/png/chat/chat@2x.png assets/png/chat/chat@3x.png assets/png/chat/wave.png + assets/png/chat/request_payment_banner.png assets/png/keycard/authenticate.png assets/png/keycard/biometrics-fail.png assets/png/keycard/biometrics-success.png diff --git a/ui/StatusQ/src/assets/png/chat/request_payment_banner.png b/ui/StatusQ/src/assets/png/chat/request_payment_banner.png new file mode 100644 index 0000000000..9714b5a1c5 Binary files /dev/null and b/ui/StatusQ/src/assets/png/chat/request_payment_banner.png differ diff --git a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml index ff76d33239..9f9c7394b5 100644 --- a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml @@ -200,6 +200,14 @@ Item { // Call later to make sure activeUsersStore and activeMessagesStore bindings are updated Qt.callLater(d.restoreInputState, preservedText) } + + function formatBalance(amount, symbol) { + let asset = ModelUtils.getByKey(WalletStore.RootStore.tokensStore.flatTokensModel, "symbol", symbol) + if (!asset) + return "0" + const num = AmountsArithmetic.toNumber(amount, asset.decimals) + return root.rootStore.currencyStore.formatCurrencyAmount(num, symbol, {noSynbol: true}) + } } EmptyChatPanel { @@ -238,11 +246,13 @@ Item { utilsStore: root.utilsStore rootStore: root.rootStore contactsStore: root.contactsStore + formatBalance: d.formatBalance emojiPopup: root.emojiPopup stickersPopup: root.stickersPopup stickersLoaded: root.stickersLoaded isBlocked: model.blocked sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled + areTestNetworksEnabled: root.areTestNetworksEnabled onOpenStickerPackPopup: { root.openStickerPackPopup(stickerPackId) } @@ -294,6 +304,8 @@ Item { sharedStore: root.sharedRootStore linkPreviewModel: !!d.activeChatContentModule ? d.activeChatContentModule.inputAreaModule.linkPreviewModel : null + paymentRequestModel: !!d.activeChatContentModule ? d.activeChatContentModule.inputAreaModule.paymentRequestModel : null + formatBalance: d.formatBalance urlsList: d.urlsList askToEnableLinkPreview: { if(!d.activeChatContentModule || !d.activeChatContentModule.inputAreaModule || !d.activeChatContentModule.inputAreaModule.preservedProperties) @@ -391,6 +403,7 @@ Item { d.activeChatContentModule.inputAreaModule.setLinkPreviewEnabledForCurrentMessage(false) } onDismissLinkPreview: (index) => d.activeChatContentModule.inputAreaModule.removeLinkPreviewData(index) + onRemovePaymentRequestPreview: (index) => d.activeChatContentModule.inputAreaModule.removePaymentRequestPreviewData(index) } ChatPermissionQualificationPanel { diff --git a/ui/app/AppLayouts/Chat/views/ChatContentView.qml b/ui/app/AppLayouts/Chat/views/ChatContentView.qml index b638956c81..cfd60e6305 100644 --- a/ui/app/AppLayouts/Chat/views/ChatContentView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatContentView.qml @@ -38,8 +38,10 @@ ColumnLayout { property ContactsStore contactsStore property string chatId property int chatType: Constants.chatType.unknown + property var formatBalance readonly property alias chatMessagesLoader: chatMessagesLoader + property bool areTestNetworksEnabled property var emojiPopup property var stickersPopup @@ -94,6 +96,7 @@ ColumnLayout { rootStore: root.rootStore contactsStore: root.contactsStore messageStore: root.messageStore + formatBalance: root.formatBalance emojiPopup: root.emojiPopup stickersPopup: root.stickersPopup usersStore: root.usersStore @@ -103,6 +106,7 @@ ColumnLayout { isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "") sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled + areTestNetworksEnabled: root.areTestNetworksEnabled onShowReplyArea: (messageId, senderId) => { root.showReplyArea(messageId) } diff --git a/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml b/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml index 575ed4edab..2f0e5a164b 100644 --- a/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml @@ -39,9 +39,11 @@ Item { property UsersStore usersStore property ContactsStore contactsStore property string channelEmoji + property var formatBalance property var emojiPopup property var stickersPopup + property bool areTestNetworksEnabled property string chatId: "" property bool stickersLoaded: false @@ -283,10 +285,12 @@ Item { stickersPopup: root.stickersPopup chatLogView: ListView.view chatContentModule: root.chatContentModule + formatBalance: root.formatBalance isChatBlocked: root.isChatBlocked sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled + areTestNetworksEnabled: root.areTestNetworksEnabled chatId: root.chatId messageId: model.id @@ -326,6 +330,7 @@ Item { deletedByContactColorHash: model.deletedByContactColorHash linkPreviewModel: model.linkPreviewModel links: model.links + paymentRequestModel: model.paymentRequestModel messageAttachments: model.messageAttachments transactionParams: model.transactionParameters hasMention: model.mentioned diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 7461975633..9dcbfb088e 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -708,6 +708,21 @@ Item { sendModal.open(address) } + function onPaymentRequestClicked(receiverAddress: string, symbol: string, amount: string, chainId: int) { + if (!!symbol) { + sendModal.preSelectedHoldingID = symbol + sendModal.preSelectedHoldingType = Constants.TokenType.ERC20 + } + if (!!amount) { + sendModal.preDefinedRawAmountToSend = amount + } + if (!!chainId) { + sendModal.preSelectedChainId = chainId + } + + sendModal.open(receiverAddress) + } + function onSwitchToCommunity(communityId: string) { appMain.communitiesStore.setActiveCommunity(communityId) } @@ -1875,6 +1890,7 @@ Item { property int preSelectedHoldingType: Constants.TokenType.Unknown property int preSelectedSendType: Constants.SendType.Unknown property string preDefinedAmountToSend + property string preDefinedRawAmountToSend property int preSelectedChainId: 0 property bool onlyAssets: false @@ -1903,6 +1919,7 @@ Item { sendModal.preSelectedAccountAddress = "" sendModal.preSelectedRecipient = undefined sendModal.preDefinedAmountToSend = "" + sendModal.preDefinedRawAmountToSend = "" sendModal.preSelectedChainId = 0 sendModal.stickersPackId = "" @@ -1929,6 +1946,9 @@ Item { if (sendModal.preDefinedAmountToSend != "") { item.preDefinedAmountToSend = sendModal.preDefinedAmountToSend } + if (sendModal.preDefinedRawAmountToSend != "") { + item.preDefinedRawAmountToSend = sendModal.preDefinedRawAmountToSend + } if (!!sendModal.preSelectedChainId) { item.preSelectedChainId = sendModal.preSelectedChainId } diff --git a/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml b/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml index 9da7811cd7..8192552dba 100644 --- a/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml +++ b/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml @@ -32,15 +32,25 @@ Control { */ required property var linkPreviewModel required property bool showLinkPreviewSettings + /* + Expected roles: + string symbol + string amount + */ + required property var paymentRequestModel + + property var formatBalance: null readonly property alias hoveredUrl: d.hoveredUrl - readonly property bool hasContent: imagePreviewArray.length > 0 || showLinkPreviewSettings || linkPreviewRepeater.count > 0 + readonly property bool hasContent: imagePreviewArray.length > 0 || showLinkPreviewSettings || linkPreviewRepeater.count > 0 || paymentRequestRepeater.count > 0 signal imageRemoved(int index) signal imageClicked(var chatImage) signal linkReload(string link) signal linkClicked(string link) + signal removePaymentRequestPreview(int index) + signal enableLinkPreview() signal enableLinkPreviewForThisMessage() signal disableLinkPreview() @@ -96,6 +106,21 @@ Control { onImageRemoved: root.imageRemoved(index) visible: !!imagePreviewArray && imagePreviewArray.length > 0 } + Repeater { + id: paymentRequestRepeater + model: root.paymentRequestModel + delegate: PaymentRequestMiniCardDelegate { + required property var model + + amount: { + if (!root.formatBalance) + return model.amount + return root.formatBalance(model.amount, model.symbol) + } + symbol: model.symbol + onClose: root.removePaymentRequestPreview(model.index) + } + } Repeater { id: linkPreviewRepeater model: d.filteredModel diff --git a/ui/imports/shared/controls/chat/PaymentRequestMiniCardDelegate.qml b/ui/imports/shared/controls/chat/PaymentRequestMiniCardDelegate.qml new file mode 100644 index 0000000000..e226e85bf5 --- /dev/null +++ b/ui/imports/shared/controls/chat/PaymentRequestMiniCardDelegate.qml @@ -0,0 +1,112 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 + +import utils 1.0 + +CalloutCard { + id: root + + required property string amount + required property string symbol + + readonly property bool containsMouse: mouseArea.hovered || closeButton.hovered + + signal close() + + implicitWidth:260 + implicitHeight: 64 + verticalPadding: 15 + horizontalPadding: 12 + borderColor: Theme.palette.directColor7 + backgroundColor: root.containsMouse ? Theme.palette.directColor7 : Theme.palette.background + + contentItem: Item { + implicitHeight: layout.implicitHeight + implicitWidth: layout.implicitWidth + + RowLayout { + id: layout + anchors.fill: parent + spacing: 16 + + StatusRoundIcon { + id: favIcon + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 + asset.width: 24 + asset.height: 24 + asset.bgColor: Theme.palette.directColor7 + asset.bgHeight: 36 + asset.bgWidth: 36 + asset.color: Theme.palette.primaryColor1 + asset.name: Theme.svg("send") + + StatusSmartIdenticon { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.right + asset.width: 16 + asset.height: 16 + asset.bgColor: root.containsMouse ? Theme.palette.transparent : Theme.palette.background + asset.bgHeight: 20 + asset.bgWidth: 20 + asset.isImage: true + asset.name: Constants.tokenIcon(root.symbol) + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + StatusBaseText { + Layout.fillWidth: true + Layout.fillHeight: true + text: qsTr("Payment request") + font.pixelSize: Theme.additionalTextSize + font.weight: Font.Medium + } + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + StatusBaseText { + Layout.maximumWidth: parent.width * 0.8 + Layout.fillHeight: true + font.pixelSize: Theme.tertiaryTextFontSize + color: Theme.palette.baseColor1 + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: root.amount + } + StatusBaseText { + Layout.fillHeight: true + font.pixelSize: Theme.tertiaryTextFontSize + color: Theme.palette.baseColor1 + verticalAlignment: Text.AlignVCenter + text: root.symbol + } + } + } + + StatusFlatButton { + id: closeButton + icon.name: "close" + size: StatusBaseButton.Size.Small + hoverColor: Theme.palette.directColor8 + textColor: Theme.palette.directColor1 + onClicked: root.close() + } + } + } + + HoverHandler { + id: mouseArea + target: background + cursorShape: Qt.PointingHandCursor + } +} diff --git a/ui/imports/shared/controls/chat/qmldir b/ui/imports/shared/controls/chat/qmldir index 3acd3d2689..b422a4d1ac 100644 --- a/ui/imports/shared/controls/chat/qmldir +++ b/ui/imports/shared/controls/chat/qmldir @@ -10,6 +10,7 @@ LinkPreviewCard 1.0 LinkPreviewCard.qml LinkPreviewMiniCard 1.0 LinkPreviewMiniCard.qml LinkPreviewSettingsCard 1.0 LinkPreviewSettingsCard.qml LinkPreviewSettingsCardMenu 1.0 LinkPreviewSettingsCardMenu.qml +PaymentRequestMiniCardDelegate 1.0 PaymentRequestMiniCardDelegate.qml MessageMouseArea 1.0 MessageMouseArea.qml MessageReactionsRow 1.0 MessageReactionsRow.qml ProfileHeader 1.0 ProfileHeader.qml diff --git a/ui/imports/shared/controls/delegates/PaymentRequestCardDelegate.qml b/ui/imports/shared/controls/delegates/PaymentRequestCardDelegate.qml new file mode 100644 index 0000000000..c5a507e754 --- /dev/null +++ b/ui/imports/shared/controls/delegates/PaymentRequestCardDelegate.qml @@ -0,0 +1,155 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 +import StatusQ.Core 0.1 + +import QtGraphicalEffects 1.15 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 + +import shared.controls.chat 1.0 + +import utils 1.0 + +CalloutCard { + id: root + + required property string amount + required property string symbol + required property string address + + required property bool areTestNetworksEnabled + + property string senderName + property string senderThumbnailImage + property int senderColorId + + property bool highlight: false + + signal clicked(var mouse) + + implicitHeight: 187 + implicitWidth: 305 + 2 * borderWidth + borderWidth: 2 + hoverEnabled: true + dropShadow: d.highlight + borderColor: d.highlight ? Theme.palette.background : Theme.palette.border + + padding: 12 + + Behavior on borderColor { + ColorAnimation { duration: 200 } + } + + QtObject { + id: d + readonly property bool highlight: (root.highlight || root.hovered) && isAvailable + readonly property bool isAvailable: !root.areTestNetworksEnabled + } + + contentItem: ColumnLayout { + spacing: 4 + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true + radius: 8 + color: Theme.palette.primaryColor3 + clip: true + border.width: 1 + border.color: Theme.palette.primaryColor2 + + StatusImage { + anchors.fill: parent + asynchronous: true + source: Theme.png("chat/request_payment_banner") + } + + Row { + id: iconRow + spacing: -8 + anchors.centerIn: parent + StatusRoundedImage { + id: symbolImage + anchors.verticalCenter: parent.verticalCenter + image.source: Constants.tokenIcon(root.symbol) + width: 44 + height: width + image.layer.enabled: true + image.layer.effect: OpacityMask { + id: mask + invert: true + + maskSource: Item { + width: mask.width + 2 + height: mask.height + 2 + + Rectangle { + anchors.centerIn: parent + anchors.horizontalCenterOffset: symbolImage.width + iconRow.spacing - 2 + + width: parent.width + height: width + radius: width / 2 + } + } + } + } + + StatusSmartIdenticon { + width: symbolImage.width + height: symbolImage.height + asset.width: symbolImage.width + asset.height: symbolImage.height + asset.isImage: !!root.senderThumbnailImage + asset.name: root.senderThumbnailImage + asset.isLetterIdenticon: root.senderThumbnailImage === "" + asset.color: Theme.palette.userCustomizationColors[root.senderColorId] + asset.charactersLen: 2 + name: root.senderName + } + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 4 + } + + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: qsTr("Send %1 %2 to %3").arg(root.amount).arg(root.symbol).arg(Utils.compactAddress(root.address.toLowerCase(), 4)) + font.pixelSize: Theme.additionalTextSize + font.weight: Font.Medium + } + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + font.pixelSize: Theme.tertiaryTextFontSize + color: Theme.palette.baseColor1 + verticalAlignment: Text.AlignVCenter + text: qsTr("Requested by %1").arg(root.senderName) + } + } + + StatusToolTip { + text: qsTr("Not available in the testnet mode") + visible: !d.isAvailable && root.hovered + y: -height + } + + MouseArea { + id: ma + anchors.fill: root + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if (!d.isAvailable) + return + root.clicked(mouse) + } + } +} diff --git a/ui/imports/shared/controls/delegates/qmldir b/ui/imports/shared/controls/delegates/qmldir index 45e2a69f02..8e630eb00e 100644 --- a/ui/imports/shared/controls/delegates/qmldir +++ b/ui/imports/shared/controls/delegates/qmldir @@ -2,4 +2,5 @@ ContactListItemDelegate 1.0 ContactListItemDelegate.qml LinkPreviewCardDelegate 1.0 LinkPreviewCardDelegate.qml LinkPreviewGifDelegate 1.0 LinkPreviewGifDelegate.qml LinkPreviewMiniCardDelegate 1.0 LinkPreviewMiniCardDelegate.qml +PaymentRequestCardDelegate 1.0 PaymentRequestCardDelegate.qml InfoCard 1.0 InfoCard.qml diff --git a/ui/imports/shared/popups/send/SendModal.qml b/ui/imports/shared/popups/send/SendModal.qml index d2981d7714..46973a7315 100644 --- a/ui/imports/shared/popups/send/SendModal.qml +++ b/ui/imports/shared/popups/send/SendModal.qml @@ -41,6 +41,7 @@ StatusDialog { property alias preSelectedRecipientType: recipientInputLoader.selectedRecipientType property string preDefinedAmountToSend + property string preDefinedRawAmountToSend property int preSelectedChainId: 0 property string stickersPackId @@ -99,8 +100,8 @@ StatusDialog { return Constants.NoError } - readonly property double maxFiatBalance: isSelectedHoldingValidAsset ? selectedHolding.currencyBalance : 0 - readonly property double maxCryptoBalance: isSelectedHoldingValidAsset ? selectedHolding.currentBalance : 0 + readonly property double maxFiatBalance: isSelectedHoldingValidAsset && !!selectedHolding.currencyBalance ? selectedHolding.currencyBalance : 0 + readonly property double maxCryptoBalance: isSelectedHoldingValidAsset && !!selectedHolding.currencyBalance ? selectedHolding.currentBalance : 0 readonly property double maxInputBalance: amountToSend.fiatMode ? maxFiatBalance : maxCryptoBalance readonly property string tokenSymbol: !!d.selectedHolding && !!d.selectedHolding.symbol ? d.selectedHolding.symbol: "" @@ -234,13 +235,24 @@ StatusDialog { if (popup.preSelectedHoldingType === Constants.TokenType.Native || popup.preSelectedHoldingType === Constants.TokenType.ERC20) { - const entry = SQUtils.ModelUtils.getByKey( + let iconSource = "" + let entry = SQUtils.ModelUtils.getByKey( assetsAdaptor.outputAssetsModel, "tokensKey", popup.preSelectedHoldingID) + + if (entry) { + iconSource = entry.iconSource + } else { + entry = SQUtils.ModelUtils.getByKey( + popup.store.walletAssetStore.renamedTokensBySymbolModel, "tokensKey", + popup.preSelectedHoldingID) + iconSource = Constants.tokenIcon(entry.symbol) + } + d.selectedHoldingType = Constants.TokenType.ERC20 d.selectedHolding = entry - holdingSelector.setSelection(entry.symbol, entry.iconSource, + holdingSelector.setSelection(entry.symbol, iconSource, popup.preSelectedHoldingID) holdingSelector.selectedItem = entry } else { @@ -260,6 +272,7 @@ StatusDialog { holdingSelector.currentTab = TokenSelectorPanel.Tabs.Collectibles } } + if(!!popup.preDefinedAmountToSend) { // TODO: At this stage the number should not be localized. However // in many places when initializing popup the number is provided @@ -267,9 +280,12 @@ StatusDialog { // number consistently. Only the displaying component should apply // final localized formatting. const delocalized = popup.preDefinedAmountToSend.replace(LocaleUtils.userInputLocale.decimalPoint, ".") - amountToSend.setValue(delocalized) } + + if (!!popup.preDefinedRawAmountToSend) { + amountToSend.setRawValue(popup.preDefinedRawAmountToSend) + } if (!!popup.preSelectedChainId) { popup.preDefinedAmountToSend = popup.preDefinedAmountToSend.replace(Qt.locale().decimalPoint, '.') diff --git a/ui/imports/shared/popups/send/views/AmountToSend.qml b/ui/imports/shared/popups/send/views/AmountToSend.qml index b1b67b6c53..5c62aab902 100644 --- a/ui/imports/shared/popups/send/views/AmountToSend.qml +++ b/ui/imports/shared/popups/send/views/AmountToSend.qml @@ -107,6 +107,22 @@ Control { textField.text = d.localize(trimmed) } + function setRawValue(valueString) { + if (!valueString) + valueString = "0" + + if (d.fiatMode) { + setValue(valueString) + return + } + + const divisor = SQUtils.AmountsArithmetic.fromExponent(root.multiplierIndex) + const stringNumber = SQUtils.AmountsArithmetic.div(SQUtils.AmountsArithmetic.fromString(valueString), divisor).toFixed(root.multiplierIndex) + const trimmed = d.removeDecimalTrailingZeros(stringNumber) + + textField.text = d.localize(trimmed) + } + function clear() { textField.clear() } diff --git a/ui/imports/shared/status/StatusChatInput.qml b/ui/imports/shared/status/StatusChatInput.qml index 735fbedb23..e2b05dfa43 100644 --- a/ui/imports/shared/status/StatusChatInput.qml +++ b/ui/imports/shared/status/StatusChatInput.qml @@ -37,6 +37,7 @@ Rectangle { signal disableLinkPreview() signal dismissLinkPreviewSettings() signal dismissLinkPreview(int index) + signal removePaymentRequestPreview(int index) property var usersModel property SharedStores.RootStore sharedStore @@ -67,6 +68,9 @@ Rectangle { property var fileUrlsAndSources: [] property var linkPreviewModel: null + property var paymentRequestModel: null + + property var formatBalance: null property var urlsList: [] @@ -1224,6 +1228,8 @@ Rectangle { topPadding: 12 imagePreviewArray: control.fileUrlsAndSources linkPreviewModel: control.linkPreviewModel + paymentRequestModel: control.paymentRequestModel + formatBalance: control.formatBalance 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 +1248,7 @@ Rectangle { onDisableLinkPreview: () => control.disableLinkPreview() onDismissLinkPreviewSettings: () => control.dismissLinkPreviewSettings() onDismissLinkPreview: (index) => control.dismissLinkPreview(index) + onRemovePaymentRequestPreview: (index) => control.removePaymentRequestPreview(index) } RowLayout { diff --git a/ui/imports/shared/views/chat/LinksMessageView.qml b/ui/imports/shared/views/chat/LinksMessageView.qml index 8d7787d4b9..e623ad9ce5 100644 --- a/ui/imports/shared/views/chat/LinksMessageView.qml +++ b/ui/imports/shared/views/chat/LinksMessageView.qml @@ -6,6 +6,7 @@ import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 import StatusQ.Components 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils import shared.controls 1.0 import shared.panels 1.0 @@ -25,15 +26,26 @@ Flow { required property var linkPreviewModel required property var gifLinks + required property var paymentRequestModel + required property bool gifUnfurlingEnabled required property bool canAskToUnfurlGifs + required property bool areTestNetworksEnabled + + property var formatBalance: null + + property string senderName + property string senderThumbnailImage + property int senderColorId + readonly property alias hoveredLink: linksRepeater.hoveredUrl property string highlightLink: "" signal imageClicked(var image, var mouse, string imageSource, string url) signal openContextMenu(var item, string url, string domain) signal setNeverAskAboutUnfurlingAgain(bool neverAskAgain) + signal paymentRequestClicked(int index) function resetLocalAskAboutUnfurling() { d.localAskAboutUnfurling = true @@ -56,9 +68,30 @@ Flow { sourceComponent: enableLinkComponent } + Repeater { + id: paymentRequestRepeater + model: root.paymentRequestModel + delegate: PaymentRequestCardDelegate { + required property var model + objectName: "PaymentRequestDelegate_" + model.index + areTestNetworksEnabled: root.areTestNetworksEnabled + amount: { + if (!root.formatBalance) + return model.amount + return root.formatBalance(model.amount, model.symbol) + } + symbol: model.symbol + address: model.receiver + senderName: root.senderName + senderThumbnailImage: root.senderThumbnailImage + senderColorId: root.senderColorId + onClicked: root.paymentRequestClicked(model.index) + } + } + Repeater { id: tempRepeater - visible: root.canAskToUnfurlGifs + visible: root.cankToUnfurlGifs model: root.gifUnfurlingEnabled ? gifLinks : [] delegate: LinkPreviewGifDelegate { diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml index 5632709078..be295f677d 100644 --- a/ui/imports/shared/views/chat/MessageView.qml +++ b/ui/imports/shared/views/chat/MessageView.qml @@ -70,9 +70,11 @@ Loader { property string messagePinnedBy: "" property var reactionsModel: [] property var linkPreviewModel + property var paymentRequestModel property string messageAttachments: "" property var transactionParams property var emojiReactionsModel + property var formatBalance // These 2 properties can be dropped when the new unfurling flow supports GIFs property var links @@ -137,6 +139,8 @@ Loader { property bool sendViaPersonalChatEnabled + property bool areTestNetworksEnabled + property bool stickersLoaded: false property string sticker property int stickerPack: -1 @@ -720,6 +724,7 @@ Loader { resendError: root.resendError reactionsModel: root.reactionsModel linkPreviewModel: root.linkPreviewModel + paymentRequestModel: root.paymentRequestModel gifLinks: root.gifLinks showHeader: root.shouldRepeatHeader || dateGroupLabel.visible || isAReply || @@ -973,9 +978,15 @@ Loader { linkPreviewModel: root.linkPreviewModel gifLinks: root.gifLinks + senderName: root.senderDisplayName + senderThumbnailImage: root.senderIcon || "" + senderColorId: Utils.colorIdForPubkey(root.senderId) + paymentRequestModel: root.paymentRequestModel playAnimations: root.Window.active && root.messageStore.isChatActive isOnline: root.rootStore.mainModuleInst.isOnline highlightLink: delegate.hoveredLink + areTestNetworksEnabled: root.areTestNetworksEnabled + formatBalance: root.formatBalance onImageClicked: (image, mouse, imageSource, url) => { d.onImageClicked(image, mouse, imageSource, url) } @@ -986,6 +997,10 @@ Loader { gifUnfurlingEnabled: root.sharedRootStore.gifUnfurlingEnabled canAskToUnfurlGifs: !root.sharedRootStore.neverAskAboutUnfurlingAgain onSetNeverAskAboutUnfurlingAgain: root.sharedRootStore.setNeverAskAboutUnfurlingAgain(neverAskAgain) + onPaymentRequestClicked: (index) => { + const request = StatusQUtils.ModelUtils.get(paymentRequestModel, index) + Global.paymentRequestClicked(request.receiver, request.symbol, request.amount, request.chainId) + } Component.onCompleted: { root.messageStore.messageModule.forceLinkPreviewsLocalData(root.messageId) diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index fe3d7e6722..ad82200e97 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -70,6 +70,7 @@ QtObject { signal appSectionBySectionTypeChanged(int sectionType, int subsection, int subSubsection, var data) signal openSendModal(string address) + signal paymentRequestClicked(string receiverAddress, string symbol, string amount, int chainId) signal switchToCommunity(string communityId) signal switchToCommunitySettings(string communityId) signal switchToCommunityChannelsView(string communityId)