diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 2549dd5..30e2c85 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -3500,15 +3500,101 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { return } + val recipient = opponentKey + val sender = myPublicKey + val privateKey = myPrivateKey val context = getApplication() val groupDebugId = UUID.randomUUID().toString().replace("-", "").take(8) + + if (recipient == null || sender == null || privateKey == null) { + ProtocolManager.addLog( + "❌ IMG-GROUP $groupDebugId | aborted: missing keys (recipient=${recipient != null}, sender=${sender != null}, private=${privateKey != null})" + ) + return + } + if (isSending) { + ProtocolManager.addLog("⚠️ IMG-GROUP $groupDebugId | skipped: another send in progress") + return + } + + isSending = true + + val messageId = UUID.randomUUID().toString().replace("-", "").take(32) + val timestamp = System.currentTimeMillis() + val text = caption.trim() + val attachmentIds = imageUris.indices.map { index -> "img_${timestamp}_$index" } + + val optimisticAttachments = + imageUris.mapIndexed { index, uri -> + MessageAttachment( + id = attachmentIds[index], + blob = "", + type = AttachmentType.IMAGE, + preview = "", + width = 0, + height = 0, + localUri = uri.toString() + ) + } + + addMessageSafely( + ChatMessage( + id = messageId, + text = text, + isOutgoing = true, + timestamp = Date(timestamp), + status = MessageStatus.SENDING, + attachments = optimisticAttachments + ) + ) + _inputText.value = "" + ProtocolManager.addLog( "📸 IMG-GROUP $groupDebugId | prepare start: count=${imageUris.size}, captionLen=${caption.trim().length}" ) backgroundUploadScope.launch { + try { + val optimisticAttachmentsJson = + JSONArray().apply { + imageUris.forEachIndexed { index, uri -> + put( + JSONObject().apply { + put("id", attachmentIds[index]) + put("type", AttachmentType.IMAGE.value) + put("preview", "") + put("blob", "") + put("width", 0) + put("height", 0) + put("localUri", uri.toString()) + } + ) + } + }.toString() + + saveMessageToDatabase( + messageId = messageId, + text = text, + encryptedContent = "", + encryptedKey = "", + timestamp = timestamp, + isFromMe = true, + delivered = 0, + attachmentsJson = optimisticAttachmentsJson, + opponentPublicKey = recipient + ) + + saveDialog( + lastMessage = if (text.isNotEmpty()) text else "📷 ${imageUris.size} photos", + timestamp = timestamp, + opponentPublicKey = recipient + ) + } catch (_: Exception) { + ProtocolManager.addLog("⚠️ IMG-GROUP $groupDebugId | optimistic DB save skipped (non-fatal)") + } + val preparedImages = - imageUris.mapIndexedNotNull { index, uri -> + imageUris.mapIndexed { index, uri -> val (width, height) = com.rosetta.messenger.utils.MediaUtils.getImageDimensions( context, @@ -3523,7 +3609,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { ProtocolManager.addLog( "❌ IMG-GROUP $groupDebugId | item#$index base64 conversion failed" ) - return@mapIndexedNotNull null + throw IllegalStateException( + "group item#$index base64 conversion failed" + ) } val blurhash = com.rosetta.messenger.utils.MediaUtils.generateBlurhash( @@ -3533,26 +3621,156 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { ProtocolManager.addLog( "📸 IMG-GROUP $groupDebugId | item#$index prepared: ${width}x$height, base64Len=${imageBase64.length}, blurhashLen=${blurhash.length}" ) - ImageData( - base64 = imageBase64, - blurhash = blurhash, - width = width, - height = height - ) + index to + ImageData( + base64 = imageBase64, + blurhash = blurhash, + width = width, + height = height + ) } if (preparedImages.isEmpty()) { ProtocolManager.addLog( "❌ IMG-GROUP $groupDebugId | no prepared images, send canceled" ) + updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value) + withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.ERROR) } + isSending = false return@launch } ProtocolManager.addLog( "📸 IMG-GROUP $groupDebugId | prepare done: ready=${preparedImages.size}" ) - withContext(Dispatchers.Main) { - sendImageGroup(preparedImages, caption) + try { + val groupStartedAt = System.currentTimeMillis() + val encryptionContext = + buildEncryptionContext( + plaintext = text, + recipient = recipient, + privateKey = privateKey + ) ?: throw IllegalStateException("Cannot resolve chat encryption context") + val encryptedContent = encryptionContext.encryptedContent + val encryptedKey = encryptionContext.encryptedKey + val aesChachaKey = encryptionContext.aesChachaKey + val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey) + val isSavedMessages = sender == recipient + + val networkAttachments = mutableListOf() + val finalDbAttachments = JSONArray() + val finalAttachmentsById = mutableMapOf() + + for ((originalIndex, imageData) in preparedImages) { + val attachmentId = attachmentIds[originalIndex] + val encryptedImageBlob = + encryptAttachmentPayload(imageData.base64, encryptionContext) + val uploadTag = + if (!isSavedMessages) { + TransportManager.uploadFile(attachmentId, encryptedImageBlob) + } else { + "" + } + val previewWithTag = + if (uploadTag.isNotEmpty()) "$uploadTag::${imageData.blurhash}" + else imageData.blurhash + + AttachmentFileManager.saveAttachment( + context = context, + blob = imageData.base64, + attachmentId = attachmentId, + publicKey = sender, + privateKey = privateKey + ) + + val finalAttachment = + MessageAttachment( + id = attachmentId, + blob = if (uploadTag.isNotEmpty()) "" else encryptedImageBlob, + type = AttachmentType.IMAGE, + preview = previewWithTag, + width = imageData.width, + height = imageData.height, + localUri = "" + ) + networkAttachments.add(finalAttachment) + finalAttachmentsById[attachmentId] = finalAttachment + + finalDbAttachments.put( + JSONObject().apply { + put("id", attachmentId) + put("type", AttachmentType.IMAGE.value) + put("preview", previewWithTag) + put("blob", "") + put("width", imageData.width) + put("height", imageData.height) + } + ) + } + + val packet = + PacketMessage().apply { + fromPublicKey = sender + toPublicKey = recipient + content = encryptedContent + chachaKey = encryptedKey + this.aesChachaKey = aesChachaKey + this.timestamp = timestamp + this.privateKey = privateKeyHash + this.messageId = messageId + attachments = networkAttachments + } + + if (!isSavedMessages) { + ProtocolManager.send(packet) + } + + updateMessageStatusAndAttachmentsInDb( + messageId = messageId, + delivered = 1, + attachmentsJson = finalDbAttachments.toString() + ) + + withContext(Dispatchers.Main) { + _messages.value = + _messages.value.map { msg -> + if (msg.id != messageId) return@map msg + msg.copy( + status = MessageStatus.SENT, + attachments = + msg.attachments.map { current -> + val final = finalAttachmentsById[current.id] + if (final != null) { + current.copy( + preview = final.preview, + width = final.width, + height = final.height, + localUri = "" + ) + } else { + current.copy(localUri = "") + } + } + ) + } + updateCacheFromCurrentMessages() + } + + saveDialog( + lastMessage = if (text.isNotEmpty()) text else "📷 ${imageUris.size} photos", + timestamp = timestamp, + opponentPublicKey = recipient + ) + logPhotoPipeline( + messageId, + "group-from-uri completed; totalElapsed=${System.currentTimeMillis() - groupStartedAt}ms" + ) + } catch (e: Exception) { + logPhotoPipelineError(messageId, "group-from-uri", e) + updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value) + withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.ERROR) } + } finally { + isSending = false } } }