feat(chat): support copy & pasting images into chat input
This adds support for receiving copied images from the clipboard and pasting it into the chat input. After pasting, chat input will recognize the image and render a preview similar to how it would do it when selecting images via the file dialog. **Also important to note**: At the time of this PR, it seems that desktop only supports sending jpegs to status-go. I'm not sure if this was deliberately done this way because the protocol says it supports jpg, png, webp and gif. Because of this, pasting for example pngs will work, however transparency will be lost (which is also most likely the cause of #8820) This PR operates on that assumption. So while it adds support for copy/pasting images, it does not address the lack of file type support. Closes #3395
This commit is contained in:
parent
5dcd3dde23
commit
213924f6e9
|
@ -45,8 +45,8 @@ proc getChatId*(self: Controller): string =
|
|||
proc belongsToCommunity*(self: Controller): bool =
|
||||
return self.belongsToCommunity
|
||||
|
||||
proc sendImages*(self: Controller, imagePathsJson: string): string =
|
||||
self.chatService.sendImages(self.chatId, imagePathsJson)
|
||||
proc sendImages*(self: Controller, imagePathsAndDataJson: string): string =
|
||||
self.chatService.sendImages(self.chatId, imagePathsAndDataJson)
|
||||
|
||||
proc sendChatMessage*(
|
||||
self: Controller,
|
||||
|
|
|
@ -60,8 +60,8 @@ method getModuleAsVariant*(self: Module): QVariant =
|
|||
method getChatId*(self: Module): string =
|
||||
return self.controller.getChatId()
|
||||
|
||||
method sendImages*(self: Module, imagePathsJson: string): string =
|
||||
self.controller.sendImages(imagePathsJson)
|
||||
method sendImages*(self: Module, imagePathsAndDataJson: string): string =
|
||||
self.controller.sendImages(imagePathsAndDataJson)
|
||||
|
||||
method sendChatMessage*(
|
||||
self: Module,
|
||||
|
|
|
@ -36,8 +36,8 @@ QtObject:
|
|||
contentType: int) {.slot.} =
|
||||
self.delegate.sendChatMessage(msg, replyTo, contentType)
|
||||
|
||||
proc sendImages*(self: View, sendImages: string): string {.slot.} =
|
||||
self.delegate.sendImages(sendImages)
|
||||
proc sendImages*(self: View, imagePathsAndDataJson: string): string {.slot.} =
|
||||
self.delegate.sendImages(imagePathsAndDataJson)
|
||||
|
||||
proc acceptAddressRequest*(self: View, messageId: string , address: string) {.slot.} =
|
||||
self.delegate.acceptRequestAddressForTransaction(messageId, address)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import NimQml, Tables, json, sequtils, strformat, chronicles, os, std/algorithm, strutils
|
||||
import NimQml, Tables, json, sequtils, strformat, chronicles, os, std/algorithm, strutils, uuids, base64
|
||||
|
||||
import ./dto/chat as chat_dto
|
||||
import ../message/dto/message as message_dto
|
||||
|
@ -339,18 +339,45 @@ QtObject:
|
|||
error "Error deleting channel", chatId, msg = e.msg
|
||||
return
|
||||
|
||||
proc sendImages*(self: Service, chatId: string, imagePathsJson: string): string =
|
||||
proc sendImages*(self: Service, chatId: string, imagePathsAndDataJson: string): string =
|
||||
result = ""
|
||||
try:
|
||||
var images = Json.decode(imagePathsJson, seq[string])
|
||||
var images = Json.decode(imagePathsAndDataJson, seq[string])
|
||||
let base64JPGPrefix = "data:image/jpeg;base64,"
|
||||
var imagePaths: seq[string] = @[]
|
||||
|
||||
for imagePathOrSource in images.mitems:
|
||||
|
||||
var imagePath = ""
|
||||
|
||||
if imagePathOrSource.startsWith(base64JPGPrefix):
|
||||
# If an image was copied to clipboard we're receiving it from there
|
||||
# as base64 encoded string. The base64 string will always have JPG data
|
||||
# because image_resizer (a few lines below) will always generate jpgs
|
||||
# (which needs to be fixed as well).
|
||||
#
|
||||
# We save the image data to a tmp location because image_resizer expects
|
||||
# a filepath to operate on.
|
||||
#
|
||||
# TODO:
|
||||
# Make image_resizer work for all image types (and ensure the same
|
||||
# for base64 encoded string received via clipboard
|
||||
let base64Str = imagePathOrSource.replace(base64JPGPrefix, "")
|
||||
let bytes = base64.decode(base64Str)
|
||||
let tmpPath = TMPDIR & $genUUID() & ".jpg"
|
||||
writeFile(tmpPath, bytes)
|
||||
imagePath = tmpPath
|
||||
else:
|
||||
imagePath = imagePathOrSource
|
||||
|
||||
for imagePath in images.mitems:
|
||||
var image = singletonInstance.utils.formatImagePath(imagePath)
|
||||
imagePath = image_resizer(image, 2000, TMPDIR)
|
||||
|
||||
let response = status_chat.sendImages(chatId, images)
|
||||
imagePaths.add(imagePath)
|
||||
|
||||
for imagePath in images.items:
|
||||
let response = status_chat.sendImages(chatId, imagePaths)
|
||||
|
||||
for imagePath in imagePaths:
|
||||
removeFile(imagePath)
|
||||
|
||||
discard self.processMessageUpdateAfterSend(response)
|
||||
|
|
|
@ -98,11 +98,12 @@ QtObject {
|
|||
return msg
|
||||
}
|
||||
|
||||
function sendMessage(event, text, replyMessageId, fileUrls) {
|
||||
function sendMessage(event, text, replyMessageId, fileUrlsAndSources) {
|
||||
var chatContentModule = currentChatContentModule()
|
||||
if (fileUrls.length > 0){
|
||||
chatContentModule.inputAreaModule.sendImages(JSON.stringify(fileUrls));
|
||||
if (fileUrlsAndSources.length > 0){
|
||||
chatContentModule.inputAreaModule.sendImages(JSON.stringify(fileUrlsAndSources));
|
||||
}
|
||||
|
||||
let msg = globalUtils.plainText(StatusQUtils.Emoji.deparse(text))
|
||||
if (msg.length > 0) {
|
||||
msg = interpretMessage(msg)
|
||||
|
|
|
@ -234,7 +234,7 @@ ColumnLayout {
|
|||
if(root.rootStore.sendMessage(event,
|
||||
chatInput.getTextWithPublicKeys(),
|
||||
chatInput.isReply? chatInput.replyMessageId : "",
|
||||
chatInput.fileUrls
|
||||
chatInput.fileUrlsAndSources
|
||||
))
|
||||
{
|
||||
Global.sendMessageSound.stop();
|
||||
|
|
|
@ -58,7 +58,7 @@ Rectangle {
|
|||
|
||||
property alias textInput: messageInputField
|
||||
|
||||
property var fileUrls: []
|
||||
property var fileUrlsAndSources: []
|
||||
|
||||
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this proeprty?
|
||||
|
||||
|
@ -398,21 +398,27 @@ Rectangle {
|
|||
}
|
||||
|
||||
if (event.matches(StandardKey.Paste)) {
|
||||
messageInputField.remove(messageInputField.selectionStart, messageInputField.selectionEnd)
|
||||
|
||||
// cursor position must be stored in a helper property because setting readonly to true causes change
|
||||
// of the cursor position to the end of the input
|
||||
d.copyTextStart = messageInputField.cursorPosition
|
||||
messageInputField.readOnly = true
|
||||
|
||||
const clipboardText = globalUtils.plainText(QClipboardProxy.text)
|
||||
const copiedText = globalUtils.plainText(d.copiedTextPlain)
|
||||
if (copiedText === clipboardText) {
|
||||
d.internalPaste = true
|
||||
if (QClipboardProxy.hasImage) {
|
||||
const clipboardImage = QClipboardProxy.imageBase64
|
||||
showImageArea([clipboardImage])
|
||||
event.accepted = true
|
||||
} else {
|
||||
d.copiedTextPlain = ""
|
||||
d.copiedTextFormatted = ""
|
||||
d.copiedMentionsPos = []
|
||||
messageInputField.remove(messageInputField.selectionStart, messageInputField.selectionEnd)
|
||||
|
||||
// cursor position must be stored in a helper property because setting readonly to true causes change
|
||||
// of the cursor position to the end of the input
|
||||
d.copyTextStart = messageInputField.cursorPosition
|
||||
messageInputField.readOnly = true
|
||||
|
||||
const clipboardText = globalUtils.plainText(QClipboardProxy.text)
|
||||
const copiedText = globalUtils.plainText(d.copiedTextPlain)
|
||||
if (copiedText === clipboardText) {
|
||||
d.internalPaste = true
|
||||
} else {
|
||||
d.copiedTextPlain = ""
|
||||
d.copiedTextFormatted = ""
|
||||
d.copiedMentionsPos = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -814,7 +820,7 @@ Rectangle {
|
|||
function hideExtendedArea() {
|
||||
isImage = false;
|
||||
isReply = false;
|
||||
control.fileUrls = []
|
||||
control.fileUrlsAndSources = []
|
||||
imageArea.imageSource = [];
|
||||
replyArea.userName = ""
|
||||
replyArea.message = ""
|
||||
|
@ -836,11 +842,12 @@ Rectangle {
|
|||
return validImages
|
||||
}
|
||||
|
||||
function showImageArea(imagePaths) {
|
||||
function showImageArea(imagePathsOrData) {
|
||||
isImage = true;
|
||||
isReply = false;
|
||||
imageArea.imageSource = imagePaths
|
||||
control.fileUrls = imageArea.imageSource
|
||||
|
||||
imageArea.imageSource = imagePathsOrData
|
||||
control.fileUrlsAndSources = imageArea.imageSource
|
||||
}
|
||||
|
||||
function showReplyArea(messageId, userName, message, contentType, image, sticker) {
|
||||
|
@ -890,7 +897,7 @@ Rectangle {
|
|||
]
|
||||
onAccepted: {
|
||||
imageBtn.highlighted = false
|
||||
let validImages = validateImages(imageDialog.fileUrls)
|
||||
let validImages = validateImages(imageDialog.fileUrlsAndSources)
|
||||
if (validImages.length > 0) {
|
||||
control.showImageArea(validImages)
|
||||
}
|
||||
|
@ -1147,11 +1154,11 @@ Rectangle {
|
|||
visible: isImage
|
||||
onImageClicked: Global.openImagePopup(chatImage, messageContextMenu)
|
||||
onImageRemoved: {
|
||||
if (control.fileUrls.length > index && control.fileUrls[index]) {
|
||||
control.fileUrls.splice(index, 1)
|
||||
if (control.fileUrlsAndSources.length > index && control.fileUrlsAndSources[index]) {
|
||||
control.fileUrlsAndSources.splice(index, 1)
|
||||
}
|
||||
isImage = control.fileUrls.length > 0
|
||||
validateImages(control.fileUrls)
|
||||
isImage = control.fileUrlsAndSources.length > 0
|
||||
validateImages(control.fileUrlsAndSources)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit ea89a41d96c2545902f2d0d8006663f00f1ee905
|
||||
Subproject commit 3047521bd009091cbc077909a3e4a24513aeece0
|
Loading…
Reference in New Issue