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 {