refactor(messages): make message sending async (#15237)

Fixes #12411

Makes message sending async and adds a placeholder "Sending.." message
This commit is contained in:
Jonathan Rainville 2024-06-20 18:46:12 -04:00 committed by GitHub
parent 033bf99974
commit 8ca51c34cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 298 additions and 85 deletions

View File

@ -95,6 +95,18 @@ proc init*(self: Controller) =
self.unfurlingPlanActiveRequest = ""
self.handleUnfurlingPlan(self.unfurlingPlanActiveRequestUnfurlAfter)
self.events.on(SIGNAL_SENDING_SUCCESS) do(e:Args):
let args = MessageSendingSuccess(e)
if self.chatId != args.chat.id:
return
self.delegate.onSendingMessageSuccess()
self.events.on(SIGNAL_SENDING_FAILED) do(e:Args):
let args = MessageSendingFailure(e)
if self.chatId != args.chatId:
return
self.delegate.onSendingMessageFailure()
proc getChatId*(self: Controller): string =
return self.chatId
@ -116,9 +128,9 @@ proc sendImages*(self: Controller,
msg: string,
replyTo: string,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview]): string =
linkPreviews: seq[LinkPreview]) =
self.resetLinkPreviews()
self.chatService.sendImages(
self.chatService.asyncSendImages(
self.chatId,
imagePathsAndDataJson,
msg,
@ -134,7 +146,7 @@ proc sendChatMessage*(self: Controller,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview]) =
self.resetLinkPreviews()
self.chatService.sendChatMessage(self.chatId,
self.chatService.asyncSendChatMessage(self.chatId,
msg,
replyTo,
contentType,

View File

@ -22,7 +22,7 @@ method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
method sendChatMessage*(self: AccessInterface, msg: string, replyTo: string, contentType: int, linkPreviews: seq[LinkPreview]) {.base.} =
raise newException(ValueError, "No implementation available")
method sendImages*(self: AccessInterface, imagePathsJson: string, msg: string, replyTo: string, linkPreviews: seq[LinkPreview]): string {.base.} =
method sendImages*(self: AccessInterface, imagePathsJson: string, msg: string, replyTo: string, linkPreviews: seq[LinkPreview]) {.base.} =
raise newException(ValueError, "No implementation available")
method requestAddressForTransaction*(self: AccessInterface, fromAddress: string, amount: string, tokenAddress: string) {.base.} =
@ -138,3 +138,9 @@ method setUrls*(self: AccessInterface, urls: seq[string]) {.base.} =
method getContactDetails*(self: AccessInterface, contactId: string): ContactDetails {.base.} =
raise newException(ValueError, "No implementation available")
method onSendingMessageSuccess*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onSendingMessageFailure*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -65,7 +65,7 @@ method getModuleAsVariant*(self: Module): QVariant =
proc getChatId*(self: Module): string =
return self.controller.getChatId()
method sendImages*(self: Module, imagePathsAndDataJson: string, msg: string, replyTo: string, linkPreviews: seq[LinkPreview]): string =
method sendImages*(self: Module, imagePathsAndDataJson: string, msg: string, replyTo: string, linkPreviews: seq[LinkPreview]) =
self.controller.sendImages(imagePathsAndDataJson, msg, replyTo, singletonInstance.userProfile.getPreferredName(), linkPreviews)
method sendChatMessage*(
@ -136,3 +136,9 @@ method setUrls*(self: Module, urls: seq[string]) =
method getContactDetails*(self: Module, contactId: string): ContactDetails =
return self.controller.getContactDetails(contactId)
method onSendingMessageSuccess*(self: Module) =
self.view.emitSendingMessageSuccess()
method onSendingMessageFailure*(self: Module) =
self.view.emitSendingMessageFailure()

View File

@ -15,10 +15,13 @@ QtObject:
linkPreviewModelVariant: QVariant
urlsModel: urls_model.Model
urlsModelVariant: QVariant
sendingInProgress: bool
askToEnableLinkPreview: bool
emojiReactionsModel: emoji_reactions_model.Model
emojiReactionsModelVariant: QVariant
proc setSendingInProgress*(self: View, value: bool)
proc delete*(self: View) =
self.QObject.delete
self.preservedPropertiesVariant.delete
@ -53,11 +56,13 @@ QtObject:
replyTo: string,
contentType: int) {.slot.} =
# FIXME: Update this when `setText` is async.
self.setSendingInProgress(true)
self.delegate.setText(msg, false)
self.delegate.sendChatMessage(msg, replyTo, contentType, self.linkPreviewModel.getUnfuledLinkPreviews())
proc sendImages*(self: View, imagePathsAndDataJson: string, msg: string, replyTo: string): string {.slot.} =
proc sendImages*(self: View, imagePathsAndDataJson: string, msg: string, replyTo: string) {.slot.} =
# FIXME: Update this when `setText` is async.
self.setSendingInProgress(true)
self.delegate.setText(msg, false)
self.delegate.sendImages(imagePathsAndDataJson, msg, replyTo, self.linkPreviewModel.getUnfuledLinkPreviews())
@ -154,3 +159,21 @@ QtObject:
QtProperty[QVariant] urlsModel:
read = getUrlsModel
notify = urlsModelChanged
proc sendingInProgressChanged(self: View) {.signal.}
proc getSendingInProgress*(self: View): bool {.slot.} =
return self.sendingInProgress
QtProperty[bool] sendingInProgress:
read = getSendingInProgress
notify = sendingInProgressChanged
proc setSendingInProgress*(self: View, value: bool) =
self.sendingInProgress = value
self.sendingInProgressChanged()
proc emitSendingMessageSuccess*(self: View) =
self.setSendingInProgress(false)
proc emitSendingMessageFailure*(self: View) =
self.setSendingInProgress(false)

View File

@ -79,10 +79,10 @@ proc init*(self: Controller) =
self.delegate.onSendingMessageSuccess(args.message)
self.events.on(SIGNAL_SENDING_FAILED) do(e:Args):
let args = ChatArgs(e)
let args = MessageSendingFailure(e)
if(self.chatId != args.chatId):
return
self.delegate.onSendingMessageError()
self.delegate.onSendingMessageError(args.error)
self.events.on(SIGNAL_ENVELOPE_SENT) do(e:Args):
let args = EnvelopeSentArgs(e)

View File

@ -60,7 +60,7 @@ method messagesAdded*(self: AccessInterface, messages: seq[MessageDto]) {.base.}
method onSendingMessageSuccess*(self: AccessInterface, message: MessageDto) {.base.} =
raise newException(ValueError, "No implementation available")
method onSendingMessageError*(self: AccessInterface) {.base.} =
method onSendingMessageError*(self: AccessInterface, error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onEnvelopeSent*(self: AccessInterface, messagesIds: seq[string]) {.base.} =

View File

@ -433,8 +433,8 @@ method onSendingMessageSuccess*(self: Module, message: MessageDto) =
self.view.emitSendingMessageSuccessSignal()
self.removeNewMessagesMarker()
method onSendingMessageError*(self: Module) =
self.view.emitSendingMessageErrorSignal()
method onSendingMessageError*(self: Module, error: string) =
self.view.emitSendingMessageErrorSignal(error)
method onEnvelopeSent*(self: Module, messagesIds: seq[string]) =
for messageId in messagesIds:

View File

@ -106,10 +106,10 @@ QtObject:
proc emitSendingMessageSuccessSignal*(self: View) =
self.messageSuccessfullySent()
proc sendingMessageFailed*(self: View) {.signal.}
proc sendingMessageFailed*(self: View, error: string) {.signal.}
proc emitSendingMessageErrorSignal*(self: View) =
self.sendingMessageFailed()
proc emitSendingMessageErrorSignal*(self: View, error: string) =
self.sendingMessageFailed(error)
proc deleteMessage*(self: View, messageId: string) {.slot.} =
self.delegate.deleteMessage(messageId)

View File

@ -151,7 +151,7 @@ proc sendSticker*(
replyTo: string,
sticker: StickerDto,
preferredUsername: string) =
self.stickerService.sendSticker(channelId, replyTo, sticker, preferredUsername)
self.stickerService.asyncSendSticker(channelId, replyTo, sticker, preferredUsername)
proc wei2Eth*(self: Controller, price: Stuint[256]): string =
eth_utils.wei2Eth(price)

View File

@ -62,3 +62,77 @@ proc asyncCheckAllChannelsPermissionsTask(argEncoded: string) {.gcsafe, nimcall.
"communityId": arg.communityId,
"error": e.msg,
})
type
AsyncSendMessageTaskArg = ref object of QObjectTaskArg
chatId: string
processedMsg: string
replyTo: string
contentType: int
preferredUsername: string
communityId: string
standardLinkPreviews: JsonNode
statusLinkPreviews: JsonNode
const asyncSendMessageTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncSendMessageTaskArg](argEncoded)
try:
let response = status_chat.sendChatMessage(
arg.chatId,
arg.processedMsg,
arg.replyTo,
arg.contentType,
arg.preferredUsername,
arg.standardLinkPreviews,
arg.statusLinkPreviews,
arg.communityId)
arg.finish(%* {
"response": response,
"chatId": arg.chatId,
"error": "",
})
except Exception as e:
arg.finish(%* {
"error": e.msg,
"chatId": arg.chatId,
})
type
AsyncSendImagesTaskArg = ref object of QObjectTaskArg
chatId: string
imagePathsAndDataJson: string
tempDir: string
msg: string
replyTo: string
preferredUsername: string
linkPreviews: JsonNode
const asyncSendImagesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncSendImagesTaskArg](argEncoded)
try:
var images = Json.decode(arg.imagePathsAndDataJson, seq[string])
var imagePaths: seq[string] = @[]
for imagePathOrSource in images.mitems:
let imagePath = image_resizer(imagePathOrSource, 2000, arg.tempDir)
if imagePath != "":
imagePaths.add(imagePath)
let response = status_chat.sendImages(arg.chatId, imagePaths, arg.msg, arg.replyTo, arg.preferredUsername,
arg.linkPreviews)
for imagePath in imagePaths:
removeFile(imagePath)
arg.finish(%* {
"response": response,
"chatId": arg.chatId,
"error": "",
})
except Exception as e:
arg.finish(%* {
"error": e.msg,
"chatId": arg.chatId,
})

View File

@ -1,10 +1,10 @@
import NimQml, Tables, json, sequtils, stew/shims/strformat, chronicles, os, strutils, uuids, base64
import NimQml, Tables, json, sequtils, chronicles, os, strutils, uuids, base64
import std/[times, os]
import ../../../app/core/tasks/[qt, threadpool]
import ./dto/chat as chat_dto
import ../message/dto/message as message_dto
import ../message/dto/link_preview
import ../message/dto/[link_preview, standard_link_preview, status_link_preview]
import ../activity_center/dto/notification as notification_dto
import ../community/dto/community as community_dto
import ../contacts/service as contact_service
@ -47,6 +47,10 @@ type
chat*: ChatDto
message*: MessageDto
MessageSendingFailure* = ref object of Args
chatId*: string
error*: string
MessageArgs* = ref object of Args
id*: string
channel*: string
@ -420,61 +424,94 @@ QtObject:
error "Error deleting channel", chatId, msg = e.msg
return
proc sendImages*(self: Service,
proc asyncSendImages*(self: Service,
chatId: string,
imagePathsAndDataJson: string,
msg: string,
replyTo: string,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview] = @[]): string =
result = ""
linkPreviews: seq[LinkPreview] = @[]) =
let arg = AsyncSendImagesTaskArg(
tptr: asyncSendImagesTask,
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncSendImagesDone",
chatId: chatId,
imagePathsAndDataJson: imagePathsAndDataJson,
tempDir: TMPDIR,
msg: msg,
replyTo: replyTo,
preferredUsername: preferredUsername,
linkPreviews: %linkPreviews,
)
self.threadpool.start(arg)
proc onAsyncSendImagesDone*(self: Service, rpcResponseJson: string) {.slot.} =
let rpcResponseObj = rpcResponseJson.parseJson
try:
var images = Json.decode(imagePathsAndDataJson, seq[string])
let base64JPGPrefix = "data:image/jpeg;base64,"
var imagePaths: seq[string] = @[]
for imagePathOrSource in images.mitems:
let imagePath = image_resizer(imagePathOrSource, 2000, TMPDIR)
if imagePath != "":
imagePaths.add(imagePath)
let errorString = rpcResponseObj{"error"}.getStr()
if errorString != "":
raise newException(CatchableError, errorString)
let response = status_chat.sendImages(chatId, imagePaths, msg, replyTo, preferredUsername, linkPreviews)
let rpcResponse = Json.decode($rpcResponseObj["response"], RpcResponse[JsonNode])
for imagePath in imagePaths:
removeFile(imagePath)
discard self.processMessengerResponse(response)
discard self.processMessengerResponse(rpcResponse)
except Exception as e:
error "Error sending images", msg = e.msg
result = fmt"Error sending images: {e.msg}"
self.events.emit(SIGNAL_SENDING_FAILED, MessageSendingFailure(chatId: rpcResponseObj["chatId"].getStr, error: e.msg))
proc sendChatMessage*(
self: Service,
chatId: string,
msg: string,
replyTo: string,
contentType: int,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview] = @[],
communityId: string = "") =
proc asyncSendChatMessage*(self: Service,
chatId: string,
msg: string,
replyTo: string,
contentType: int,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview] = @[],
communityId: string = "") =
try:
let allKnownContacts = self.contactService.getContactsByGroup(ContactsGroup.AllKnownContacts)
let processedMsg = message_common.replaceMentionsWithPubKeys(allKnownContacts, msg)
let response = status_chat.sendChatMessage(
chatId,
processedMsg,
replyTo,
contentType,
preferredUsername,
linkPreviews,
communityId) # Only send a community ID for the community invites
let (standardLinkPreviews, statusLinkPreviews) = extractLinkPreviewsLists(linkPreviews)
let (chats, messages) = self.processMessengerResponse(response)
if chats.len == 0 or messages.len == 0:
self.events.emit(SIGNAL_SENDING_FAILED, ChatArgs(chatId: chatId))
let arg = AsyncSendMessageTaskArg(
tptr: asyncSendMessageTask,
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncSendMessageDone",
chatId: chatId,
processedMsg: processedMsg,
replyTo: replyTo,
contentType: contentType,
preferredUsername: preferredUsername,
communityId: communityId, # Only send a community ID for the community invites
standardLinkPreviews: %standardLinkPreviews,
statusLinkPreviews: %statusLinkPreviews,
)
self.threadpool.start(arg)
except Exception as e:
error "Error sending message", msg = e.msg
self.events.emit(SIGNAL_SENDING_FAILED, MessageSendingFailure(chatId: chatId, error: e.msg))
proc onAsyncSendMessageDone*(self: Service, rpcResponseJson: string) {.slot.} =
let rpcResponseObj = rpcResponseJson.parseJson
try:
let errorString = rpcResponseObj{"error"}.getStr()
if errorString != "":
raise newException(CatchableError, errorString)
let rpcResponse = Json.decode($rpcResponseObj["response"], RpcResponse[JsonNode])
let (chats, messages) = self.processMessengerResponse(rpcResponse)
if chats.len == 0 or messages.len == 0:
raise newException(CatchableError, "no chat or message returned")
except Exception as e:
error "Error sending message", msg = e.msg
self.events.emit(SIGNAL_SENDING_FAILED, MessageSendingFailure(chatId: rpcResponseObj["chatId"].getStr, error: e.msg))
proc requestAddressForTransaction*(self: Service, chatId: string, fromAddress: string, amount: string, tokenAddress: string) =
try:

