From df348629c41d7c1fc4c1d089e99346e1c9ee9fa5 Mon Sep 17 00:00:00 2001 From: Emil Sawicki Date: Fri, 8 Nov 2024 16:50:35 +0100 Subject: [PATCH] wip: chat request payment card --- storybook/pages/StatusMessagePage.qml | 32 +++++ .../qmlTests/tests/tst_StatusMessage.qml | 67 ++++++++- .../src/StatusQ/Components/StatusMessage.qml | 72 +++++++++- .../Components/StatusMessageDetails.qml | 1 + .../statusMessage/StatusMessageImageAlbum.qml | 1 + ui/StatusQ/src/assets.qrc | 1 + .../png/chat/request_payment_banner.png | Bin 0 -> 3596 bytes .../chat/RequestPaymentCardDelegate.qml | 133 ++++++++++++++++++ ui/imports/shared/controls/chat/qmldir | 1 + ui/imports/shared/views/chat/MessageView.qml | 2 + ui/imports/utils/Constants.qml | 1 + 11 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 ui/StatusQ/src/assets/png/chat/request_payment_banner.png create mode 100644 ui/imports/shared/controls/chat/RequestPaymentCardDelegate.qml diff --git a/storybook/pages/StatusMessagePage.qml b/storybook/pages/StatusMessagePage.qml index 8481b6cd65..cb84cb9ac5 100644 --- a/storybook/pages/StatusMessagePage.qml +++ b/storybook/pages/StatusMessagePage.qml @@ -17,6 +17,9 @@ SplitView { QtObject { id: d + readonly property var exampleAlbum: [ModelsData.banners.coinbase, ModelsData.icons.status] + readonly property var requestPaymentModel: RequestPaymentModel {} + readonly property var messagesModel: ListModel { ListElement { timestamp: 1656937930123 @@ -152,6 +155,28 @@ 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 + } + ListElement { + timestamp: 1667937830123 + senderId: "zq123456790" + senderDisplayName: "Alice" + contentType: StatusMessage.ContentType.Attachment + message: "This message contains attachments" + isContact: true + isAReply: false + trustIndicator: StatusContactVerificationIcons.TrustedType.None + outgoingStatus: StatusMessage.OutgoingStatus.Delivered + } } readonly property var colorHash: ListModel { ListElement { colorId: 13; segmentLength: 5 } @@ -202,6 +227,11 @@ SplitView { colorId: index colorHash: d.colorHash } + album: model.contentType === StatusMessage.ContentType.Image + || model.contentType === StatusMessage.ContentType.Attachment ? d.exampleAlbum : [] + albumCount: model.contentType === StatusMessage.ContentType.Image + || model.contentType === StatusMessage.ContentType.Attachment ? d.exampleAlbum.length : 0 + requestPaymentModel: model.contentType === StatusMessage.ContentType.Attachment ? d.requestPaymentModel : null } replyDetails { @@ -222,6 +252,8 @@ SplitView { onReplyMessageClicked: logs.logEvent("StatusMessage::replyMessageClicked") onResendClicked: logs.logEvent("StatusMessage::resendClicked") onLinkActivated: logs.logEvent("StatusMessage::linkActivated" + link) + onRequestPaymentClicked: logs.logEvent("StatusMessage::requestPaymentActivated") + onImageClicked: logs.logEvent("StatusMessage::imageClicked") } } } diff --git a/storybook/qmlTests/tests/tst_StatusMessage.qml b/storybook/qmlTests/tests/tst_StatusMessage.qml index af66388891..1f7063320a 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() { @@ -86,5 +86,70 @@ Item { compare(actualLinkCount, data.validAddressEnsCount, "TextEdit should contain a link %1".arg(data.messageText)) } + + function test_attachment_empty() { + verify(!!controlUnderTest) + controlUnderTest.messageDetails.contentType = StatusMessage.ContentType.Attachment + controlUnderTest.messageDetails.messageText = "" + waitForRendering(controlUnderTest) + + const statusTextMessage = findChild(controlUnderTest, "StatusMessage_textMessage") + verify(!statusTextMessage) + const imageAlbum = findChild(controlUnderTest, "StatusMessage_imageAlbum") + verify(!!imageAlbum) + compare(imageAlbum.albumCount, 0) + const image = findChild(imageAlbum, "album_image_loader_0") + verify(!image) + const requestPaymentItem = findChild(controlUnderTest, "StatusMessage_requestPaymentDelegate_0") + verify(!requestPaymentItem) + } + + function test_attachment_only_text() { + verify(!!controlUnderTest) + controlUnderTest.messageDetails.contentType = StatusMessage.ContentType.Attachment + controlUnderTest.messageDetails.messageText = "test message" + waitForRendering(controlUnderTest) + + const statusTextMessage = findChild(controlUnderTest, "StatusMessage_textMessage") + verify(!!statusTextMessage) + verify(statusTextMessage.textField.text.indexOf("test message") > 0) + const imageAlbum = findChild(controlUnderTest, "StatusMessage_imageAlbum") + verify(!!imageAlbum) + compare(imageAlbum.albumCount, 0) + const image = findChild(imageAlbum, "album_image_loader_0") + verify(!image) + const requestPaymentItem = findChild(controlUnderTest, "StatusMessage_requestPaymentDelegate_0") + verify(!requestPaymentItem) + } + + function test_attachment_multiple_attachments() { + verify(!!controlUnderTest) + controlUnderTest.messageDetails.contentType = StatusMessage.ContentType.Attachment + controlUnderTest.messageDetails.messageText = "test message with attachments" + controlUnderTest.messageDetails.album = [ "image0", "image1", "image2" ] + controlUnderTest.messageDetails.albumCount = 3 + controlUnderTest.messageDetails.requestPaymentModel = [ + {amount: "0.1", currency: "ETH", address: "0x1234567890abcdef1234567890abcdef12345678", chainId: 1}, + {amount: "0.2", currency: "DAI", address: "0xAbCdEf1234567890abcdef1234567890AbCdEf12", chainId: 10}, + ] + waitForRendering(controlUnderTest) + + const statusTextMessage = findChild(controlUnderTest, "StatusMessage_textMessage") + verify(!!statusTextMessage) + verify(statusTextMessage.textField.text.indexOf("test message with attachments") > 0) + const imageAlbum = findChild(controlUnderTest, "StatusMessage_imageAlbum") + verify(!!imageAlbum) + compare(imageAlbum.albumCount, 3) + for (let i = 0 ; i < 3 ; i++) { + const image = findChild(imageAlbum, "album_image_loader_"+i) + verify(!!image) + } + const requestPaymentItem0 = findChild(controlUnderTest, "StatusMessage_requestPaymentDelegate_0") + verify(!!requestPaymentItem0) + const requestPaymentItem1 = findChild(controlUnderTest, "StatusMessage_requestPaymentDelegate_1") + verify(!!requestPaymentItem1) + const requestPaymentItem2 = findChild(controlUnderTest, "StatusMessage_requestPaymentDelegate_2") + verify(!requestPaymentItem2) + } } } diff --git a/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml b/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml index 77d9650bc1..e8695c8c96 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml @@ -7,6 +7,8 @@ import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 import StatusQ.Controls 0.1 +import shared.controls.chat 1.0 + import "./private/statusMessage" Control { @@ -26,7 +28,8 @@ Control { SystemMessageMutualEventSent = 15, SystemMessageMutualEventAccepted = 16, SystemMessageMutualEventRemoved = 17, - BridgeMessage = 18 + BridgeMessage = 18, + Attachment = 19 } enum OutgoingStatus { @@ -89,6 +92,7 @@ Control { signal addReactionClicked(var sender, var mouse) signal toggleReactionClicked(int emojiId) signal imageClicked(var image, var mouse, var imageSource) + signal requestPaymentClicked(var symbol, var amount, var address, var chainId) signal stickerClicked() signal resendClicked() @@ -299,7 +303,7 @@ Control { } } Loader { - active: root.messageDetails.contentType === StatusMessage.ContentType.Image && !editMode + active: root.messageDetails.contentType === StatusMessage.ContentType.Image && !editMode visible: active Layout.fillWidth: true @@ -312,6 +316,7 @@ Control { anchors.right: parent.right visible: active sourceComponent: StatusTextMessage { + objectName: "StatusMessage_textMessage" messageDetails: root.messageDetails isEdited: root.isEdited allowShowMore: !root.isInPinnedPopup @@ -326,6 +331,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 @@ -337,6 +343,68 @@ Control { } } + Loader { + active: root.messageDetails.contentType === StatusMessage.ContentType.Attachment && !editMode + visible: active + Layout.fillWidth: true + + sourceComponent: Column { + id: attachmentsColumn + spacing: 8 + Loader { + active: root.messageDetails.messageText !== "" + anchors.left: parent.left + anchors.right: parent.right + visible: active + sourceComponent: StatusTextMessage { + objectName: "StatusMessage_textMessage" + messageDetails: root.messageDetails + isEdited: root.isEdited + allowShowMore: !root.isInPinnedPopup + textField.anchors.rightMargin: root.isInPinnedPopup ? Theme.xlPadding : 0 // margin for the "Unpin" floating button + highlightedLink: root.highlightedLink + onLinkActivated: { + root.linkActivated(link); + } + } + } + + Flow { + width: messageLayout.width + height: childrenRect.height + Theme.smallPadding + Loader { + active: true + sourceComponent: StatusMessageImageAlbum { + objectName: "StatusMessage_imageAlbum" + album: root.messageDetails.albumCount > 0 ? root.messageDetails.album : [root.messageDetails.messageContent] + albumCount: root.messageDetails.albumCount > 0 ? root.messageDetails.albumCount : 0 + imageWidth: Math.min(messageLayout.width / root.messageDetails.albumCount - 9 * (root.messageDetails.albumCount - 1), 144) + shapeType: StatusImageMessage.ShapeType.LEFT_ROUNDED + onImageClicked: root.imageClicked(image, mouse, imageSource) + } + } + Loader { + active: true + sourceComponent: RowLayout { + Repeater { + model: root.messageDetails.requestPaymentModel + delegate: RequestPaymentCardDelegate { + objectName: "StatusMessage_requestPaymentDelegate_" + model.index + required property var model + amount: model.amount + symbol: model.symbol + address: model.address + senderName: root.messageDetails.sender.displayName + senderImageAssetSettings: root.messageDetails.sender.profileImage.assetSettings + onClicked: root.requestPaymentClicked(model.symbol, model.amount, model.address, model.chainId) + } + } + } + } + } + } + } + Loader { active: root.messageAttachments && !editMode visible: active diff --git a/ui/StatusQ/src/StatusQ/Components/StatusMessageDetails.qml b/ui/StatusQ/src/StatusQ/Components/StatusMessageDetails.qml index 939b93acd3..a880864fd2 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusMessageDetails.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusMessageDetails.qml @@ -15,4 +15,5 @@ QtObject { property bool messageDeleted: false property var album: [] property int albumCount: 0 + property var requestPaymentModel: null } 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 8f7d43516f..9a7f4ea985 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 0000000000000000000000000000000000000000..9714b5a1c50c740a40ad3134503d34f823d356c5 GIT binary patch literal 3596 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