From fbab2d0f80241a7a8452293d1e1ae8e4c33f36e6 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Fri, 16 Jan 2026 03:38:33 +0500 Subject: [PATCH] feat: Enhance ForwardManager and ChatViewModel for improved message handling and logging; adjust ForwardChatPickerBottomSheet height for better UX --- .../rosetta/messenger/data/ForwardManager.kt | 18 +- .../messenger/ui/chats/ChatViewModel.kt | 167 +++++++++++++----- .../ui/chats/ForwardChatPickerBottomSheet.kt | 2 +- 3 files changed, 144 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/data/ForwardManager.kt b/app/src/main/java/com/rosetta/messenger/data/ForwardManager.kt index 84653ae..d7b15f0 100644 --- a/app/src/main/java/com/rosetta/messenger/data/ForwardManager.kt +++ b/app/src/main/java/com/rosetta/messenger/data/ForwardManager.kt @@ -103,6 +103,22 @@ object ForwardManager { */ fun hasForwardMessagesForChat(publicKey: String): Boolean { val selectedKey = _selectedChatPublicKey.value - return selectedKey == publicKey && _forwardMessages.value.isNotEmpty() + val hasMessages = _forwardMessages.value.isNotEmpty() + android.util.Log.d("ForwardManager", "📨 hasForwardMessagesForChat($publicKey): selectedKey=$selectedKey, hasMessages=$hasMessages") + return selectedKey == publicKey && hasMessages + } + + /** + * Установить выбранный чат и вернуть сообщения для него + * Комбинированный метод для атомарного получения данных + */ + fun getForwardMessagesForChat(publicKey: String): List { + val selectedKey = _selectedChatPublicKey.value + return if (selectedKey == publicKey && _forwardMessages.value.isNotEmpty()) { + android.util.Log.d("ForwardManager", "📨 getForwardMessagesForChat: returning ${_forwardMessages.value.size} messages") + _forwardMessages.value + } else { + emptyList() + } } } 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 fd3973a..21cecb8 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 @@ -355,6 +355,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { opponentTitle = title opponentUsername = username + // 📨 СНАЧАЛА проверяем ForwardManager - ДО сброса состояния! + // Это важно для правильного отображения forward сообщений сразу + val forwardMessages = ForwardManager.getForwardMessagesForChat(publicKey) + val hasForward = forwardMessages.isNotEmpty() + if (hasForward) { + android.util.Log.d("ChatViewModel", "📨 Will apply ${forwardMessages.size} forward messages") + } + // Сбрасываем состояние _messages.value = emptyList() _opponentOnline.value = false @@ -366,25 +374,27 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { readReceiptSentForCurrentDialog = false isDialogActive = true // 🔥 Диалог активен! - // 📨 Проверяем ForwardManager - если есть сообщения для пересылки в этот чат - if (ForwardManager.hasForwardMessagesForChat(publicKey)) { - val forwardMessages = ForwardManager.consumeForwardMessages() - if (forwardMessages.isNotEmpty()) { - android.util.Log.d("ChatViewModel", "📨 Received ${forwardMessages.size} forward messages") - // Конвертируем ForwardMessage в ReplyMessage - _replyMessages.value = forwardMessages.map { fm -> - ReplyMessage( - messageId = fm.messageId, - text = fm.text, - timestamp = fm.timestamp, - isOutgoing = fm.isOutgoing, - publicKey = fm.senderPublicKey - ) - } - _isForwardMode.value = true - // Очищаем ForwardManager после применения - ForwardManager.clear() + // 📨 Применяем Forward сообщения СРАЗУ после сброса + if (hasForward) { + android.util.Log.d("ChatViewModel", "📨 Applying ${forwardMessages.size} forward messages NOW") + // Конвертируем ForwardMessage в ReplyMessage + _replyMessages.value = forwardMessages.map { fm -> + ReplyMessage( + messageId = fm.messageId, + text = fm.text, + timestamp = fm.timestamp, + isOutgoing = fm.isOutgoing, + publicKey = fm.senderPublicKey + ) } + _isForwardMode.value = true + android.util.Log.d("ChatViewModel", "📨 isForwardMode set to TRUE, replyMessages.size = ${_replyMessages.value.size}") + // Очищаем ForwardManager после применения + ForwardManager.clear() + } else { + // Сбрасываем forward state если нет forward сообщений + _replyMessages.value = emptyList() + _isForwardMode.value = false } // Подписываемся на онлайн статус @@ -608,6 +618,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { * Как в архиве: расшифровываем при каждой загрузке */ private suspend fun entityToChatMessage(entity: MessageEntity): ChatMessage { + android.util.Log.d("ReplyDebug", "🔄 [DB LOAD] entityToChatMessage called") + android.util.Log.d("ReplyDebug", " - messageId: ${entity.messageId}") + android.util.Log.d("ReplyDebug", " - attachments: ${entity.attachments}") + android.util.Log.d("ReplyDebug", " - attachments length: ${entity.attachments.length}") + // Расшифровываем сообщение из content + chachaKey var displayText = try { val privateKey = myPrivateKey @@ -645,24 +660,36 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } } + android.util.Log.d("ReplyDebug", " - Decrypted text: ${displayText.take(50)}") + // Парсим attachments для поиска MESSAGES (цитата) // 🔥 ВАЖНО: Передаем content и chachaKey для расшифровки reply blob если нужно + android.util.Log.d("ReplyDebug", " - Calling parseReplyFromAttachments...") var replyData = parseReplyFromAttachments( attachmentsJson = entity.attachments, isFromMe = entity.fromMe == 1, content = entity.content, chachaKey = entity.chachaKey ) + android.util.Log.d("ReplyDebug", " - parseReplyFromAttachments returned: ${if (replyData != null) "NOT NULL" else "NULL"}") // Если не нашли reply в attachments, пробуем распарсить из текста if (replyData == null) { + android.util.Log.d("ReplyDebug", " - Reply is null, trying to parse from text...") val parseResult = parseReplyFromText(displayText) if (parseResult != null) { + android.util.Log.d("ReplyDebug", " - ✅ Parsed reply from text") replyData = parseResult.first displayText = parseResult.second + } else { + android.util.Log.d("ReplyDebug", " - ❌ Failed to parse reply from text") } + } else { + android.util.Log.d("ReplyDebug", " - ✅ Reply found in attachments!") } + android.util.Log.d("ReplyDebug", " - Creating ChatMessage with replyData: ${if (replyData != null) "NOT NULL" else "NULL"}") + return ChatMessage( id = entity.messageId, text = displayText, @@ -714,74 +741,132 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { content: String, chachaKey: String ): ReplyData? { - if (attachmentsJson.isEmpty() || attachmentsJson == "[]") return null + android.util.Log.d("ReplyDebug", "🔍 [DB LOAD] parseReplyFromAttachments called") + android.util.Log.d("ReplyDebug", " - attachmentsJson.isEmpty(): ${attachmentsJson.isEmpty()}") + android.util.Log.d("ReplyDebug", " - attachmentsJson == '[]': ${attachmentsJson == "[]"}") + + if (attachmentsJson.isEmpty() || attachmentsJson == "[]") { + android.util.Log.d("ReplyDebug", " - Early return: attachments empty or []") + return null + } return try { - android.util.Log.d("ReplyDebug", "💾 [DB] Parsing reply from attachments JSON") - android.util.Log.d("ReplyDebug", " - Attachments JSON preview: ${attachmentsJson.take(200)}") + android.util.Log.d("ReplyDebug", "💾 [DB LOAD] Parsing reply from attachments JSON") + android.util.Log.d("ReplyDebug", " - Full JSON: $attachmentsJson") + android.util.Log.d("ReplyDebug", " - JSON length: ${attachmentsJson.length}") val attachments = JSONArray(attachmentsJson) android.util.Log.d("ReplyDebug", " - Attachments count: ${attachments.length()}") + for (i in 0 until attachments.length()) { + android.util.Log.d("ReplyDebug", " - Processing attachment $i...") val attachment = attachments.getJSONObject(i) val type = attachment.optInt("type", 0) - android.util.Log.d("ReplyDebug", " - Attachment $i type: $type") + android.util.Log.d("ReplyDebug", " - type: $type") // MESSAGES = 1 (цитата) if (type == 1) { + android.util.Log.d("ReplyDebug", " - ✅ Found MESSAGES type!") + // Данные могут быть в blob или preview - var dataJson = attachment.optString("blob", "").ifEmpty { - attachment.optString("preview", "") + var dataJson = attachment.optString("blob", "") + android.util.Log.d("ReplyDebug", " - blob from JSON: ${if (dataJson.isEmpty()) "EMPTY" else "length=${dataJson.length}"}") + + if (dataJson.isEmpty()) { + dataJson = attachment.optString("preview", "") + android.util.Log.d("ReplyDebug", " - blob was empty, trying preview: ${if (dataJson.isEmpty()) "ALSO EMPTY" else "length=${dataJson.length}"}") + } + + android.util.Log.d("ReplyDebug", " - Final data length: ${dataJson.length}") + android.util.Log.d("ReplyDebug", " - Data preview (first 200): ${dataJson.take(200)}") + + if (dataJson.isEmpty()) { + android.util.Log.e("ReplyDebug", " - ❌ Data is empty, skipping") + continue } - android.util.Log.d("ReplyDebug", " - Found MESSAGES attachment, data length: ${dataJson.length}") - android.util.Log.d("ReplyDebug", " - Data preview: ${dataJson.take(200)}") - if (dataJson.isEmpty()) continue // 🔥 Проверяем формат blob - если содержит ":", то это зашифрованный формат "iv:ciphertext" - // Расшифровываем с приватным ключом (как в Desktop Архиве) + val colonCount = dataJson.count { it == ':' } + android.util.Log.d("ReplyDebug", " - Colon count in data: $colonCount") + if (dataJson.contains(":") && dataJson.split(":").size == 2) { - android.util.Log.d("ReplyDebug", " - Blob is encrypted, decrypting with private key...") + android.util.Log.d("ReplyDebug", " - 🔒 Blob is ENCRYPTED (iv:ciphertext format)") + android.util.Log.d("ReplyDebug", " - Attempting to decrypt with private key...") val privateKey = myPrivateKey + android.util.Log.d("ReplyDebug", " - Private key available: ${privateKey != null}") + if (privateKey != null) { + android.util.Log.d("ReplyDebug", " - Private key length: ${privateKey.length}") try { - dataJson = CryptoManager.decryptWithPassword(dataJson, privateKey) ?: dataJson - android.util.Log.d("ReplyDebug", " - Decrypted successfully, length: ${dataJson.length}") + val decrypted = CryptoManager.decryptWithPassword(dataJson, privateKey) + android.util.Log.d("ReplyDebug", " - Decryption result: ${if (decrypted != null) "SUCCESS" else "NULL"}") + if (decrypted != null) { + android.util.Log.d("ReplyDebug", " - Decrypted length: ${decrypted.length}") + android.util.Log.d("ReplyDebug", " - Decrypted preview: ${decrypted.take(200)}") + dataJson = decrypted + } else { + android.util.Log.e("ReplyDebug", " - ❌ Decryption returned null") + continue + } } catch (e: Exception) { - android.util.Log.e("ReplyDebug", " - Failed to decrypt blob", e) + android.util.Log.e("ReplyDebug", " - ❌ Decryption exception: ${e.message}", e) + android.util.Log.e("ReplyDebug", " - Stack trace:", e) continue } } else { - android.util.Log.e("ReplyDebug", " - Cannot decrypt: private key is null") + android.util.Log.e("ReplyDebug", " - ❌ Cannot decrypt: private key is NULL") continue } + } else { + android.util.Log.d("ReplyDebug", " - 📄 Blob is PLAINTEXT (not encrypted)") } - val messagesArray = JSONArray(dataJson) + android.util.Log.d("ReplyDebug", " - Attempting to parse as JSONArray...") + val messagesArray = try { + JSONArray(dataJson) + } catch (e: Exception) { + android.util.Log.e("ReplyDebug", " - ❌ Failed to parse as JSONArray: ${e.message}") + android.util.Log.e("ReplyDebug", " - Data was: $dataJson") + continue + } + + android.util.Log.d("ReplyDebug", " - Messages array length: ${messagesArray.length()}") + if (messagesArray.length() > 0) { + android.util.Log.d("ReplyDebug", " - Extracting first message from array...") val replyMessage = messagesArray.getJSONObject(0) val replyPublicKey = replyMessage.optString("publicKey", "") val replyText = replyMessage.optString("message", "") val replyMessageId = replyMessage.optString("message_id", "") - // Определяем, кто автор цитируемого сообщения - // Если publicKey == myPublicKey - цитата от меня - val isReplyFromMe = replyPublicKey == myPublicKey + android.util.Log.d("ReplyDebug", " - replyMessageId: $replyMessageId") + android.util.Log.d("ReplyDebug", " - replyPublicKey: ${replyPublicKey.take(20)}...") + android.util.Log.d("ReplyDebug", " - replyText: ${replyText.take(50)}") + android.util.Log.d("ReplyDebug", " - myPublicKey: ${myPublicKey?.take(20)}...") + + // Определяем, кто автор цитируемого сообщения + val isReplyFromMe = replyPublicKey == myPublicKey + android.util.Log.d("ReplyDebug", " - isReplyFromMe: $isReplyFromMe") - android.util.Log.d("ReplyDebug", " - Parsed from DB: id=$replyMessageId, text=${replyText.take(30)}, isFromMe=$isReplyFromMe") val result = ReplyData( messageId = replyMessageId, senderName = if (isReplyFromMe) "You" else opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } }, text = replyText, isFromMe = isReplyFromMe ) - android.util.Log.d("ReplyDebug", "✅ [DB] Reply data parsed successfully from DB") + android.util.Log.d("ReplyDebug", " - ✅ Created ReplyData: senderName=${result.senderName}, text=${result.text.take(30)}") + android.util.Log.d("ReplyDebug", "✅ [DB LOAD] Reply data parsed successfully from DB - RETURNING") return result + } else { + android.util.Log.e("ReplyDebug", " - ❌ Messages array is empty") } + } else { + android.util.Log.d("ReplyDebug", " - ⏭️ Skipping attachment: type != 1") } } - android.util.Log.d("ReplyDebug", "💾 [DB] No MESSAGES attachment found") + android.util.Log.d("ReplyDebug", "💾 [DB LOAD] No MESSAGES attachment found") null } catch (e: Exception) { - android.util.Log.e("ReplyDebug", "❌ [DB] Failed to parse reply from attachments:", e) + android.util.Log.e("ReplyDebug", "❌ [DB LOAD] Failed to parse reply from attachments:", e) null } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt index 6d63088..9972cb0 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt @@ -165,7 +165,7 @@ fun ForwardChatPickerBottomSheet( LazyColumn( modifier = Modifier .fillMaxWidth() - .heightIn(max = 400.dp) + .heightIn(min = 300.dp, max = 400.dp) // 🔥 Минимальная высота для лучшего UX ) { items(dialogs, key = { it.opponentKey }) { dialog -> ForwardDialogItem(