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:
Pascal Precht 2022-11-25 15:08:12 +01:00 committed by r4bbit.eth
parent 5dcd3dde23
commit 213924f6e9
8 changed files with 76 additions and 41 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -234,7 +234,7 @@ ColumnLayout {
if(root.rootStore.sendMessage(event,
chatInput.getTextWithPublicKeys(),
chatInput.isReply? chatInput.replyMessageId : "",
chatInput.fileUrls
chatInput.fileUrlsAndSources
))
{
Global.sendMessageSound.stop();

View File

@ -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)
}
}

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit ea89a41d96c2545902f2d0d8006663f00f1ee905
Subproject commit 3047521bd009091cbc077909a3e4a24513aeece0