From db4731f19b9e2685a1f6f75cf32cc9b22e6dcb39 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Mon, 25 Apr 2022 12:44:24 +0400 Subject: [PATCH] mobile: correctly resize images, refine image sending UI (#546) --- .../java/chat/simplex/app/model/SimpleXAPI.kt | 2 +- .../chat/simplex/app/views/chat/ChatView.kt | 34 +- .../app/views/chat/ComposeImageView.kt | 17 +- .../simplex/app/views/chat/ComposeView.kt | 62 ++- .../simplex/app/views/helpers/GetImageView.kt | 28 +- .../simplex/app/views/helpers/LinkPreviews.kt | 2 +- .../chat/simplex/app/views/helpers/Util.kt | 3 + .../app/views/usersettings/UserProfileView.kt | 2 +- .../app/src/main/res/values-ru/strings.xml | 3 +- .../app/src/main/res/values/strings.xml | 1 + apps/ios/Shared/FileUtils.swift | 3 + apps/ios/Shared/Model/SimpleXAPI.swift | 3 +- apps/ios/Shared/Views/Chat/ChatView.swift | 102 +---- .../Chat/ComposeMessage/ComposeView.swift | 379 +++++++++++++----- .../Chat/ComposeMessage/ContextItemView.swift | 39 +- .../Chat/ComposeMessage/SendMessageView.swift | 47 +-- .../Views/Helpers/ComposeImageView.swift | 35 +- .../Views/Helpers/ComposeLinkView.swift | 2 +- .../Shared/Views/Helpers/ImagePicker.swift | 25 +- apps/ios/Shared/Views/TerminalView.swift | 20 +- .../Views/UserSettings/UserProfile.swift | 2 +- 21 files changed, 485 insertions(+), 326 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index bc126724b1..6655a60a90 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -371,7 +371,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt val cItem = r.chatItem.chatItem chatModel.addChatItem(cInfo, cItem) val file = cItem.file - if (file != null && file.fileSize <= 236700) { // 394500 + if (file != null && file.fileSize <= MAX_IMAGE_SIZE) { withApi {receiveFile(file.fileId)} } if (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 37716c8979..ed5b7c8f6b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -87,13 +87,16 @@ fun ChatView(chatModel: ChatModel) { val cInfo = chat.chatInfo val ei = editingItem.value if (ei != null) { - val updatedItem = chatModel.controller.apiUpdateChatItem( - type = cInfo.chatType, - id = cInfo.apiId, - itemId = ei.meta.itemId, - mc = MsgContent.MCText(msg) - ) - if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem) + val oldMsgContent = ei.content.msgContent + if (oldMsgContent != null) { + val updatedItem = chatModel.controller.apiUpdateChatItem( + type = cInfo.chatType, + id = cInfo.apiId, + itemId = ei.meta.itemId, + mc = updateMsgContent(oldMsgContent, msg) + ) + if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem) + } } else { var file: String? = null val imagePreviewData = imagePreview.value @@ -142,17 +145,26 @@ fun ChatView(chatModel: ChatModel) { } }, parseMarkdown = { text -> runBlocking { chatModel.controller.apiParseMarkdown(text) } }, - onImageChange = { bitmap -> imagePreview.value = resizeImageToDataSize(bitmap, maxDataSize = 12500) } + onImageChange = { bitmap -> imagePreview.value = resizeImageToStrSize(bitmap, maxDataSize = 14000) } ) } } +fun updateMsgContent(msgContent: MsgContent, text: String): MsgContent { + return when (msgContent) { + is MsgContent.MCText -> MsgContent.MCText(text) + is MsgContent.MCLink -> MsgContent.MCLink(text, preview = msgContent.preview) + is MsgContent.MCImage -> MsgContent.MCImage(text, image = msgContent.image) + is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = text, json = msgContent.json) + } +} + fun saveImage(context: Context, image: Bitmap): String { - val imageResized = base64ToBitmap(resizeImageToDataSize(image, 160000)) + val dataResized = resizeImageToDataSize(image, maxDataSize = MAX_IMAGE_SIZE) val fileToSave = "image_${System.currentTimeMillis()}.jpg" val file = File(getAppFilesDirectory(context) + "/" + fileToSave) val output = FileOutputStream(file) - imageResized.compress(Bitmap.CompressFormat.JPEG, 100, output) + dataResized.writeTo(output) output.flush() output.close() return fileToSave @@ -205,7 +217,7 @@ fun ChatLayout( topBar = { ChatInfoToolbar(chat, back, info) }, bottomBar = { ComposeView( - msg, quotedItem, editingItem, linkPreview, imagePreview, sendMessage, resetMessage, parseMarkdown, + msg, quotedItem, editingItem, linkPreview, chosenImage, imagePreview, sendMessage, resetMessage, parseMarkdown, showBottomSheet = { scope.launch { bottomSheetModalState.show() } } ) }, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt index 0e2b438de8..4b42cc0256 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt @@ -1,16 +1,22 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp +import chat.simplex.app.R import chat.simplex.app.views.chat.item.SentColorLight import chat.simplex.app.views.helpers.base64ToBitmap +import chat.simplex.app.views.helpers.generalGetString @Composable -fun ComposeImageView(image: String) { +fun ComposeImageView(image: String, cancelImage: () -> Unit) { Row( Modifier .fillMaxWidth() @@ -27,5 +33,14 @@ fun ComposeImageView(image: String) { .height(60.dp) .padding(end = 8.dp) ) + Spacer(Modifier.weight(1f)) + IconButton(onClick = cancelImage, modifier = Modifier.padding(0.dp)) { + Icon( + Icons.Outlined.Close, + contentDescription = generalGetString(R.string.icon_descr_cancel_image_preview), + tint = MaterialTheme.colors.primary, + modifier = Modifier.padding(10.dp) + ) + } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index 6734b30891..425837802c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -1,22 +1,34 @@ package chat.simplex.app.views.chat import ComposeImageView +import android.graphics.Bitmap +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.filled.AttachFile import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.views.helpers.ComposeLinkView +import chat.simplex.app.views.helpers.generalGetString // TODO ComposeState - @Composable fun ComposeView( msg: MutableState, quotedItem: MutableState, editingItem: MutableState, linkPreview: MutableState, + chosenImage: MutableState, imagePreview: MutableState, sendMessage: (String) -> Unit, resetMessage: () -> Unit, @@ -33,10 +45,15 @@ fun ComposeView( linkPreview.value = null } + fun cancelImage() { + chosenImage.value = null + imagePreview.value = null + } + Column { val ip = imagePreview.value if (ip != null) { - ComposeImageView(ip) + ComposeImageView(ip, ::cancelImage) } else { val lp = linkPreview.value if (lp != null) ComposeLinkView(lp, ::cancelPreview) @@ -51,28 +68,29 @@ fun ComposeView( else -> {} } Row( - modifier = Modifier.padding(horizontal = 8.dp), -// // use this padding when attach button is uncommented -// modifier = Modifier.padding(start = 2.dp, end = 8.dp), - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 4.dp, end = 8.dp), + verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(2.dp) ) { -// Icon( -// Icons.Outlined.AddCircleOutline, -// contentDescription = generalGetString(R.string.attach), -// tint = if (editingItem.value == null) MaterialTheme.colors.primary else Color.Gray, -// modifier = Modifier -// .size(40.dp) -// .padding(vertical = 4.dp) -// .clip(CircleShape) -// .clickable { -// if (editingItem.value == null) { -// showBottomSheet() -// } -// } -// ) - SendMsgView(msg, linkPreview, cancelledLinks, parseMarkdown, sendMessage, - editing = editingItem.value != null, sendEnabled = msg.value.isNotEmpty() || imagePreview.value != null) + Box(Modifier.padding(bottom = 12.dp)) { + Icon( + Icons.Filled.AttachFile, + contentDescription = generalGetString(R.string.attach), + tint = if (editingItem.value == null) MaterialTheme.colors.primary else Color.Gray, + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .clickable { + if (editingItem.value == null) { + showBottomSheet() + } + } + ) + } + SendMsgView( + msg, linkPreview, cancelledLinks, parseMarkdown, sendMessage, + editing = editingItem.value != null, sendEnabled = msg.value.isNotEmpty() || imagePreview.value != null + ) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt index a5a0ef27e3..fa8971a071 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt @@ -49,24 +49,42 @@ fun cropToSquare(image: Bitmap): Bitmap { return Bitmap.createBitmap(image, xOffset, yOffset, side, side) } -fun resizeImageToDataSize(image: Bitmap, maxDataSize: Int): String { +fun resizeImageToStrSize(image: Bitmap, maxDataSize: Int): String { var img = image - var str = compressImage(img) + var str = compressImageStr(img) while (str.length > maxDataSize) { val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble()) val clippedRatio = min(ratio, 2.0) val width = (img.width.toDouble() / clippedRatio).toInt() val height = img.height * width / img.width img = Bitmap.createScaledBitmap(img, width, height, true) - str = compressImage(img) + str = compressImageStr(img) } return str } -private fun compressImage(bitmap: Bitmap): String { +private fun compressImageStr(bitmap: Bitmap): String { + return "data:image/jpg;base64," + Base64.encodeToString(compressImageData(bitmap).toByteArray(), Base64.NO_WRAP) +} + +fun resizeImageToDataSize(image: Bitmap, maxDataSize: Int): ByteArrayOutputStream { + var img = image + var stream = compressImageData(img) + while (stream.size() > maxDataSize) { + val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble()) + val clippedRatio = min(ratio, 2.0) + val width = (img.width.toDouble() / clippedRatio).toInt() + val height = img.height * width / img.width + img = Bitmap.createScaledBitmap(img, width, height, true) + stream = compressImageData(img) + } + return stream +} + +private fun compressImageData(bitmap: Bitmap): ByteArrayOutputStream { val stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 85, stream) - return "data:image/jpg;base64," + Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP) + return stream } fun base64ToBitmap(base64ImageString: String) : Bitmap { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt index ec13b5dd27..c078adc830 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt @@ -42,7 +42,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? { if (imageUri != null) { try { val stream = java.net.URL(imageUri).openStream() - val image = resizeImageToDataSize(BitmapFactory.decodeStream(stream), maxDataSize = 14000) + val image = resizeImageToStrSize(BitmapFactory.decodeStream(stream), maxDataSize = 14000) // TODO add once supported in iOS // val description = ogTags.firstOrNull { // it.attr("property") == "og:description" diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index be4e5d2d7c..15d6e1863b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -199,6 +199,9 @@ private fun spannableStringToAnnotatedString( } } +// maximum image file size to be auto-accepted +const val MAX_IMAGE_SIZE = 236700 + fun getFilesDirectory(context: Context): String { return context.filesDir.toString() } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt index 0a401c1460..8a663b36c9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt @@ -79,7 +79,7 @@ fun UserProfileLayout( sheetContent = { GetImageBottomSheet( chosenImage, - onImageChange = { bitmap -> profileImage.value = resizeImageToDataSize(cropToSquare(bitmap), maxDataSize = 12500) }, + onImageChange = { bitmap -> profileImage.value = resizeImageToStrSize(cropToSquare(bitmap), maxDataSize = 12500) }, hideBottomSheet = { scope.launch { bottomSheetModalState.hide() } }) diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index d5031bfd54..313b9f39fa 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -78,6 +78,7 @@ Изображение + удалить превью изображения Удалить контакт? @@ -167,7 +168,7 @@ Ваш профиль будет отправлен\nвашему контакту Если вы не можете встретиться лично, вы можете сосканировать QR код во время видеозвонка, или ваш контакт может отправить вам ссылку. Поделиться ссылкой - Чтобы соединиться, вставьте в это поле ссылку, полученную от вашего контакта.. + Чтобы соединиться, вставьте в это поле ссылку, полученную от вашего контакта. diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 4307d757f5..1d840899fc 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -78,6 +78,7 @@ Image + cancel image preview Delete contact? diff --git a/apps/ios/Shared/FileUtils.swift b/apps/ios/Shared/FileUtils.swift index 2c36e33a2f..5739cc6d59 100644 --- a/apps/ios/Shared/FileUtils.swift +++ b/apps/ios/Shared/FileUtils.swift @@ -9,6 +9,9 @@ import Foundation import SwiftUI +// maximum image file size to be auto-accepted +let maxImageSize = 236700 + func getDocumentsDirectory() -> URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 01c9c68b05..524fd047b0 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -743,8 +743,7 @@ func processReceivedMsg(_ res: ChatResponse) { let cItem = aChatItem.chatItem chatModel.addChatItem(cInfo, cItem) if let file = cItem.file, - file.fileSize <= 236700 { - // file.fileSize <= 394500 { + file.fileSize <= maxImageSize { Task { do { try await receiveFile(fileId: file.fileId) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 00068617f6..708f92512b 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -14,19 +14,12 @@ struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat - @State var message: String = "" - @State var quotedItem: ChatItem? = nil - @State var editingItem: ChatItem? = nil - @State var linkPreview: LinkPreview? = nil + @State var composeState = ComposeState() @State var deletingItem: ChatItem? = nil - @State private var inProgress: Bool = false @FocusState private var keyboardVisible: Bool @State private var showChatInfo = false @State private var showDeleteMessage = false - @State private var chosenImage: UIImage? = nil - @State private var imagePreview: String? = nil - var body: some View { let cInfo = chat.chatInfo @@ -86,16 +79,9 @@ struct ChatView: View { Spacer(minLength: 0) ComposeView( - message: $message, - quotedItem: $quotedItem, - editingItem: $editingItem, - linkPreview: $linkPreview, - sendMessage: sendMessage, - resetMessage: { message = "" }, - inProgress: inProgress, - keyboardVisible: $keyboardVisible, - chosenImage: $chosenImage, - imagePreview: $imagePreview + chat: chat, + composeState: $composeState, + keyboardVisible: $keyboardVisible ) } .navigationTitle(cInfo.chatViewName) @@ -130,8 +116,7 @@ struct ChatView: View { if ci.isMsgContent() { Button { withAnimation { - editingItem = nil - quotedItem = ci + composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) } } label: { Label("Reply", systemImage: "arrowshape.turn.up.left") } Button { @@ -142,7 +127,9 @@ struct ChatView: View { showShareSheet(items: shareItems) } label: { Label("Share", systemImage: "square.and.arrow.up") } Button { - if case .image = ci.content.msgContent, let image = getStoredImage(ci.file) { + if case let .image(text, _) = ci.content.msgContent, + text == "", + let image = getStoredImage(ci.file) { UIPasteboard.general.image = image } else { UIPasteboard.general.string = ci.content.text @@ -151,9 +138,7 @@ struct ChatView: View { if ci.meta.editable { Button { withAnimation { - quotedItem = nil - editingItem = ci - message = ci.content.text + composeState = ComposeState(editingItem: ci) } } label: { Label("Edit", systemImage: "square.and.pencil") } } @@ -214,75 +199,6 @@ struct ChatView: View { } } } - - func sendMessage(_ text: String) { - logger.debug("ChatView sendMessage") - Task { - logger.debug("ChatView sendMessage: in Task") - do { - if let ei = editingItem { - let chatItem = try await apiUpdateChatItem( - type: chat.chatInfo.chatType, - id: chat.chatInfo.apiId, - itemId: ei.id, - msg: .text(text) - ) - DispatchQueue.main.async { - editingItem = nil - linkPreview = nil - let _ = chatModel.upsertChatItem(chat.chatInfo, chatItem) - } - } else { - let mc: MsgContent - var file: String? = nil - if let preview = imagePreview, - let uiImage = chosenImage, - let savedFile = saveImage(uiImage) { - mc = .image(text: text, image: preview) - file = savedFile - } else if let preview = linkPreview { - mc = .link(text: text, preview: preview) - } else { - mc = .text(text) - } - let chatItem = try await apiSendMessage( - type: chat.chatInfo.chatType, - id: chat.chatInfo.apiId, - file: file, - quotedItemId: quotedItem?.meta.itemId, - msg: mc - ) - DispatchQueue.main.async { - quotedItem = nil - linkPreview = nil - chosenImage = nil - imagePreview = nil - chatModel.addChatItem(chat.chatInfo, chatItem) - } - } - } catch { - logger.error("ChatView.sendMessage error: \(error.localizedDescription)") - } - } - } - - func saveImage(_ uiImage: UIImage) -> String? { - if let imageResized = resizeImageToDataSize(uiImage, maxDataSize: 160000), - let dataResized = Data(base64Encoded: dropImagePrefix(imageResized)), - let jpegData = UIImage(data: dataResized)?.jpegData(compressionQuality: 1) { - let millisecondsSince1970 = Int64((Date().timeIntervalSince1970 * 1000.0).rounded()) - let fileToSave = "image_\(millisecondsSince1970).jpg" - let filePath = getAppFilesDirectory().appendingPathComponent(fileToSave) - do { - try jpegData.write(to: filePath) - return fileToSave - } catch { - logger.error("ChatView.saveImage error: \(error.localizedDescription)") - return nil - } - } - return nil - } func deleteMessage(_ mode: CIDeleteMode) { logger.debug("ChatView deleteMessage") diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 62e9eec0dd..a73ed23140 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -8,43 +8,106 @@ import SwiftUI -// TODO -//enum ComposeState { -// case plain -// case quoted(quotedItem: ChatItem) -// case editing(editingItem: ChatItem) -//} +enum ComposePreview { + case noPreview + case linkPreview(linkPreview: LinkPreview) + case imagePreview(imagePreview: String) +} -//enum ReferencedItem { -// case none -// case quoted(quotedItem: ChatItem) -// case editing(editingItem: ChatItem) -//} -// -//enum Preview { -// case none -// case link(linkPreview: LinkPreview) -// case image(image: UIImage) -//} -// -//struct ComposeState { -// var quotedItem: ChatItem? = nil -// var editingItem: ChatItem? = nil -// var linkPreview: LinkPreview? = nil -//} +enum ComposeContextItem { + case noContextItem + case quotedItem(chatItem: ChatItem) + case editingItem(chatItem: ChatItem) +} + +struct ComposeState { + var message: String + var preview: ComposePreview + var contextItem: ComposeContextItem + var inProgress: Bool = false + + init( + message: String = "", + preview: ComposePreview = .noPreview, + contextItem: ComposeContextItem = .noContextItem + ) { + self.message = message + self.preview = preview + self.contextItem = contextItem + } + + init(editingItem: ChatItem) { + self.message = editingItem.content.text + self.preview = chatItemPreview(chatItem: editingItem) + self.contextItem = .editingItem(chatItem: editingItem) + } + + func copy( + message: String? = nil, + preview: ComposePreview? = nil, + contextItem: ComposeContextItem? = nil + ) -> ComposeState { + ComposeState( + message: message ?? self.message, + preview: preview ?? self.preview, + contextItem: contextItem ?? self.contextItem + ) + } + + func editing() -> Bool { + switch contextItem { + case .editingItem: return true + default: return false + } + } + + func sendEnabled() -> Bool { + switch preview { + case .imagePreview: + return true + default: + return !message.isEmpty + } + } + + func linkPreviewAllowed() -> Bool { + switch preview { + case .imagePreview: + return false + default: + return true + } + } + + func linkPreview() -> LinkPreview? { + switch preview { + case let .linkPreview(linkPreview): + return linkPreview + default: + return nil + } + } +} + +func chatItemPreview(chatItem: ChatItem) -> ComposePreview { + let chatItemPreview: ComposePreview + switch chatItem.content.msgContent { + case let .link(_, preview: preview): + chatItemPreview = .linkPreview(linkPreview: preview) + case let .image(_, image: image): + chatItemPreview = .imagePreview(imagePreview: image) + default: + chatItemPreview = .noPreview + } + return chatItemPreview +} struct ComposeView: View { - @Binding var message: String - @Binding var quotedItem: ChatItem? - @Binding var editingItem: ChatItem? - @Binding var linkPreview: LinkPreview? - - var sendMessage: (String) -> Void - var resetMessage: () -> Void - var inProgress: Bool = false + @EnvironmentObject var chatModel: ChatModel + let chat: Chat + @Binding var composeState: ComposeState @FocusState.Binding var keyboardVisible: Bool - @State var editing: Bool = false - @State var sendEnabled: Bool = false + @State var linkUrl: URL? = nil @State var prevLinkUrl: URL? = nil @State var pendingLinkUrl: URL? = nil @@ -53,59 +116,43 @@ struct ComposeView: View { @State private var showChooseSource = false @State private var showImagePicker = false @State private var imageSource: ImageSource = .imageLibrary - @Binding var chosenImage: UIImage? - @Binding var imagePreview: String? + @State var chosenImage: UIImage? var body: some View { VStack(spacing: 0) { - if let metadata = imagePreview { - ComposeImageView(image: metadata, cancelImage: nil) - } else if let metadata = linkPreview { - ComposeLinkView(linkPreview: metadata, cancelPreview: cancelPreview) - } - if (quotedItem != nil) { - ContextItemView(contextItem: $quotedItem, editing: $editing) - } else if (editingItem != nil) { - ContextItemView(contextItem: $editingItem, editing: $editing, resetMessage: resetMessage) - } - HStack{ -// Button { -// showChooseSource = true -// } label: { -// Image(systemName: "paperclip") -// .resizable() -// } -// .disabled(editingItem != nil) -// .frame(width: 25, height: 25) -// .padding(.vertical, 4) -// .padding(.leading, 12) + contextItemView() + previewView() + HStack (alignment: .bottom) { + Button { + showChooseSource = true + } label: { + Image(systemName: "paperclip") + .resizable() + } + .disabled(composeState.editing()) + .frame(width: 25, height: 25) + .padding(.bottom, 12) + .padding(.leading, 12) SendMessageView( + composeState: $composeState, sendMessage: { text in sendMessage(text) resetLinkPreview() }, - inProgress: inProgress, - message: $message, - keyboardVisible: $keyboardVisible, - editing: $editing, - sendEnabled: $sendEnabled + keyboardVisible: $keyboardVisible ) - .padding(.horizontal, 12) -// // use this padding when attach button is uncommented -// .padding(.trailing, 12) + .padding(.trailing, 12) .background(.background) } } - .onChange(of: message) { _ in - if message.count > 0 { - showLinkPreview(message) - } else { - resetLinkPreview() + .onChange(of: composeState.message) { _ in + if composeState.linkPreviewAllowed() { + if composeState.message.count > 0 { + showLinkPreview(composeState.message) + } else { + resetLinkPreview() + } } - sendEnabled = (imagePreview != nil || !message.isEmpty) - } - .onChange(of: editingItem == nil) { _ in - editing = (editingItem != nil) } .confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { @@ -128,22 +175,133 @@ struct ComposeView: View { } } .onChange(of: chosenImage) { image in - if let image = image { - imagePreview = resizeImageToDataSize(image, maxDataSize: 12500) + if let image = image, + let imagePreview = resizeImageToStrSize(image, maxDataSize: 14000) { + composeState = composeState.copy(preview: .imagePreview(imagePreview: imagePreview)) } else { - imagePreview = nil + composeState = composeState.copy(preview: .noPreview) } } - .onChange(of: imagePreview) { _ in - sendEnabled = (imagePreview != nil || !message.isEmpty) + } + + @ViewBuilder func previewView() -> some View { + switch composeState.preview { + case .noPreview: + EmptyView() + case let .linkPreview(linkPreview: preview): + ComposeLinkView(linkPreview: preview, cancelPreview: cancelLinkPreview) + case let .imagePreview(imagePreview: img): + ComposeImageView( + image: img, + cancelImage: { composeState = composeState.copy(preview: .noPreview) }, + cancelEnabled: !composeState.editing()) } } + @ViewBuilder private func contextItemView() -> some View { + switch composeState.contextItem { + case .noContextItem: + EmptyView() + case let .quotedItem(chatItem: quotedItem): + ContextItemView(contextItem: quotedItem, cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }) + case let .editingItem(chatItem: editingItem): + ContextItemView(contextItem: editingItem, cancelContextItem: { composeState = ComposeState()}) + } + } + + private func sendMessage(_ text: String) { + logger.debug("ChatView sendMessage") + Task { + logger.debug("ChatView sendMessage: in Task") + do { + switch composeState.contextItem { + case let .editingItem(chatItem: ei): + if let oldMsgContent = ei.content.msgContent { + let chatItem = try await apiUpdateChatItem( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + itemId: ei.id, + msg: updateMsgContent(oldMsgContent) + ) + DispatchQueue.main.async { + let _ = self.chatModel.upsertChatItem(self.chat.chatInfo, chatItem) + } + } + default: + var mc: MsgContent? = nil + var file: String? = nil + switch (composeState.preview) { + case .noPreview: + mc = .text(composeState.message) + case .linkPreview: + mc = checkLinkPreview() + case let .imagePreview(imagePreview: image): + if let uiImage = chosenImage, + let savedFile = saveImage(uiImage) { + mc = .image(text: composeState.message, image: image) + file = savedFile + } + } + + var quotedItemId: Int64? = nil + switch (composeState.contextItem) { + case let .quotedItem(chatItem: quotedItem): + quotedItemId = quotedItem.id + default: + quotedItemId = nil + } + if let mc = mc { + let chatItem = try await apiSendMessage( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + file: file, + quotedItemId: quotedItemId, + msg: mc + ) + chatModel.addChatItem(chat.chatInfo, chatItem) + } + } + composeState = ComposeState() + } catch { + logger.error("ChatView.sendMessage error: \(error.localizedDescription)") + } + } + } + + private func updateMsgContent(_ msgContent: MsgContent) -> MsgContent { + switch msgContent { + case .text: + return checkLinkPreview() + case .link: + return checkLinkPreview() + case .image(_, let image): + return .image(text: composeState.message, image: image) + case .unknown(let type, _): + return .unknown(type: type, text: composeState.message) + } + } + + private func saveImage(_ uiImage: UIImage) -> String? { + if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: maxImageSize) { + let millisecondsSince1970 = Int64((Date().timeIntervalSince1970 * 1000.0).rounded()) + let fileToSave = "image_\(millisecondsSince1970).jpg" + let filePath = getAppFilesDirectory().appendingPathComponent(fileToSave) + do { + try imageDataResized.write(to: filePath) + return fileToSave + } catch { + logger.error("ChatView.saveImage error: \(error.localizedDescription)") + return nil + } + } + return nil + } + private func showLinkPreview(_ s: String) { prevLinkUrl = linkUrl linkUrl = parseMessage(s) if let url = linkUrl { - if url != linkPreview?.uri && url != pendingLinkUrl { + if url != composeState.linkPreview()?.uri && url != pendingLinkUrl { pendingLinkUrl = url if prevLinkUrl == url { loadLinkPreview(url) @@ -154,7 +312,7 @@ struct ComposeView: View { } } } else { - linkPreview = nil + composeState = composeState.copy(preview: .noPreview) } } @@ -162,7 +320,7 @@ struct ComposeView: View { do { let parsedMsg = try apiParseMarkdown(text: msg) let uri = parsedMsg?.first(where: { ft in - ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) + ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) }) if let uri = uri { return URL(string: uri.text) } else { return nil } @@ -176,18 +334,19 @@ struct ComposeView: View { link.starts(with: "https://simplex.chat") || link.starts(with: "http://simplex.chat") } - private func cancelPreview() { - if let uri = linkPreview?.uri.absoluteString { + private func cancelLinkPreview() { + if let uri = composeState.linkPreview()?.uri.absoluteString { cancelledLinks.insert(uri) } - linkPreview = nil + composeState = composeState.copy(preview: .noPreview) } private func loadLinkPreview(_ url: URL) { if pendingLinkUrl == url { - getLinkPreview(url: url) { lp in - if pendingLinkUrl == url { - linkPreview = lp + getLinkPreview(url: url) { linkPreview in + if let linkPreview = linkPreview, + pendingLinkUrl == url { + composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) pendingLinkUrl = nil } } @@ -200,40 +359,38 @@ struct ComposeView: View { pendingLinkUrl = nil cancelledLinks = [] } + + private func checkLinkPreview() -> MsgContent { + switch (composeState.preview) { + case let .linkPreview(linkPreview: linkPreview): + if let url = parseMessage(composeState.message), + url == linkPreview.uri { + return .link(text: composeState.message, preview: linkPreview) + } else { + return .text(composeState.message) + } + default: + return .text(composeState.message) + } + } } struct ComposeView_Previews: PreviewProvider { static var previews: some View { - @State var message: String = "" + let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) + @State var composeState = ComposeState(message: "hello") @FocusState var keyboardVisible: Bool - @State var item: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") - @State var nilItem: ChatItem? = nil - @State var linkPreview: LinkPreview? = nil - @State var chosenImage: UIImage? = nil - @State var imagePreview: String? = nil return Group { ComposeView( - message: $message, - quotedItem: $item, - editingItem: $nilItem, - linkPreview: $linkPreview, - sendMessage: { print ($0) }, - resetMessage: {}, - keyboardVisible: $keyboardVisible, - chosenImage: $chosenImage, - imagePreview: $imagePreview + chat: chat, + composeState: $composeState, + keyboardVisible: $keyboardVisible ) ComposeView( - message: $message, - quotedItem: $nilItem, - editingItem: $item, - linkPreview: $linkPreview, - sendMessage: { print ($0) }, - resetMessage: {}, - keyboardVisible: $keyboardVisible, - chosenImage: $chosenImage, - imagePreview: $imagePreview + chat: chat, + composeState: $composeState, + keyboardVisible: $keyboardVisible ) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index d853e17026..f7ebb1e53f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -10,31 +10,25 @@ import SwiftUI struct ContextItemView: View { @Environment(\.colorScheme) var colorScheme - @Binding var contextItem: ChatItem? - @Binding var editing: Bool - var resetMessage: () -> Void = {} + let contextItem: ChatItem + let cancelContextItem: () -> Void var body: some View { - if let cxtItem = contextItem { - HStack { - contextText(cxtItem).lineLimit(3) - Spacer() - Button { - withAnimation { - contextItem = nil - if editing { resetMessage() } - } - } label: { - Image(systemName: "multiply") + HStack { + contextText(contextItem).lineLimit(3) + Spacer() + Button { + withAnimation { + cancelContextItem() } + } label: { + Image(systemName: "multiply") } - .padding(12) - .frame(maxWidth: .infinity) - .background(chatItemFrameColor(cxtItem, colorScheme)) - .padding(.top, 8) - } else { - EmptyView() } + .padding(12) + .frame(maxWidth: .infinity) + .background(chatItemFrameColor(contextItem, colorScheme)) + .padding(.top, 8) } func contextText(_ cxtItem: ChatItem) -> some View { @@ -48,8 +42,7 @@ struct ContextItemView: View { struct ContextItemView_Previews: PreviewProvider { static var previews: some View { - @State var contextItem: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") - @State var editing: Bool = false - return ContextItemView(contextItem: $contextItem, editing: $editing) + let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello") + return ContextItemView(contextItem: contextItem, cancelContextItem: {}) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index bede2e2c03..3c4663f504 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -9,13 +9,10 @@ import SwiftUI struct SendMessageView: View { + @Binding var composeState: ComposeState var sendMessage: (String) -> Void - var inProgress: Bool = false - @Binding var message: String @Namespace var namespace @FocusState.Binding var keyboardVisible: Bool - @Binding var editing: Bool - @Binding var sendEnabled: Bool @State private var teHeight: CGFloat = 42 @State private var teFont: Font = .body var maxHeight: CGFloat = 360 @@ -25,15 +22,15 @@ struct SendMessageView: View { ZStack { HStack(alignment: .bottom) { ZStack(alignment: .leading) { - Text(message) + Text(composeState.message) + .lineLimit(10) .font(teFont) .foregroundColor(.clear) .padding(.horizontal, 10) .padding(.vertical, 8) .matchedGeometryEffect(id: "te", in: namespace) .background(GeometryReader(content: updateHeight)) - TextEditor(text: $message) - .onSubmit(submit) + TextEditor(text: $composeState.message) .focused($keyboardVisible) .font(teFont) .textInputAutocapitalization(.sentences) @@ -42,18 +39,18 @@ struct SendMessageView: View { .frame(height: teHeight) } - if (inProgress) { + if (composeState.inProgress) { ProgressView() .scaleEffect(1.4) .frame(width: 31, height: 31, alignment: .center) .padding([.bottom, .trailing], 3) } else { - Button(action: submit) { - Image(systemName: editing ? "checkmark.circle.fill" : "arrow.up.circle.fill") + Button(action: { sendMessage(composeState.message) }) { + Image(systemName: composeState.editing() ? "checkmark.circle.fill" : "arrow.up.circle.fill") .resizable() .foregroundColor(.accentColor) } - .disabled(!sendEnabled) + .disabled(!composeState.sendEnabled()) .frame(width: 29, height: 29) .padding([.bottom, .trailing], 4) } @@ -66,16 +63,11 @@ struct SendMessageView: View { .padding(.vertical, 8) } - func submit() { - sendMessage(message) - message = "" - } - func updateHeight(_ g: GeometryProxy) -> Color { DispatchQueue.main.async { teHeight = min(max(g.frame(in: .local).size.height, minHeight), maxHeight) - teFont = isShortEmoji(message) - ? message.count < 4 + teFont = isShortEmoji(composeState.message) + ? composeState.message.count < 4 ? largeEmojiFont : mediumEmojiFont : .body @@ -86,34 +78,29 @@ struct SendMessageView: View { struct SendMessageView_Previews: PreviewProvider { static var previews: some View { - @State var message: String = "" + @State var composeStateNew = ComposeState() + let ci = ChatItem.getSample(1, .directSnd, .now, "hello") + @State var composeStateEditing = ComposeState(editingItem: ci) @FocusState var keyboardVisible: Bool - @State var editingOff: Bool = false - @State var editingOn: Bool = true @State var sendEnabled: Bool = true - @State var item: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") return Group { VStack { Text("") Spacer(minLength: 0) SendMessageView( + composeState: $composeStateNew, sendMessage: { print ($0) }, - message: $message, - keyboardVisible: $keyboardVisible, - editing: $editingOff, - sendEnabled: $sendEnabled + keyboardVisible: $keyboardVisible ) } VStack { Text("") Spacer(minLength: 0) SendMessageView( + composeState: $composeStateEditing, sendMessage: { print ($0) }, - message: $message, - keyboardVisible: $keyboardVisible, - editing: $editingOn, - sendEnabled: $sendEnabled + keyboardVisible: $keyboardVisible ) } } diff --git a/apps/ios/Shared/Views/Helpers/ComposeImageView.swift b/apps/ios/Shared/Views/Helpers/ComposeImageView.swift index 2d083c9721..c23de72229 100644 --- a/apps/ios/Shared/Views/Helpers/ComposeImageView.swift +++ b/apps/ios/Shared/Views/Helpers/ComposeImageView.swift @@ -11,7 +11,8 @@ import SwiftUI struct ComposeImageView: View { @Environment(\.colorScheme) var colorScheme let image: String - var cancelImage: (() -> Void)? = nil + let cancelImage: (() -> Void) + let cancelEnabled: Bool var body: some View { HStack(alignment: .center, spacing: 8) { @@ -20,9 +21,10 @@ struct ComposeImageView: View { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) - .frame(maxWidth: 80, maxHeight: 60) + .frame(maxWidth: 80, minHeight: 40, maxHeight: 60) } - if let cancelImage = cancelImage { + Spacer() + if cancelEnabled { Button { cancelImage() } label: { Image(systemName: "multiply") } @@ -35,3 +37,30 @@ struct ComposeImageView: View { .padding(.top, 8) } } + +//struct ComposeImageView: View { +// @Environment(\.colorScheme) var colorScheme +// let image: String +// let cancelImage: (() -> Void) +// +// var body: some View { +// if let data = Data(base64Encoded: dropImagePrefix(image)), +// let uiImage = UIImage(data: data) { +// HStack(alignment: .center) { +// ZStack(alignment: .topTrailing) { +// Image(uiImage: uiImage) +// .resizable() +// .scaledToFit() +// .cornerRadius(20) +// .frame(maxHeight: 150) +// Button { cancelImage() } label: { +// Image(systemName: "multiply") +// .foregroundColor(.white) +// } +// .padding(8) +// } +// } +// .padding(.top, 8) +// } +// } +//} diff --git a/apps/ios/Shared/Views/Helpers/ComposeLinkView.swift b/apps/ios/Shared/Views/Helpers/ComposeLinkView.swift index 1a9c446499..6ab5e49980 100644 --- a/apps/ios/Shared/Views/Helpers/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Helpers/ComposeLinkView.swift @@ -25,7 +25,7 @@ func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)") } else { if let image = object as? UIImage, - let resized = resizeImageToDataSize(image, maxDataSize: 14000), + let resized = resizeImageToStrSize(image, maxDataSize: 14000), let title = metadata.title, let uri = metadata.originalURL { linkPreview = LinkPreview(uri: uri, title: title, image: resized) diff --git a/apps/ios/Shared/Views/Helpers/ImagePicker.swift b/apps/ios/Shared/Views/Helpers/ImagePicker.swift index 7fa0bc722b..06c4825c6a 100644 --- a/apps/ios/Shared/Views/Helpers/ImagePicker.swift +++ b/apps/ios/Shared/Views/Helpers/ImagePicker.swift @@ -46,28 +46,43 @@ func reduceSize(_ image: UIImage, ratio: CGFloat) -> UIImage { return resizeImage(image, newBounds: bounds, drawIn: bounds) } -func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int) -> String? { +func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int) -> String? { var img = image - var str = compressImage(img) + var str = compressImageStr(img) var dataSize = str?.count ?? 0 while dataSize != 0 && dataSize > maxDataSize { let ratio = sqrt(Double(dataSize) / Double(maxDataSize)) let clippedRatio = min(ratio, 2.0) img = reduceSize(img, ratio: clippedRatio) - str = compressImage(img) + str = compressImageStr(img) dataSize = str?.count ?? 0 } - logger.debug("resizeImageToDataSize final \(dataSize)") + logger.debug("resizeImageToStrSize final \(dataSize)") return str } -func compressImage(_ image: UIImage, _ compressionQuality: CGFloat = 0.85) -> String? { +func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85) -> String? { if let data = image.jpegData(compressionQuality: compressionQuality) { return "data:image/jpg;base64,\(data.base64EncodedString())" } return nil } +func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int) -> Data? { + var img = image + var data = img.jpegData(compressionQuality: 0.85) + var dataSize = data?.count ?? 0 + while dataSize != 0 && dataSize > maxDataSize { + let ratio = sqrt(Double(dataSize) / Double(maxDataSize)) + let clippedRatio = min(ratio, 2.0) + img = reduceSize(img, ratio: clippedRatio) + data = img.jpegData(compressionQuality: 0.85) + dataSize = data?.count ?? 0 + } + logger.debug("resizeImageToDataSize final \(dataSize)") + return data +} + enum ImageSource { case imageLibrary case camera diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 98d7bbd39d..56cc0bb8c9 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -14,11 +14,8 @@ private let maxItemSize: Int = 50000 struct TerminalView: View { @EnvironmentObject var chatModel: ChatModel - @State var inProgress: Bool = false - @State var message: String = "" + @State var composeState: ComposeState = ComposeState() @FocusState private var keyboardVisible: Bool - @State var editing: Bool = false - @State var sendEnabled: Bool = false var body: some View { VStack { @@ -64,21 +61,15 @@ struct TerminalView: View { Spacer() SendMessageView( + composeState: $composeState, sendMessage: sendMessage, - inProgress: inProgress, - message: $message, - keyboardVisible: $keyboardVisible, - editing: $editing, - sendEnabled: $sendEnabled + keyboardVisible: $keyboardVisible ) .padding(.horizontal, 12) } } .navigationViewStyle(.stack) .navigationTitle("Chat console") - .onChange(of: message) { _ in - sendEnabled = !message.isEmpty - } } func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) { @@ -93,11 +84,12 @@ struct TerminalView: View { let cmd = ChatCommand.string(cmdStr) DispatchQueue.global().async { Task { - inProgress = true + composeState.inProgress = true _ = await chatSendCmd(cmd) - inProgress = false + composeState.inProgress = false } } + composeState = ComposeState() } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 8c38681b3c..4de7695136 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -99,7 +99,7 @@ struct UserProfile: View { } .onChange(of: chosenImage) { image in if let image = image { - profile.image = resizeImageToDataSize(cropToSquare(image), maxDataSize: 12500) + profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) } else { profile.image = nil }