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.
This commit is contained in:
parent
de290727c2
commit
f1e83f74bc
|
@ -224,6 +224,24 @@ QtObject:
|
||||||
error "Error sending the image", msg = e.msg
|
error "Error sending the image", msg = e.msg
|
||||||
result = fmt"Error sending the image: {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 activeChannelChanged*(self: ChatsView) {.signal.}
|
||||||
|
|
||||||
proc contextChannelChanged*(self: ChatsView) {.signal.}
|
proc contextChannelChanged*(self: ChatsView) {.signal.}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import ../../status/libstatus/settings
|
||||||
import ../../status/libstatus/wallet as status_wallet
|
import ../../status/libstatus/wallet as status_wallet
|
||||||
import ../../status/libstatus/utils as status_utils
|
import ../../status/libstatus/utils as status_utils
|
||||||
import ../../status/ens as status_ens
|
import ../../status/ens as status_ens
|
||||||
|
import ../utils/image_utils
|
||||||
import web3/[ethtypes, conversions]
|
import web3/[ethtypes, conversions]
|
||||||
import stew/byteutils
|
import stew/byteutils
|
||||||
|
|
||||||
|
@ -99,3 +100,13 @@ QtObject:
|
||||||
|
|
||||||
proc getNetworkName*(self: UtilsView): string {.slot.} =
|
proc getNetworkName*(self: UtilsView): string {.slot.} =
|
||||||
getCurrentNetworkDetails().name
|
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)
|
||||||
|
|
|
@ -247,6 +247,10 @@ proc sendImage*(self: ChatModel, chatId: string, image: string) =
|
||||||
var response = status_chat.sendImageMessage(chatId, image)
|
var response = status_chat.sendImageMessage(chatId, image)
|
||||||
discard self.processMessageUpdateAfterSend(response)
|
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) =
|
proc sendSticker*(self: ChatModel, chatId: string, sticker: Sticker) =
|
||||||
var response = status_chat.sendStickerMessage(chatId, sticker)
|
var response = status_chat.sendStickerMessage(chatId, sticker)
|
||||||
self.events.emit("stickerSent", StickerArgs(sticker: sticker, save: true))
|
self.events.emit("stickerSent", StickerArgs(sticker: sticker, save: true))
|
||||||
|
|
|
@ -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 =
|
proc sendStickerMessage*(chatId: string, sticker: Sticker): string =
|
||||||
let preferredUsername = getSetting[string](Setting.PreferredUsername, "")
|
let preferredUsername = getSetting[string](Setting.PreferredUsername, "")
|
||||||
callPrivateRPC("sendChatMessage".prefix, %* [
|
callPrivateRPC("sendChatMessage".prefix, %* [
|
||||||
|
@ -358,10 +374,9 @@ proc pendingRequestsToJoinForCommunity*(communityId: string): seq[CommunityMembe
|
||||||
|
|
||||||
proc myPendingRequestsToJoin*(): seq[CommunityMembershipRequest] =
|
proc myPendingRequestsToJoin*(): seq[CommunityMembershipRequest] =
|
||||||
let rpcResult = callPrivateRPC("myPendingRequestsToJoin".prefix).parseJSON()
|
let rpcResult = callPrivateRPC("myPendingRequestsToJoin".prefix).parseJSON()
|
||||||
|
|
||||||
var communityRequests: seq[CommunityMembershipRequest] = @[]
|
var communityRequests: seq[CommunityMembershipRequest] = @[]
|
||||||
|
|
||||||
if rpcResult{"result"}.kind != JNull:
|
if rpcResult{"error"}.kind == JNull and rpcResult{"result"}.kind != JNull:
|
||||||
for jsonCommunityReqest in rpcResult["result"]:
|
for jsonCommunityReqest in rpcResult["result"]:
|
||||||
communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest())
|
communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest())
|
||||||
|
|
||||||
|
|
|
@ -317,7 +317,7 @@ StackLayout {
|
||||||
}
|
}
|
||||||
onSendMessage: {
|
onSendMessage: {
|
||||||
if (chatInput.fileUrls.length > 0){
|
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))
|
let msg = chatsModel.plainText(Emoji.deparse(chatInput.textInput.text))
|
||||||
if (msg.length > 0){
|
if (msg.length > 0){
|
||||||
|
|
|
@ -71,9 +71,13 @@ ScrollView {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: 40
|
anchors.topMargin: 40
|
||||||
chatType: Constants.chatTypeStatusUpdate
|
chatType: Constants.chatTypeStatusUpdate
|
||||||
|
imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Bottom
|
||||||
|
z: 1
|
||||||
onSendMessage: {
|
onSendMessage: {
|
||||||
if (statusUpdateInput.fileUrls.length > 0){
|
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))
|
var msg = chatsModel.plainText(Emoji.deparse(statusUpdateInput.textInput.text))
|
||||||
if (msg.length > 0){
|
if (msg.length > 0){
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Qt.labs.settings 1.0
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: appMain
|
id: appMain
|
||||||
|
property int currentView: sLayout.currentIndex
|
||||||
spacing: 0
|
spacing: 0
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
@ -124,4 +124,10 @@ QtObject {
|
||||||
|
|
||||||
readonly property string deepLinkPrefix: 'statusim://'
|
readonly property string deepLinkPrefix: 'statusim://'
|
||||||
readonly property string joinStatusLink: 'join.status.im'
|
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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -404,6 +404,14 @@ QtObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasImageExtension(url) {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
80
ui/main.qml
80
ui/main.qml
|
@ -18,6 +18,7 @@ import "./imports"
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
property bool hasAccounts: !!loginModel.rowCount()
|
property bool hasAccounts: !!loginModel.rowCount()
|
||||||
property bool removeMnemonicAfterLogin: false
|
property bool removeMnemonicAfterLogin: false
|
||||||
|
property alias dragAndDrop: dragTarget
|
||||||
|
|
||||||
Universal.theme: Universal.System
|
Universal.theme: Universal.System
|
||||||
|
|
||||||
|
@ -247,6 +248,85 @@ ApplicationWindow {
|
||||||
property var appSettings
|
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 {
|
Component {
|
||||||
id: app
|
id: app
|
||||||
AppMain {}
|
AppMain {}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,13 @@ Rectangle {
|
||||||
property alias suggestionsList: suggestions
|
property alias suggestionsList: suggestions
|
||||||
property alias suggestions: suggestionsBox
|
property alias suggestions: suggestionsBox
|
||||||
|
|
||||||
|
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top
|
||||||
|
|
||||||
|
enum ImageErrorMessageLocation {
|
||||||
|
Top,
|
||||||
|
Bottom
|
||||||
|
}
|
||||||
|
|
||||||
height: {
|
height: {
|
||||||
if (extendedArea.visible) {
|
if (extendedArea.visible) {
|
||||||
return messageInput.height + extendedArea.height + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding)
|
return messageInput.height + extendedArea.height + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding)
|
||||||
|
@ -120,6 +127,7 @@ Rectangle {
|
||||||
if (messageInputField.length < messageLimit) {
|
if (messageInputField.length < messageLimit) {
|
||||||
control.sendMessage(event)
|
control.sendMessage(event)
|
||||||
control.hideExtendedArea();
|
control.hideExtendedArea();
|
||||||
|
event.accepted = true
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(event) event.accepted = true
|
if(event) event.accepted = true
|
||||||
|
@ -255,7 +263,7 @@ Rectangle {
|
||||||
|
|
||||||
if (madeChanges) {
|
if (madeChanges) {
|
||||||
messageInputField.remove(0, messageInputField.length);
|
messageInputField.remove(0, messageInputField.length);
|
||||||
insertInTextInput(0, Emoji.parse(words.join(' ')));
|
insertInTextInput(0, Emoji.parse(words.join(' ')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -388,17 +396,33 @@ Rectangle {
|
||||||
isImage = false;
|
isImage = false;
|
||||||
isReply = false;
|
isReply = false;
|
||||||
control.fileUrls = []
|
control.fileUrls = []
|
||||||
imageArea.imageSource = "";
|
imageArea.imageSource = [];
|
||||||
replyArea.userName = ""
|
replyArea.userName = ""
|
||||||
replyArea.identicon = ""
|
replyArea.identicon = ""
|
||||||
replyArea.message = ""
|
replyArea.message = ""
|
||||||
|
for (let i=0; i<validators.children.length; i++) {
|
||||||
|
const validator = validators.children[i]
|
||||||
|
validator.images = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showImageArea(imagePath) {
|
function validateImages(imagePaths) {
|
||||||
|
// needed because imageArea.imageSource is not a normal js array
|
||||||
|
const existing = (imageArea.imageSource || []).map(x => x.toString())
|
||||||
|
let validImages = Utils.deduplicate(existing.concat(imagePaths))
|
||||||
|
for (let i=0; i<validators.children.length; i++) {
|
||||||
|
const validator = validators.children[i]
|
||||||
|
validator.images = validImages
|
||||||
|
validImages = validImages.filter(validImage => validator.validImages.includes(validImage))
|
||||||
|
}
|
||||||
|
return validImages
|
||||||
|
}
|
||||||
|
|
||||||
|
function showImageArea(imagePaths) {
|
||||||
isImage = true;
|
isImage = true;
|
||||||
isReply = false;
|
isReply = false;
|
||||||
control.fileUrls = imageDialog.fileUrls
|
imageArea.imageSource = imagePaths
|
||||||
imageArea.imageSource = control.fileUrls[0]
|
control.fileUrls = imageArea.imageSource
|
||||||
}
|
}
|
||||||
|
|
||||||
function showReplyArea(userName, message, identicon) {
|
function showReplyArea(userName, message, identicon) {
|
||||||
|
@ -409,24 +433,37 @@ Rectangle {
|
||||||
messageInputField.forceActiveFocus();
|
messageInputField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: applicationWindow.dragAndDrop
|
||||||
|
onDroppedOnValidScreen: (drop) => {
|
||||||
|
let validImages = validateImages(drop.urls)
|
||||||
|
if (validImages.length > 0) {
|
||||||
|
showImageArea(validImages)
|
||||||
|
drop.acceptProposedAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ListModel {
|
ListModel {
|
||||||
id: suggestions
|
id: suggestions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
FileDialog {
|
FileDialog {
|
||||||
id: imageDialog
|
id: imageDialog
|
||||||
//% "Please choose an image"
|
//% "Please choose an image"
|
||||||
title: qsTrId("please-choose-an-image")
|
title: qsTrId("please-choose-an-image")
|
||||||
folder: shortcuts.pictures
|
folder: shortcuts.pictures
|
||||||
|
selectMultiple: true
|
||||||
nameFilters: [
|
nameFilters: [
|
||||||
//% "Image files (*.jpg *.jpeg *.png)"
|
qsTr("Image files (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" "))
|
||||||
qsTrId("image-files----jpg---jpeg---png-")
|
|
||||||
]
|
]
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
imageBtn.highlighted = false
|
imageBtn.highlighted = false
|
||||||
imageBtn2.highlighted = false
|
imageBtn2.highlighted = false
|
||||||
control.showImageArea()
|
let validImages = validateImages(imageDialog.fileUrls)
|
||||||
|
if (validImages.length > 0) {
|
||||||
|
control.showImageArea(validImages)
|
||||||
|
}
|
||||||
messageInputField.forceActiveFocus();
|
messageInputField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
onRejected: {
|
onRejected: {
|
||||||
|
@ -582,6 +619,26 @@ Rectangle {
|
||||||
radius: control.isStatusUpdateInput ? 36 :
|
radius: control.isStatusUpdateInput ? 36 :
|
||||||
height > defaultInputFieldHeight + 1 || extendedArea.visible ? 16 : 32
|
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 {
|
Rectangle {
|
||||||
id: extendedArea
|
id: extendedArea
|
||||||
visible: isImage || isReply
|
visible: isImage || isReply
|
||||||
|
@ -625,9 +682,13 @@ Rectangle {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: control.isStatusUpdateInput ? 0 : Style.current.halfPadding
|
anchors.topMargin: control.isStatusUpdateInput ? 0 : Style.current.halfPadding
|
||||||
visible: isImage
|
visible: isImage
|
||||||
|
width: messageInputField.width - actions.width
|
||||||
onImageRemoved: {
|
onImageRemoved: {
|
||||||
control.fileUrls = []
|
if (control.fileUrls.length > index && control.fileUrls[index]) {
|
||||||
isImage = false
|
control.fileUrls.splice(index, 1)
|
||||||
|
}
|
||||||
|
isImage = control.fileUrls.length > 0
|
||||||
|
validateImages(control.fileUrls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,90 +4,93 @@ import QtQuick.Controls 2.13
|
||||||
import "../../imports"
|
import "../../imports"
|
||||||
import "../../shared"
|
import "../../shared"
|
||||||
|
|
||||||
Rectangle {
|
Row {
|
||||||
id: imageArea
|
id: imageArea
|
||||||
height: chatImage.height
|
spacing: Style.current.halfPadding
|
||||||
|
|
||||||
signal imageRemoved()
|
signal imageRemoved(int index)
|
||||||
property url imageSource: ""
|
property alias imageSource: rptImages.model
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Image {
|
Repeater {
|
||||||
id: chatImage
|
id: rptImages
|
||||||
property bool hovered: false
|
Item {
|
||||||
height: 64
|
height: chatImage.paintedHeight + closeBtn.height - 5
|
||||||
anchors.left: parent.left
|
width: chatImage.width
|
||||||
anchors.top: parent.top
|
Image {
|
||||||
fillMode: Image.PreserveAspectFit
|
id: chatImage
|
||||||
mipmap: true
|
property bool hovered: false
|
||||||
smooth: false
|
width: 64
|
||||||
antialiasing: true
|
height: 64
|
||||||
source: parent.imageSource
|
fillMode: Image.PreserveAspectCrop
|
||||||
MouseArea {
|
mipmap: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
smooth: false
|
||||||
anchors.fill: parent
|
antialiasing: true
|
||||||
hoverEnabled: true
|
source: modelData
|
||||||
onEntered: {
|
MouseArea {
|
||||||
chatImage.hovered = true
|
cursorShape: Qt.PointingHandCursor
|
||||||
}
|
anchors.fill: parent
|
||||||
onExited: {
|
hoverEnabled: true
|
||||||
chatImage.hovered = false
|
onEntered: {
|
||||||
}
|
chatImage.hovered = true
|
||||||
}
|
}
|
||||||
|
onExited: {
|
||||||
layer.enabled: true
|
chatImage.hovered = false
|
||||||
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
|
layer.enabled: true
|
||||||
anchors.right: parent.right
|
layer.effect: OpacityMask {
|
||||||
width: 32
|
maskSource: Item {
|
||||||
height: 32
|
width: chatImage.width
|
||||||
radius: 4
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue