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 0000000000..9714b5a1c5 Binary files /dev/null and b/ui/StatusQ/src/assets/png/chat/request_payment_banner.png differ diff --git a/ui/imports/shared/controls/chat/RequestPaymentCardDelegate.qml b/ui/imports/shared/controls/chat/RequestPaymentCardDelegate.qml new file mode 100644 index 0000000000..665d154b8b --- /dev/null +++ b/ui/imports/shared/controls/chat/RequestPaymentCardDelegate.qml @@ -0,0 +1,133 @@ +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 utils 1.0 + +CalloutCard { + id: root + + required property string amount + required property string symbol + required property string address + + property string senderName + property var senderImageAssetSettings + + 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 + property real bannerImageMargins: 1 / Screen.devicePixelRatio // image size isn't pixel perfect.. + property bool highlight: root.highlight || root.hovered + property string bannerImageSource: "" + } + + 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: root.senderImageAssetSettings + 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) + } + } + + MouseArea { + anchors.fill: root + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: root.clicked(mouse) + } +} diff --git a/ui/imports/shared/controls/chat/qmldir b/ui/imports/shared/controls/chat/qmldir index 60106adacf..04772acb80 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 +RequestPaymentCardDelegate 1.0 RequestPaymentCardDelegate.qml RequestPaymentMiniCardDelegate 1.0 RequestPaymentMiniCardDelegate.qml MessageMouseArea 1.0 MessageMouseArea.qml MessageReactionsRow 1.0 MessageReactionsRow.qml diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml index 48b138c869..624d61769c 100644 --- a/ui/imports/shared/views/chat/MessageView.qml +++ b/ui/imports/shared/views/chat/MessageView.qml @@ -350,6 +350,8 @@ Loader { return StatusMessage.ContentType.SystemMessageMutualEventAccepted; case Constants.messageContentType.systemMessageMutualEventRemoved: return StatusMessage.ContentType.SystemMessageMutualEventRemoved; + case Constants.messageContentType.attachmentType: + return StatusMessage.ContentType.Attachment; case Constants.messageContentType.fetchMoreMessagesButton: case Constants.messageContentType.chatIdentifier: case Constants.messageContentType.unknownContentType: diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 9f04a462b2..0db6827699 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -476,6 +476,7 @@ QtObject { readonly property int systemMessageMutualEventAccepted: 16 readonly property int systemMessageMutualEventRemoved: 17 readonly property int bridgeMessageType: 18 + readonly property int attachmentType: 19 } readonly property QtObject messageModelRoles: QtObject {