View File

@ -93,3 +93,38 @@ proc installStickerPackTask(argEncoded: string) {.gcsafe, nimcall.} =
error "Error installing stickers", message = getCurrentExceptionMsg()
let tpl: tuple[packId: string, installed: bool] = (arg.packId, installed)
arg.finish(tpl)
type
AsyncSendStickerTaskArg = ref object of QObjectTaskArg
chatId: string
replyTo: string
stickerHash: string
stickerPackId: string
preferredUsername: string
const asyncSendStickerTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncSendStickerTaskArg](argEncoded)
try:
let response = status_chat.sendChatMessage(
arg.chatId,
"You can see a nice sticker here!",
arg.replyTo,
ContentType.Sticker.int,
arg.preferredUsername,
standardLinkPreviews = JsonNode(),
statusLinkPreviews = JsonNode(),
communityId = "", # communityId is not necessary when sending a sticker
arg.stickerHash,
arg.stickerPackId,
)
arg.finish(%* {
"response": response,
"chatId": arg.chatId,
"error": "",
})
except Exception as e:
arg.finish(%* {
"error": e.msg,
"chatId": arg.chatId,
})

View File

@ -345,25 +345,39 @@ QtObject:
except RpcException:
error "Error removing installed sticker", message = getCurrentExceptionMsg()
proc sendSticker*(
proc asyncSendSticker*(
self: Service,
chatId: string,
replyTo: string,
sticker: StickerDto,
preferredUsername: string) =
let response = status_chat.sendChatMessage(
chatId,
"Update to latest version to see a nice sticker here!",
replyTo,
ContentType.Sticker.int,
preferredUsername,
linkPreviews = @[],
communityId = "", # communityId is not necessary when sending a sticker
sticker.hash,
sticker.packId)
discard self.chatService.processMessengerResponse(response)
let arg = AsyncSendStickerTaskArg(
tptr: asyncSendStickerTask,
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncSendStickerDone",
chatId: chatId,
replyTo: replyTo,
stickerHash: sticker.hash,
stickerPackId: sticker.packId,
preferredUsername: preferredUsername,
)
self.threadpool.start(arg)
self.addStickerToRecent(sticker)
proc onAsyncSendStickerDone*(self: Service, rpcResponseJson: string) {.slot.} =
let rpcResponseObj = rpcResponseJson.parseJson
try:
let errorString = rpcResponseObj{"error"}.getStr()
if errorString != "":
raise newException(CatchableError, errorString)
let rpcResponse = Json.decode($rpcResponseObj["response"], RpcResponse[JsonNode])
discard self.chatService.processMessengerResponse(rpcResponse)
except Exception as e:
error "Error sending sticker", msg = e.msg
self.events.emit(SIGNAL_SENDING_FAILED, ChatArgs(chatId: rpcResponseObj["chatId"].getStr))
proc removeRecentStickers*(self: Service, packId: string) =
self.recentStickers.keepItIf(it.packId != packId)

View File

@ -2,9 +2,6 @@ import json, sequtils, sugar, strutils
import core, ../app_service/common/utils
import response_type
import interpret/cropped_image
import ../app_service/service/message/dto/link_preview
import ../app_service/service/message/dto/standard_link_preview
import ../app_service/service/message/dto/status_link_preview
export response_type
@ -59,12 +56,12 @@ proc sendChatMessage*(
replyTo: string,
contentType: int,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview],
standardLinkPreviews: JsonNode,
statusLinkPreviews: JsonNode,
communityId: string = "",
stickerHash: string = "",
stickerPack: string = "0",
): RpcResponse[JsonNode] =
let (standardLinkPreviews, statusLinkPreviews) = extractLinkPreviewsLists(linkPreviews)
result = callPrivateRPC("sendChatMessage".prefix, %* [
{
"chatId": chatId,
@ -87,7 +84,7 @@ proc sendImages*(chatId: string,
msg: string,
replyTo: string,
preferredUsername: string,
linkPreviews: seq[LinkPreview],
linkPreviews: JsonNode,
): RpcResponse[JsonNode] =
let imagesJson = %* images.map(image => %*
{

View File

@ -80,6 +80,8 @@ Item {
id: d
readonly property var activeChatContentModule: d.getChatContentModule(root.activeChatId)
property bool sendingInProgress: !!d.activeChatContentModule? d.activeChatContentModule.inputAreaModule.sendingInProgress : false
readonly property var urlsList: {
if (!d.activeChatContentModule) {
return
@ -276,6 +278,7 @@ Item {
&& root.rootStore.sectionDetails.joined
&& !root.rootStore.sectionDetails.amIBanned
&& root.rootStore.isUserAllowedToSendMessage
&& !d.sendingInProgress
}
store: root.rootStore
@ -298,6 +301,9 @@ Item {
if (!root.canPost) {
return qsTr("Sorry, you don't have permissions to post in this channel.")
}
if (d.sendingInProgress) {
return qsTr("Sending...")
}
return root.rootStore.chatInputPlaceHolderText
} else {
return "";
@ -339,19 +345,19 @@ Item {
return
}
if (root.rootStore.sendMessage(d.activeChatContentModule.getMyChatId(),
event,
chatInput.getTextWithPublicKeys(),
chatInput.isReply? chatInput.replyMessageId : "",
chatInput.fileUrlsAndSources
))
{
Global.playSendMessageSound()
if (root.rootStore.sendMessage(d.activeChatContentModule.getMyChatId(),
event,
chatInput.getTextWithPublicKeys(),
chatInput.isReply? chatInput.replyMessageId : "",
chatInput.fileUrlsAndSources
))
{
Global.playSendMessageSound()
chatInput.setText("")
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
chatInput.setText("")
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
}
onKeyUpPress: {

View File

@ -86,7 +86,8 @@ Item {
chatLogView.positionViewAtBeginning()
}
function onSendingMessageFailed() {
function onSendingMessageFailed(error) {
sendingMsgFailedPopup.error = error
sendingMsgFailedPopup.open()
}
@ -377,9 +378,11 @@ Item {
}
MessageDialog {
property string error
id: sendingMsgFailedPopup
standardButtons: StandardButton.Ok
text: qsTr("Failed to send message.")
text: qsTr("Failed to send message.\n" + error)
icon: StandardIcon.Critical
}