From f1e83f74bcb8f3aed846bd267ddfb78ce8340be3 Mon Sep 17 00:00:00 2001 From: Eric Mastro Date: Wed, 10 Mar 2021 15:59:01 +1100 Subject: [PATCH] feat: drag and drop images Allow up to 5 images to be dragged and dropped in to one-on-one chats and in the timeline. Can be combined with the existing upload button. The upload file dialog has been changed to allow multiple selections. Drag and dropped images adhere to the following rules, with corresponding validations messages: - Max 5 image - Image size must be 0.5 MB or less - File extension must be one of [".png", ".jpg", ".jpeg", ".heif", "tif", ".tiff"] Drag and drop and uploaded images are now also deduplicated. --- src/app/chat/view.nim | 18 ++ src/app/utilsView/view.nim | 11 ++ src/status/chat.nim | 4 + src/status/libstatus/chat.nim | 19 ++- ui/app/AppLayouts/Chat/ChatColumn.qml | 2 +- ui/app/AppLayouts/Timeline/TimelineLayout.qml | 6 +- ui/app/AppMain.qml | 1 + ui/imports/Constants.qml | 6 + ui/imports/Utils.qml | 10 +- ui/main.qml | 80 +++++++++ .../StatusChatImageExtensionValidator.qml | 23 +++ .../status/StatusChatImageQtyValidator.qml | 20 +++ .../status/StatusChatImageSizeValidator.qml | 23 +++ ui/shared/status/StatusChatImageValidator.qml | 73 ++++++++ ui/shared/status/StatusChatInput.qml | 83 +++++++-- ui/shared/status/StatusChatInputImageArea.qml | 159 +++++++++--------- 16 files changed, 444 insertions(+), 94 deletions(-) create mode 100644 ui/shared/status/StatusChatImageExtensionValidator.qml create mode 100644 ui/shared/status/StatusChatImageQtyValidator.qml create mode 100644 ui/shared/status/StatusChatImageSizeValidator.qml create mode 100644 ui/shared/status/StatusChatImageValidator.qml diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index 1ce49da58f..3894468112 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -224,6 +224,24 @@ QtObject: error "Error sending the image", msg = e.msg result = fmt"Error sending the image: {e.msg}" + proc sendImages*(self: ChatsView, imagePathsArray: string): string {.slot.} = + result = "" + try: + var images = Json.decode(imagePathsArray, seq[string]) + let channelId = self.activeChannel.id + + for imagePath in images.mitems: + var image = image_utils.formatImagePath(imagePath) + imagePath = image_resizer(image, 2000, TMPDIR) + + self.status.chat.sendImages(channelId, images) + + for imagePath in images.items: + removeFile(imagePath) + except Exception as e: + error "Error sending images", msg = e.msg + result = fmt"Error sending images: {e.msg}" + proc activeChannelChanged*(self: ChatsView) {.signal.} proc contextChannelChanged*(self: ChatsView) {.signal.} diff --git a/src/app/utilsView/view.nim b/src/app/utilsView/view.nim index 2368c60c77..5965727efc 100644 --- a/src/app/utilsView/view.nim +++ b/src/app/utilsView/view.nim @@ -10,6 +10,7 @@ import ../../status/libstatus/settings import ../../status/libstatus/wallet as status_wallet import ../../status/libstatus/utils as status_utils import ../../status/ens as status_ens +import ../utils/image_utils import web3/[ethtypes, conversions] import stew/byteutils @@ -99,3 +100,13 @@ QtObject: proc getNetworkName*(self: UtilsView): string {.slot.} = getCurrentNetworkDetails().name + + proc getFileSize*(self: UtilsView, filename: string): string {.slot.} = + var f: File = nil + if f.open(filename.formatImagePath): + try: + result = $(f.getFileSize()) + finally: + close(f) + else: + raise newException(IOError, "cannot open: " & filename) diff --git a/src/status/chat.nim b/src/status/chat.nim index 265bcae77d..5884d98180 100644 --- a/src/status/chat.nim +++ b/src/status/chat.nim @@ -247,6 +247,10 @@ proc sendImage*(self: ChatModel, chatId: string, image: string) = var response = status_chat.sendImageMessage(chatId, image) discard self.processMessageUpdateAfterSend(response) +proc sendImages*(self: ChatModel, chatId: string, images: var seq[string]) = + var response = status_chat.sendImageMessages(chatId, images) + discard self.processMessageUpdateAfterSend(response) + proc sendSticker*(self: ChatModel, chatId: string, sticker: Sticker) = var response = status_chat.sendStickerMessage(chatId, sticker) self.events.emit("stickerSent", StickerArgs(sticker: sticker, save: true)) diff --git a/src/status/libstatus/chat.nim b/src/status/libstatus/chat.nim index fff7d1157e..0ff51012c8 100644 --- a/src/status/libstatus/chat.nim +++ b/src/status/libstatus/chat.nim @@ -166,6 +166,22 @@ proc sendImageMessage*(chatId: string, image: string): string = } ]) +proc sendImageMessages*(chatId: string, images: var seq[string]): string = + let + preferredUsername = getSetting[string](Setting.PreferredUsername, "") + debugEcho ">>> [status/libstatus/chat.sendImageMessages] about to send images" + let imagesJson = %* images.map(image => %* + { + "chatId": chatId, + "contentType": ContentType.Image.int, + "imagePath": image, + "ensName": preferredUsername, + "text": "Update to latest version to see a nice image here!" + } + ) + debugEcho ">>> [status/libstatus/chat.sendImageMessages] imagesJson:", $imagesJson + callPrivateRPC("sendChatMessages".prefix, %* [imagesJson]) + proc sendStickerMessage*(chatId: string, sticker: Sticker): string = let preferredUsername = getSetting[string](Setting.PreferredUsername, "") callPrivateRPC("sendChatMessage".prefix, %* [ @@ -358,10 +374,9 @@ proc pendingRequestsToJoinForCommunity*(communityId: string): seq[CommunityMembe proc myPendingRequestsToJoin*(): seq[CommunityMembershipRequest] = let rpcResult = callPrivateRPC("myPendingRequestsToJoin".prefix).parseJSON() - var communityRequests: seq[CommunityMembershipRequest] = @[] - if rpcResult{"result"}.kind != JNull: + if rpcResult{"error"}.kind == JNull and rpcResult{"result"}.kind != JNull: for jsonCommunityReqest in rpcResult["result"]: communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest()) diff --git a/ui/app/AppLayouts/Chat/ChatColumn.qml b/ui/app/AppLayouts/Chat/ChatColumn.qml index 901a683180..ef0b50bd9e 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn.qml @@ -317,7 +317,7 @@ StackLayout { } onSendMessage: { if (chatInput.fileUrls.length > 0){ - chatsModel.sendImage(chatInput.fileUrls[0], false); + chatsModel.sendImages(JSON.stringify(fileUrls)); } let msg = chatsModel.plainText(Emoji.deparse(chatInput.textInput.text)) if (msg.length > 0){ diff --git a/ui/app/AppLayouts/Timeline/TimelineLayout.qml b/ui/app/AppLayouts/Timeline/TimelineLayout.qml index 6ec1210e2b..ad03065cb2 100644 --- a/ui/app/AppLayouts/Timeline/TimelineLayout.qml +++ b/ui/app/AppLayouts/Timeline/TimelineLayout.qml @@ -71,9 +71,13 @@ ScrollView { anchors.top: parent.top anchors.topMargin: 40 chatType: Constants.chatTypeStatusUpdate + imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Bottom + z: 1 onSendMessage: { if (statusUpdateInput.fileUrls.length > 0){ - chatsModel.sendImage(statusUpdateInput.fileUrls[0], true); + statusUpdateInput.fileUrls.forEach(url => { + chatsModel.sendImage(url, true); + }) } var msg = chatsModel.plainText(Emoji.deparse(statusUpdateInput.textInput.text)) if (msg.length > 0){ diff --git a/ui/app/AppMain.qml b/ui/app/AppMain.qml index 507e5a13cb..41371f67ae 100644 --- a/ui/app/AppMain.qml +++ b/ui/app/AppMain.qml @@ -15,6 +15,7 @@ import Qt.labs.settings 1.0 RowLayout { id: appMain + property int currentView: sLayout.currentIndex spacing: 0 Layout.fillHeight: true Layout.fillWidth: true diff --git a/ui/imports/Constants.qml b/ui/imports/Constants.qml index 80fcb8ee73..eb37bf7440 100644 --- a/ui/imports/Constants.qml +++ b/ui/imports/Constants.qml @@ -124,4 +124,10 @@ QtObject { readonly property string deepLinkPrefix: 'statusim://' readonly property string joinStatusLink: 'join.status.im' + + readonly property int maxUploadFiles: 5 + readonly property double maxUploadFilesizeMB: 0.5 + + readonly property var acceptedImageExtensions: [".png", ".jpg", ".jpeg", ".svg", ".gif"] + readonly property var acceptedDragNDropImageExtensions: [".png", ".jpg", ".jpeg", ".heif", "tif", ".tiff"] } diff --git a/ui/imports/Utils.qml b/ui/imports/Utils.qml index 56a03b7228..e49404839c 100644 --- a/ui/imports/Utils.qml +++ b/ui/imports/Utils.qml @@ -404,6 +404,14 @@ QtObject { } function hasImageExtension(url) { - return [".png", ".jpg", ".jpeg", ".svg", ".gif"].some(ext => url.includes(ext)) + return Constants.acceptedImageExtensions.some(ext => url.includes(ext)) + } + + function hasDragNDropImageExtension(url) { + return Constants.acceptedDragNDropImageExtensions.some(ext => url.includes(ext)) + } + + function deduplicate(array) { + return Array.from(new Set(array)) } } diff --git a/ui/main.qml b/ui/main.qml index 69b25f67b1..276ef511f8 100644 --- a/ui/main.qml +++ b/ui/main.qml @@ -18,6 +18,7 @@ import "./imports" ApplicationWindow { property bool hasAccounts: !!loginModel.rowCount() property bool removeMnemonicAfterLogin: false + property alias dragAndDrop: dragTarget Universal.theme: Universal.System @@ -247,6 +248,85 @@ ApplicationWindow { property var appSettings } + DropArea { + id: dragTarget + + signal droppedOnValidScreen(var drop) + property alias droppedUrls: rptDraggedPreviews.model + readonly property int chatView: Utils.getAppSectionIndex(Constants.chat) + readonly property int timelineView: Utils.getAppSectionIndex(Constants.timeline) + property bool enabled: containsDrag && loader.item && + ( + // in chat view + (loader.item.currentView === chatView && + ( + // in a one-to-one chat + chatsModel.activeChannel.chatType === Constants.chatTypeOneToOne || + // in a private group chat + chatsModel.activeChannel.chatType === Constants.chatTypePrivateGroupChat + ) + ) || + // in timeline view + loader.item.currentView === timelineView + ) + + width: applicationWindow.width + height: applicationWindow.height + + function cleanup() { + rptDraggedPreviews.model = [] + } + + onDropped: (drop) => { + if (enabled) { + droppedOnValidScreen(drop) + } + cleanup() + } + onEntered: { + // needed because drag.urls is not a normal js array + rptDraggedPreviews.model = drag.urls.filter(img => Utils.hasDragNDropImageExtension(img)) + } + onPositionChanged: { + rptDraggedPreviews.x = drag.x + rptDraggedPreviews.y = drag.y + } + onExited: cleanup() + Rectangle { + id: dropRectangle + + width: parent.width + height: parent.height + color: Style.current.transparent + opacity: 0.8 + + states: [ + State { + when: dragTarget.enabled + PropertyChanges { + target: dropRectangle + color: Style.current.background + } + } + ] + } + Repeater { + id: rptDraggedPreviews + + Image { + source: modelData + width: 80 + height: 80 + sourceSize.width: 160 + sourceSize.height: 160 + fillMode: Image.PreserveAspectFit + x: index * 10 + rptDraggedPreviews.x + y: index * 10 + rptDraggedPreviews.y + z: 1 + } + } + } + Component { id: app AppMain {} diff --git a/ui/shared/status/StatusChatImageExtensionValidator.qml b/ui/shared/status/StatusChatImageExtensionValidator.qml new file mode 100644 index 0000000000..510570411f --- /dev/null +++ b/ui/shared/status/StatusChatImageExtensionValidator.qml @@ -0,0 +1,23 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import QtGraphicalEffects 1.0 +import "../../imports" +import ".." + +StatusChatImageValidator { + id: root + + errorMessage: qsTr("Format not supported.") + secondaryErrorMessage: qsTr("Upload %1 only").arg(Constants.acceptedDragNDropImageExtensions.map(ext => ext.replace(".", "").toUpperCase() + "s").join(", ")) + + onImagesChanged: { + let isValid = true + root.validImages = images.filter(img => { + const isImage = Utils.hasDragNDropImageExtension(img) + isValid = isValid && isImage + return isImage + }) + root.isValid = isValid + } +} diff --git a/ui/shared/status/StatusChatImageQtyValidator.qml b/ui/shared/status/StatusChatImageQtyValidator.qml new file mode 100644 index 0000000000..2c4667f9e9 --- /dev/null +++ b/ui/shared/status/StatusChatImageQtyValidator.qml @@ -0,0 +1,20 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import QtGraphicalEffects 1.0 +import "../../imports" +import ".." + +StatusChatImageValidator { + id: root + errorMessage: qsTr("You can only upload %1 images at a time").arg(Constants.maxUploadFiles) + + onImagesChanged: { + let isValid = true + if (images.length > Constants.maxUploadFiles) { + isValid = false + } + root.isValid = isValid + root.validImages = images.slice(0, Constants.maxUploadFiles) + } +} diff --git a/ui/shared/status/StatusChatImageSizeValidator.qml b/ui/shared/status/StatusChatImageSizeValidator.qml new file mode 100644 index 0000000000..27a1f252f0 --- /dev/null +++ b/ui/shared/status/StatusChatImageSizeValidator.qml @@ -0,0 +1,23 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import QtGraphicalEffects 1.0 +import "../../imports" +import ".." + +StatusChatImageValidator { + id: root + readonly property int maxImgSizeBytes: Constants.maxUploadFilesizeMB * 1048576 /* 1 MB in bytes */ + + onImagesChanged: { + let isValid = true + root.validImages = images.filter(img => { + let size = parseInt(utilsModel.getFileSize(img)) + const isSmallEnough = size <= maxImgSizeBytes + isValid = isValid && isSmallEnough + return isSmallEnough + }) + root.isValid = isValid + } + errorMessage: qsTr("Max image size is %1 MB").arg(Constants.maxUploadFilesizeMB) +} \ No newline at end of file diff --git a/ui/shared/status/StatusChatImageValidator.qml b/ui/shared/status/StatusChatImageValidator.qml new file mode 100644 index 0000000000..e63d2b2db7 --- /dev/null +++ b/ui/shared/status/StatusChatImageValidator.qml @@ -0,0 +1,73 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import QtGraphicalEffects 1.0 +import "../../imports" +import ".." + +Item { + id: root + property bool isValid: true + property alias errorMessage: txtValidationError.text + property alias secondaryErrorMessage: txtValidationExtraInfo.text + property var images: [] + property var validImages: [] + + visible: !isValid + width: imgExclamation.width + txtValidationError.width + txtValidationExtraInfo.width + 24 + height: txtValidationError.height + 14 + + Rectangle { + anchors.fill: parent + color: Style.current.background + radius: Style.current.halfPadding + layer.enabled: true + layer.effect: DropShadow { + verticalOffset: 3 + radius: 8 + samples: 15 + fast: true + cached: true + color: "#22000000" + } + + SVGImage { + id: imgExclamation + width: 20 + height: 20 + sourceSize.height: height * 2 + sourceSize.width: width * 2 + verticalAlignment: Image.AlignVCenter + anchors.top: parent.top + anchors.left: parent.left + anchors.topMargin: 6 + anchors.leftMargin: 6 + fillMode: Image.PreserveAspectFit + source: "../../app/img/exclamation_outline.svg" + } + StyledText { + id: txtValidationError + verticalAlignment: Text.AlignVCenter + anchors.top: parent.top + anchors.left: imgExclamation.right + anchors.topMargin: 7 + anchors.leftMargin: 6 + wrapMode: Text.WordWrap + font.pixelSize: 13 + height: 18 + color: Style.current.danger + } + StyledText { + id: txtValidationExtraInfo + verticalAlignment: Text.AlignVCenter + anchors.top: parent.top + anchors.left: txtValidationError.right + anchors.topMargin: 7 + anchors.leftMargin: 6 + wrapMode: Text.WordWrap + font.pixelSize: 13 + height: 18 + color: Style.current.textColor + } + } +} diff --git a/ui/shared/status/StatusChatInput.qml b/ui/shared/status/StatusChatInput.qml index 27818b5aa8..dd25ae1889 100644 --- a/ui/shared/status/StatusChatInput.qml +++ b/ui/shared/status/StatusChatInput.qml @@ -44,6 +44,13 @@ Rectangle { property alias suggestionsList: suggestions property alias suggestions: suggestionsBox + property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top + + enum ImageErrorMessageLocation { + Top, + Bottom + } + height: { if (extendedArea.visible) { return messageInput.height + extendedArea.height + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding) @@ -120,6 +127,7 @@ Rectangle { if (messageInputField.length < messageLimit) { control.sendMessage(event) control.hideExtendedArea(); + event.accepted = true return; } if(event) event.accepted = true @@ -255,7 +263,7 @@ Rectangle { if (madeChanges) { messageInputField.remove(0, messageInputField.length); - insertInTextInput(0, Emoji.parse(words.join(' '))); + insertInTextInput(0, Emoji.parse(words.join(' '))); } } @@ -388,17 +396,33 @@ Rectangle { isImage = false; isReply = false; control.fileUrls = [] - imageArea.imageSource = ""; + imageArea.imageSource = []; replyArea.userName = "" replyArea.identicon = "" replyArea.message = "" + for (let i=0; i x.toString()) + let validImages = Utils.deduplicate(existing.concat(imagePaths)) + for (let i=0; i validator.validImages.includes(validImage)) + } + return validImages + } + + function showImageArea(imagePaths) { isImage = true; isReply = false; - control.fileUrls = imageDialog.fileUrls - imageArea.imageSource = control.fileUrls[0] + imageArea.imageSource = imagePaths + control.fileUrls = imageArea.imageSource } function showReplyArea(userName, message, identicon) { @@ -409,24 +433,37 @@ Rectangle { messageInputField.forceActiveFocus(); } + Connections { + target: applicationWindow.dragAndDrop + onDroppedOnValidScreen: (drop) => { + let validImages = validateImages(drop.urls) + if (validImages.length > 0) { + showImageArea(validImages) + drop.acceptProposedAction() + } + } + } + ListModel { id: suggestions } - FileDialog { id: imageDialog //% "Please choose an image" title: qsTrId("please-choose-an-image") folder: shortcuts.pictures + selectMultiple: true nameFilters: [ - //% "Image files (*.jpg *.jpeg *.png)" - qsTrId("image-files----jpg---jpeg---png-") + qsTr("Image files (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" ")) ] onAccepted: { imageBtn.highlighted = false imageBtn2.highlighted = false - control.showImageArea() + let validImages = validateImages(imageDialog.fileUrls) + if (validImages.length > 0) { + control.showImageArea(validImages) + } messageInputField.forceActiveFocus(); } onRejected: { @@ -582,6 +619,26 @@ Rectangle { radius: control.isStatusUpdateInput ? 36 : height > defaultInputFieldHeight + 1 || extendedArea.visible ? 16 : 32 + ColumnLayout { + id: validators + anchors.bottom: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? extendedArea.top : undefined + anchors.bottomMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? -4 : undefined + anchors.top: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? extendedArea.bottom : undefined + anchors.topMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? (isImage ? -4 : 4) : undefined + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + z: 1 + StatusChatImageExtensionValidator { + Layout.alignment: Qt.AlignHCenter + } + StatusChatImageSizeValidator { + Layout.alignment: Qt.AlignHCenter + } + StatusChatImageQtyValidator { + Layout.alignment: Qt.AlignHCenter + } + } + Rectangle { id: extendedArea visible: isImage || isReply @@ -625,9 +682,13 @@ Rectangle { anchors.top: parent.top anchors.topMargin: control.isStatusUpdateInput ? 0 : Style.current.halfPadding visible: isImage + width: messageInputField.width - actions.width onImageRemoved: { - control.fileUrls = [] - isImage = false + if (control.fileUrls.length > index && control.fileUrls[index]) { + control.fileUrls.splice(index, 1) + } + isImage = control.fileUrls.length > 0 + validateImages(control.fileUrls) } } diff --git a/ui/shared/status/StatusChatInputImageArea.qml b/ui/shared/status/StatusChatInputImageArea.qml index 46bffd9998..af2cad1dc0 100644 --- a/ui/shared/status/StatusChatInputImageArea.qml +++ b/ui/shared/status/StatusChatInputImageArea.qml @@ -4,90 +4,93 @@ import QtQuick.Controls 2.13 import "../../imports" import "../../shared" -Rectangle { +Row { id: imageArea - height: chatImage.height + spacing: Style.current.halfPadding - signal imageRemoved() - property url imageSource: "" - color: "transparent" - - Image { - id: chatImage - property bool hovered: false - height: 64 - anchors.left: parent.left - anchors.top: parent.top - fillMode: Image.PreserveAspectFit - mipmap: true - smooth: false - antialiasing: true - source: parent.imageSource - MouseArea { - cursorShape: Qt.PointingHandCursor - anchors.fill: parent - hoverEnabled: true - onEntered: { - chatImage.hovered = true - } - onExited: { - chatImage.hovered = false - } - } + signal imageRemoved(int index) + property alias imageSource: rptImages.model - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Item { - width: chatImage.width - height: chatImage.height - - Rectangle { - anchors.top: parent.top - anchors.left: parent.left - width: chatImage.width - height: chatImage.height - radius: 16 + Repeater { + id: rptImages + Item { + height: chatImage.paintedHeight + closeBtn.height - 5 + width: chatImage.width + Image { + id: chatImage + property bool hovered: false + width: 64 + height: 64 + fillMode: Image.PreserveAspectCrop + mipmap: true + smooth: false + antialiasing: true + source: modelData + MouseArea { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + hoverEnabled: true + onEntered: { + chatImage.hovered = true + } + onExited: { + chatImage.hovered = false + } } - Rectangle { - anchors.bottom: parent.bottom - anchors.right: parent.right - width: 32 - height: 32 - radius: 4 + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: chatImage.width + height: chatImage.height + + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + width: chatImage.width + height: chatImage.height + radius: 16 + } + Rectangle { + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 32 + height: 32 + radius: 4 + } + } + } + } + RoundButton { + id: closeBtn + implicitWidth: 24 + implicitHeight: 24 + padding: 0 + anchors.top: chatImage.top + anchors.topMargin: -5 + anchors.right: chatImage.right + anchors.rightMargin: -Style.current.halfPadding + visible: chatImage.hovered || hovered + contentItem: SVGImage { + source: !closeBtn.hovered ? + "../../app/img/close-filled.svg" : "../../app/img/close-filled-hovered.svg" + width: closeBtn.width + height: closeBtn.height + } + background: Rectangle { + color: "transparent" + } + onClicked: { + imageArea.imageRemoved(index) + const tmp = imageArea.imageSource.filter((url, idx) => idx !== index) + rptImages.model = tmp + } + MouseArea { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + onPressed: mouse.accepted = false } } } } - - RoundButton { - id: closeBtn - implicitWidth: 24 - implicitHeight: 24 - padding: 0 - anchors.top: chatImage.top - anchors.topMargin: -5 - anchors.right: chatImage.right - anchors.rightMargin: -Style.current.halfPadding - visible: chatImage.hovered || hovered - contentItem: SVGImage { - source: !closeBtn.hovered ? - "../../app/img/close-filled.svg" : "../../app/img/close-filled-hovered.svg" - width: closeBtn.width - height: closeBtn.height - } - background: Rectangle { - color: "transparent" - } - onClicked: { - imageArea.imageRemoved() - imageArea.imageSource = "" - } - MouseArea { - cursorShape: Qt.PointingHandCursor - anchors.fill: parent - onPressed: mouse.accepted = false - } - } - - }