feat: Integrate AccountManager to retrieve last logged account in AuthFlow and update MainActivity

This commit is contained in:
2026-01-16 06:18:47 +05:00
parent c52e6dda53
commit b1046f88e5
12 changed files with 24 additions and 242 deletions

View File

@@ -158,6 +158,7 @@ class MainActivity : ComponentActivity() {
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
hasExistingAccount = screen == "auth_unlock", hasExistingAccount = screen == "auth_unlock",
accounts = accountInfoList, accounts = accountInfoList,
accountManager = accountManager,
onAuthComplete = { account -> onAuthComplete = { account ->
currentAccount = account currentAccount = account
hasExistingAccount = true hasExistingAccount = true

View File

@@ -332,11 +332,6 @@ object CryptoManager {
// Decompress (zlib inflate - совместимо с pako.inflate в JS) // Decompress (zlib inflate - совместимо с pako.inflate в JS)
String(decompress(decrypted), Charsets.UTF_8) String(decompress(decrypted), Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("ReplyDebug", "❌ [DECRYPT] decryptWithPassword failed:", e)
android.util.Log.e("ReplyDebug", " - Input length: ${encryptedData.length}")
android.util.Log.e("ReplyDebug", " - Input preview: ${encryptedData.take(100)}")
android.util.Log.e("ReplyDebug", " - Password length: ${password.length}")
android.util.Log.e("ReplyDebug", " - Exception: ${e.message}")
null null
} }
} }

View File

@@ -606,15 +606,10 @@ object MessageCrypto {
*/ */
fun encryptReplyBlob(replyJson: String, plainKeyAndNonce: ByteArray): String { fun encryptReplyBlob(replyJson: String, plainKeyAndNonce: ByteArray): String {
return try { return try {
android.util.Log.d("ReplyDebug", "🔐 encryptReplyBlob called:")
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}")
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
// Convert plainKeyAndNonce to string - simulate JS Buffer.toString('utf-8') behavior // Convert plainKeyAndNonce to string - simulate JS Buffer.toString('utf-8') behavior
// which replaces invalid UTF-8 sequences with U+FFFD // which replaces invalid UTF-8 sequences with U+FFFD
val password = bytesToJsUtf8String(plainKeyAndNonce) val password = bytesToJsUtf8String(plainKeyAndNonce)
android.util.Log.d("ReplyDebug", " - password length: ${password.length}")
android.util.Log.d("ReplyDebug", " - password bytes (first 20): ${password.toByteArray(Charsets.UTF_8).take(20).joinToString(",")}")
// Compress with pako (deflate) // Compress with pako (deflate)
val deflater = java.util.zip.Deflater() val deflater = java.util.zip.Deflater()
@@ -817,37 +812,25 @@ object MessageCrypto {
*/ */
fun decryptReplyBlob(encryptedBlob: String, plainKeyAndNonce: ByteArray): String { fun decryptReplyBlob(encryptedBlob: String, plainKeyAndNonce: ByteArray): String {
return try { return try {
android.util.Log.d("ReplyDebug", "🔓 decryptReplyBlob called:")
android.util.Log.d("ReplyDebug", " - Input length: ${encryptedBlob.length}")
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}")
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
// Check if it's encrypted format (contains ':') // Check if it's encrypted format (contains ':')
if (!encryptedBlob.contains(':')) { if (!encryptedBlob.contains(':')) {
android.util.Log.d("ReplyDebug", " - No ':' found, returning as-is")
return encryptedBlob return encryptedBlob
} }
// Parse ivBase64:ciphertextBase64 // Parse ivBase64:ciphertextBase64
val parts = encryptedBlob.split(':') val parts = encryptedBlob.split(':')
if (parts.size != 2) { if (parts.size != 2) {
android.util.Log.d("ReplyDebug", " - Invalid format (not 2 parts), returning as-is")
return encryptedBlob return encryptedBlob
} }
android.util.Log.d("ReplyDebug", " - IV part length: ${parts[0].length}")
android.util.Log.d("ReplyDebug", " - Ciphertext part length: ${parts[1].length}")
val iv = Base64.decode(parts[0], Base64.DEFAULT) val iv = Base64.decode(parts[0], Base64.DEFAULT)
val ciphertext = Base64.decode(parts[1], Base64.DEFAULT) val ciphertext = Base64.decode(parts[1], Base64.DEFAULT)
android.util.Log.d("ReplyDebug", " - Decoded IV size: ${iv.size}")
android.util.Log.d("ReplyDebug", " - Decoded ciphertext size: ${ciphertext.size}")
// Password from plainKeyAndNonce - use same JS-like UTF-8 conversion // Password from plainKeyAndNonce - use same JS-like UTF-8 conversion
val password = bytesToJsUtf8String(plainKeyAndNonce) val password = bytesToJsUtf8String(plainKeyAndNonce)
android.util.Log.d("ReplyDebug", " - Password length: ${password.length}")
android.util.Log.d("ReplyDebug", " - Password bytes hex: ${password.toByteArray(Charsets.UTF_8).joinToString("") { "%02x".format(it) }}")
// PBKDF2 key derivation // PBKDF2 key derivation
// CRITICAL: Must use SHA256 to match React Native (not SHA1!) // CRITICAL: Must use SHA256 to match React Native (not SHA1!)
@@ -861,7 +844,6 @@ object MessageCrypto {
val secretKey = factory.generateSecret(spec) val secretKey = factory.generateSecret(spec)
val keyBytes = secretKey.encoded val keyBytes = secretKey.encoded
android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}")
// AES-CBC decryption // AES-CBC decryption
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
@@ -870,7 +852,6 @@ object MessageCrypto {
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val decompressed = cipher.doFinal(ciphertext) val decompressed = cipher.doFinal(ciphertext)
android.util.Log.d("ReplyDebug", " - Decrypted (compressed) size: ${decompressed.size}")
// Decompress with inflate // Decompress with inflate
val inflater = java.util.zip.Inflater() val inflater = java.util.zip.Inflater()
@@ -880,14 +861,9 @@ object MessageCrypto {
inflater.end() inflater.end()
val plaintext = String(outputBuffer, 0, outputSize, Charsets.UTF_8) val plaintext = String(outputBuffer, 0, outputSize, Charsets.UTF_8)
android.util.Log.d("ReplyDebug", " - Decompressed plaintext length: ${plaintext.length}")
android.util.Log.d("ReplyDebug", " - Plaintext preview: ${plaintext.take(100)}")
android.util.Log.d("ReplyDebug", "✅ decryptReplyBlob success")
plaintext plaintext
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("ReplyDebug", "❌ decryptReplyBlob failed:", e)
android.util.Log.e("ReplyDebug", " - Exception: ${e.javaClass.simpleName}: ${e.message}")
// Return as-is, might be plain JSON // Return as-is, might be plain JSON
encryptedBlob encryptedBlob
} }

View File

@@ -49,7 +49,6 @@ object ForwardManager {
messages: List<ForwardMessage>, messages: List<ForwardMessage>,
showPicker: Boolean = true showPicker: Boolean = true
) { ) {
android.util.Log.d("ForwardManager", "📨 Setting forward messages: ${messages.size}")
_forwardMessages.value = messages _forwardMessages.value = messages
if (showPicker) { if (showPicker) {
_showChatPicker.value = true _showChatPicker.value = true
@@ -60,7 +59,6 @@ object ForwardManager {
* Выбрать чат для пересылки * Выбрать чат для пересылки
*/ */
fun selectChat(publicKey: String) { fun selectChat(publicKey: String) {
android.util.Log.d("ForwardManager", "📨 Selected chat: $publicKey")
_selectedChatPublicKey.value = publicKey _selectedChatPublicKey.value = publicKey
_showChatPicker.value = false _showChatPicker.value = false
} }
@@ -69,7 +67,6 @@ object ForwardManager {
* Скрыть выбор чата (отмена) * Скрыть выбор чата (отмена)
*/ */
fun hideChatPicker() { fun hideChatPicker() {
android.util.Log.d("ForwardManager", "📨 Hide chat picker")
_showChatPicker.value = false _showChatPicker.value = false
} }
@@ -79,7 +76,6 @@ object ForwardManager {
*/ */
fun consumeForwardMessages(): List<ForwardMessage> { fun consumeForwardMessages(): List<ForwardMessage> {
val messages = _forwardMessages.value val messages = _forwardMessages.value
android.util.Log.d("ForwardManager", "📨 Consuming forward messages: ${messages.size}")
return messages return messages
} }
@@ -87,7 +83,6 @@ object ForwardManager {
* Очистить все данные (после применения или отмены) * Очистить все данные (после применения или отмены)
*/ */
fun clear() { fun clear() {
android.util.Log.d("ForwardManager", "📨 Clearing forward state")
_forwardMessages.value = emptyList() _forwardMessages.value = emptyList()
_showChatPicker.value = false _showChatPicker.value = false
_selectedChatPublicKey.value = null _selectedChatPublicKey.value = null
@@ -104,7 +99,6 @@ object ForwardManager {
fun hasForwardMessagesForChat(publicKey: String): Boolean { fun hasForwardMessagesForChat(publicKey: String): Boolean {
val selectedKey = _selectedChatPublicKey.value val selectedKey = _selectedChatPublicKey.value
val hasMessages = _forwardMessages.value.isNotEmpty() val hasMessages = _forwardMessages.value.isNotEmpty()
android.util.Log.d("ForwardManager", "📨 hasForwardMessagesForChat($publicKey): selectedKey=$selectedKey, hasMessages=$hasMessages")
return selectedKey == publicKey && hasMessages return selectedKey == publicKey && hasMessages
} }
@@ -115,7 +109,6 @@ object ForwardManager {
fun getForwardMessagesForChat(publicKey: String): List<ForwardMessage> { fun getForwardMessagesForChat(publicKey: String): List<ForwardMessage> {
val selectedKey = _selectedChatPublicKey.value val selectedKey = _selectedChatPublicKey.value
return if (selectedKey == publicKey && _forwardMessages.value.isNotEmpty()) { return if (selectedKey == publicKey && _forwardMessages.value.isNotEmpty()) {
android.util.Log.d("ForwardManager", "📨 getForwardMessagesForChat: returning ${_forwardMessages.value.size} messages")
_forwardMessages.value _forwardMessages.value
} else { } else {
emptyList() emptyList()

View File

@@ -5,6 +5,7 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.runtime.* import androidx.compose.runtime.*
import com.rosetta.messenger.data.DecryptedAccount import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.AccountManager
enum class AuthScreen { enum class AuthScreen {
SELECT_ACCOUNT, SELECT_ACCOUNT,
@@ -21,6 +22,7 @@ fun AuthFlow(
isDarkTheme: Boolean, isDarkTheme: Boolean,
hasExistingAccount: Boolean, hasExistingAccount: Boolean,
accounts: List<AccountInfo> = emptyList(), accounts: List<AccountInfo> = emptyList(),
accountManager: AccountManager,
onAuthComplete: (DecryptedAccount?) -> Unit, onAuthComplete: (DecryptedAccount?) -> Unit,
onLogout: () -> Unit = {} onLogout: () -> Unit = {}
) { ) {
@@ -33,7 +35,12 @@ fun AuthFlow(
) )
} }
var seedPhrase by remember { mutableStateOf<List<String>>(emptyList()) } var seedPhrase by remember { mutableStateOf<List<String>>(emptyList()) }
var selectedAccountId by remember { mutableStateOf<String?>(accounts.firstOrNull()?.id) } // Use last logged account or fallback to first account
var selectedAccountId by remember {
mutableStateOf<String?>(
accountManager.getLastLoggedPublicKey() ?: accounts.firstOrNull()?.id
)
}
var showCreateModal by remember { mutableStateOf(false) } var showCreateModal by remember { mutableStateOf(false) }
// Handle system back button // Handle system back button

View File

@@ -333,18 +333,12 @@ fun ChatDetailScreen(
// Логирование изменений selection mode // Логирование изменений selection mode
LaunchedEffect(isSelectionMode, selectedMessages.size) { LaunchedEffect(isSelectionMode, selectedMessages.size) {
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
android.util.Log.d("ChatDetailScreen", "📝 SELECTION MODE CHANGED")
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode: $isSelectionMode")
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size: ${selectedMessages.size}")
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
} }
// 🔥 Backup: если клавиатура ещё открыта когда selection mode активировался // 🔥 Backup: если клавиатура ещё открыта когда selection mode активировался
// (клавиатура уже должна быть закрыта в onLongClick, это только backup) // (клавиатура уже должна быть закрыта в onLongClick, это только backup)
LaunchedEffect(isSelectionMode) { LaunchedEffect(isSelectionMode) {
if (isSelectionMode) { if (isSelectionMode) {
android.util.Log.d("ChatDetailScreen", "⚠️ Backup keyboard hide triggered")
// Backup закрытие клавиатуры (основное в onLongClick) // Backup закрытие клавиатуры (основное в onLongClick)
keyboardController?.hide() keyboardController?.hide()
} }
@@ -445,13 +439,9 @@ fun ChatDetailScreen(
// 🔥 Функция для скролла к сообщению с подсветкой // 🔥 Функция для скролла к сообщению с подсветкой
val scrollToMessage: (String) -> Unit = { messageId -> val scrollToMessage: (String) -> Unit = { messageId ->
android.util.Log.d("ChatDetail", "🔍 scrollToMessage called for: '$messageId'")
android.util.Log.d("ChatDetail", " - messageId length: ${messageId.length}")
android.util.Log.d("ChatDetail", " - Total messages: ${messagesWithDates.size}")
// Логируем все ID сообщений для отладки // Логируем все ID сообщений для отладки
messagesWithDates.forEachIndexed { index, pair -> messagesWithDates.forEachIndexed { index, pair ->
android.util.Log.d("ChatDetail", " - [$index] id='${pair.first.id}', text='${pair.first.text.take(20)}...'")
} }
scope.launch { scope.launch {
@@ -461,18 +451,15 @@ fun ChatDetailScreen(
// Находим индекс сообщения в списке // Находим индекс сообщения в списке
val messageIndex = messagesWithDates.indexOfFirst { it.first.id == messageId } val messageIndex = messagesWithDates.indexOfFirst { it.first.id == messageId }
android.util.Log.d("ChatDetail", " - Found at index: $messageIndex")
if (messageIndex != -1) { if (messageIndex != -1) {
// Скроллим к сообщению // Скроллим к сообщению
listState.animateScrollToItem(messageIndex) listState.animateScrollToItem(messageIndex)
android.util.Log.d("ChatDetail", " ✅ Scrolled to message")
// Подсвечиваем на 2 секунды // Подсвечиваем на 2 секунды
highlightedMessageId = messageId highlightedMessageId = messageId
delay(2000) delay(2000)
highlightedMessageId = null highlightedMessageId = null
} else { } else {
android.util.Log.d("ChatDetail", " ❌ Message not found in list")
} }
} }
} }
@@ -1018,14 +1005,6 @@ fun ChatDetailScreen(
// Логирование состояния // Логирование состояния
LaunchedEffect(isSelectionMode, useImePadding, coordinator.isEmojiBoxVisible, coordinator.keyboardHeight) { LaunchedEffect(isSelectionMode, useImePadding, coordinator.isEmojiBoxVisible, coordinator.keyboardHeight) {
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
android.util.Log.d("ChatDetailScreen", "🔄 BOTTOM BAR STATE CHANGED")
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode: $isSelectionMode")
android.util.Log.d("ChatDetailScreen", " 📊 useImePadding: $useImePadding")
android.util.Log.d("ChatDetailScreen", " 📊 isEmojiBoxVisible: ${coordinator.isEmojiBoxVisible}")
android.util.Log.d("ChatDetailScreen", " 📊 keyboardHeight: ${coordinator.keyboardHeight}")
android.util.Log.d("ChatDetailScreen", " 📊 emojiHeight: ${coordinator.emojiHeight}")
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
} }
Column(modifier = bottomModifier) { Column(modifier = bottomModifier) {
@@ -1057,7 +1036,6 @@ fun ChatDetailScreen(
}, },
label = "bottomBarContent" label = "bottomBarContent"
) { selectionMode -> ) { selectionMode ->
android.util.Log.d("ChatDetailScreen", "🎬 AnimatedContent to selectionMode=$selectionMode")
if (selectionMode) { if (selectionMode) {
// SELECTION ACTION BAR - Reply/Forward // SELECTION ACTION BAR - Reply/Forward
@@ -1360,19 +1338,13 @@ fun ChatDetailScreen(
isSelected = selectedMessages.contains(selectionKey), isSelected = selectedMessages.contains(selectionKey),
isHighlighted = highlightedMessageId == message.id, isHighlighted = highlightedMessageId == message.id,
onLongClick = { onLongClick = {
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
android.util.Log.d("ChatDetailScreen", "👆 LONG CLICK on message")
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode BEFORE: $isSelectionMode")
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size BEFORE: ${selectedMessages.size}")
// 🔥 СНАЧАЛА закрываем клавиатуру МГНОВЕННО (до изменения state) // 🔥 СНАЧАЛА закрываем клавиатуру МГНОВЕННО (до изменения state)
if (!isSelectionMode) { if (!isSelectionMode) {
android.util.Log.d("ChatDetailScreen", " ⌨️ Closing keyboard...")
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0) imm.hideSoftInputFromWindow(view.windowToken, 0)
focusManager.clearFocus() focusManager.clearFocus()
showEmojiPicker = false showEmojiPicker = false
android.util.Log.d("ChatDetailScreen", " ✅ Keyboard closed")
} }
// Toggle selection on long press // Toggle selection on long press
selectedMessages = if (selectedMessages.contains(selectionKey)) { selectedMessages = if (selectedMessages.contains(selectionKey)) {
@@ -1381,8 +1353,6 @@ fun ChatDetailScreen(
selectedMessages + selectionKey selectedMessages + selectionKey
} }
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size AFTER: ${selectedMessages.size}")
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
}, },
onClick = { onClick = {
// If in selection mode, toggle selection // If in selection mode, toggle selection
@@ -1905,7 +1875,6 @@ private fun MessageBubble(
isOutgoing = message.isOutgoing, isOutgoing = message.isOutgoing,
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
onClick = { onClick = {
android.util.Log.d("ChatDetail", "🖱️ Reply clicked: ${reply.messageId}")
onReplyClick(reply.messageId) onReplyClick(reply.messageId)
} }
) )
@@ -2238,22 +2207,16 @@ private fun MessageInputBar(
// 🔥 Автофокус при открытии reply панели // 🔥 Автофокус при открытии reply панели
LaunchedEffect(hasReply, editTextView) { LaunchedEffect(hasReply, editTextView) {
if (hasReply) { if (hasReply) {
android.util.Log.d("EmojiPicker", "═══════════════════════════════════════════════════════")
android.util.Log.d("EmojiPicker", "💬 Reply panel opened, hasReply=$hasReply")
android.util.Log.d("EmojiPicker", " 📊 editTextView=$editTextView, showEmojiPicker=$showEmojiPicker")
// Даём время на создание view если ещё null // Даём время на создание view если ещё null
kotlinx.coroutines.delay(50) kotlinx.coroutines.delay(50)
editTextView?.let { editText -> editTextView?.let { editText ->
// 🔥 НЕ открываем клавиатуру если emoji уже открыт // 🔥 НЕ открываем клавиатуру если emoji уже открыт
if (!showEmojiPicker) { if (!showEmojiPicker) {
android.util.Log.d("EmojiPicker", " ⌨️ Requesting focus and keyboard for reply...")
editText.requestFocus() editText.requestFocus()
// Открываем клавиатуру // Открываем клавиатуру
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
android.util.Log.d("EmojiPicker", " ✅ Auto-opened keyboard for reply")
} else { } else {
android.util.Log.d("EmojiPicker", " ⏭️ Skip auto-keyboard for reply (emoji is open)")
} }
} }
} }
@@ -2356,7 +2319,6 @@ private fun MessageInputBar(
val timeSinceLastToggle = currentTime - lastToggleTime val timeSinceLastToggle = currentTime - lastToggleTime
if (timeSinceLastToggle < toggleCooldownMs) { if (timeSinceLastToggle < toggleCooldownMs) {
android.util.Log.d("EmojiPicker", "⏸️ Toggle blocked: ${timeSinceLastToggle}ms < ${toggleCooldownMs}ms")
return return
} }
@@ -2364,45 +2326,34 @@ private fun MessageInputBar(
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
android.util.Log.d("EmojiPicker", "=".repeat(60))
android.util.Log.d("EmojiPicker", "🔥 toggleEmojiPicker START")
android.util.Log.d("EmojiPicker", " showEmojiPicker(local)=$showEmojiPicker")
coordinator.logState() coordinator.logState()
// 🔥 ИСПОЛЬЗУЕМ coordinator.isEmojiVisible вместо showEmojiPicker для более точного состояния // 🔥 ИСПОЛЬЗУЕМ coordinator.isEmojiVisible вместо showEmojiPicker для более точного состояния
if (coordinator.isEmojiVisible) { if (coordinator.isEmojiVisible) {
// ========== EMOJI → KEYBOARD ========== // ========== EMOJI → KEYBOARD ==========
android.util.Log.d("EmojiPicker", "🔄 Switching: Emoji → Keyboard")
coordinator.requestShowKeyboard( coordinator.requestShowKeyboard(
showKeyboard = { showKeyboard = {
editTextView?.let { editText -> editTextView?.let { editText ->
editText.requestFocus() editText.requestFocus()
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED) imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
android.util.Log.d("EmojiPicker", "📱 Keyboard show requested")
} }
}, },
hideEmoji = { hideEmoji = {
onToggleEmojiPicker(false) onToggleEmojiPicker(false)
android.util.Log.d("EmojiPicker", "😊 Emoji panel hidden")
} }
) )
} else { } else {
// ========== KEYBOARD → EMOJI ========== // ========== KEYBOARD → EMOJI ==========
android.util.Log.d("EmojiPicker", "🔄 Switching: Keyboard → Emoji")
coordinator.requestShowEmoji( coordinator.requestShowEmoji(
hideKeyboard = { hideKeyboard = {
imm.hideSoftInputFromWindow(view.windowToken, 0) imm.hideSoftInputFromWindow(view.windowToken, 0)
android.util.Log.d("EmojiPicker", "⌨️ Keyboard hide requested")
}, },
showEmoji = { showEmoji = {
onToggleEmojiPicker(true) onToggleEmojiPicker(true)
android.util.Log.d("EmojiPicker", "😊 Emoji panel shown")
} }
) )
} }
android.util.Log.d("EmojiPicker", "🔥 toggleEmojiPicker END")
android.util.Log.d("EmojiPicker", "=".repeat(60))
} }
// Функция отправки - НЕ закрывает клавиатуру (UX правило #6) // Функция отправки - НЕ закрывает клавиатуру (UX правило #6)
@@ -2643,24 +2594,12 @@ private fun MessageInputBar(
editTextView = view editTextView = view
}, },
onFocusChanged = { hasFocus -> onFocusChanged = { hasFocus ->
android.util.Log.d("EmojiPicker", "═══════════════════════════════════════════════════════")
android.util.Log.d("EmojiPicker", "🎯 TextField focus changed: hasFocus=$hasFocus")
android.util.Log.d("EmojiPicker", " 📊 Current state:")
android.util.Log.d("EmojiPicker", " - showEmojiPicker=$showEmojiPicker")
android.util.Log.d("EmojiPicker", " - coordinator.isEmojiVisible=${coordinator.isEmojiVisible}")
android.util.Log.d("EmojiPicker", " - coordinator.isKeyboardVisible=${coordinator.isKeyboardVisible}")
android.util.Log.d("EmojiPicker", " - coordinator.currentState=${coordinator.currentState}")
// Если TextField получил фокус И emoji открыт → закрываем emoji // Если TextField получил фокус И emoji открыт → закрываем emoji
if (hasFocus && showEmojiPicker) { if (hasFocus && showEmojiPicker) {
android.util.Log.d("EmojiPicker", "🔄 TextField focused while emoji open → closing emoji")
android.util.Log.d("EmojiPicker", " 📞 Calling onToggleEmojiPicker(false)...")
onToggleEmojiPicker(false) onToggleEmojiPicker(false)
android.util.Log.d("EmojiPicker", " ✅ Emoji close requested")
} else if (hasFocus && !showEmojiPicker) { } else if (hasFocus && !showEmojiPicker) {
android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior")
} else if (!hasFocus) { } else if (!hasFocus) {
android.util.Log.d("EmojiPicker", "👋 TextField lost focus")
} }
} }
) )

View File

@@ -229,36 +229,24 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
var blobToStore = att.blob // По умолчанию сохраняем оригинальный blob var blobToStore = att.blob // По умолчанию сохраняем оригинальный blob
if (att.type == AttachmentType.MESSAGES && att.blob.isNotEmpty()) { if (att.type == AttachmentType.MESSAGES && att.blob.isNotEmpty()) {
try { try {
android.util.Log.d("ReplyDebug", "📥 [RECEIVE] Processing reply attachment:")
android.util.Log.d("ReplyDebug", " - Encrypted blob length: ${att.blob.length}")
android.util.Log.d("ReplyDebug", " - Encrypted blob preview: ${att.blob.take(100)}")
android.util.Log.d("ReplyDebug", " - Decrypting with plainKeyAndNonce (${plainKeyAndNonce.size} bytes)")
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
// 🔥 Расшифровываем с полным plainKeyAndNonce (56 bytes) // 🔥 Расшифровываем с полным plainKeyAndNonce (56 bytes)
// Desktop использует chachaDecryptedKey.toString('utf-8') = полные 56 байт! // Desktop использует chachaDecryptedKey.toString('utf-8') = полные 56 байт!
val decryptedBlob = MessageCrypto.decryptReplyBlob(att.blob, plainKeyAndNonce) val decryptedBlob = MessageCrypto.decryptReplyBlob(att.blob, plainKeyAndNonce)
android.util.Log.d("ReplyDebug", " - Decrypted blob length: ${decryptedBlob.length}")
android.util.Log.d("ReplyDebug", " - Decrypted blob preview: ${decryptedBlob.take(200)}")
// 🔥 Сохраняем расшифрованный blob в БД // 🔥 Сохраняем расшифрованный blob в БД
blobToStore = decryptedBlob blobToStore = decryptedBlob
// Парсим JSON массив с цитируемыми сообщениями // Парсим JSON массив с цитируемыми сообщениями
val replyArray = JSONArray(decryptedBlob) val replyArray = JSONArray(decryptedBlob)
android.util.Log.d("ReplyDebug", " - Reply array length: ${replyArray.length()}")
if (replyArray.length() > 0) { if (replyArray.length() > 0) {
val firstReply = replyArray.getJSONObject(0) val firstReply = replyArray.getJSONObject(0)
val replyPublicKey = firstReply.optString("publicKey", "") val replyPublicKey = firstReply.optString("publicKey", "")
val replyText = firstReply.optString("message", "") val replyText = firstReply.optString("message", "")
val replyMessageId = firstReply.optString("message_id", "") val replyMessageId = firstReply.optString("message_id", "")
android.util.Log.d("ReplyDebug", " - Parsed reply: id=$replyMessageId")
android.util.Log.d("ReplyDebug", " publicKey=${replyPublicKey.take(20)}...")
android.util.Log.d("ReplyDebug", " message=${replyText.take(50)}")
// Определяем автора цитаты // Определяем автора цитаты
val isReplyFromMe = replyPublicKey == myPublicKey val isReplyFromMe = replyPublicKey == myPublicKey
android.util.Log.d("ReplyDebug", " - Is reply from me: $isReplyFromMe")
replyData = ReplyData( replyData = ReplyData(
messageId = replyMessageId, messageId = replyMessageId,
@@ -266,11 +254,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
text = replyText, text = replyText,
isFromMe = isReplyFromMe isFromMe = isReplyFromMe
) )
android.util.Log.d("ReplyDebug", "✅ [RECEIVE] Reply data created successfully")
} }
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("ReplyDebug", "❌ [RECEIVE] Failed to decrypt/parse reply:", e)
android.util.Log.e("ReplyDebug", " - Encrypted blob: ${att.blob.take(100)}")
} }
} }
@@ -364,7 +349,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val forwardMessages = ForwardManager.getForwardMessagesForChat(publicKey) val forwardMessages = ForwardManager.getForwardMessagesForChat(publicKey)
val hasForward = forwardMessages.isNotEmpty() val hasForward = forwardMessages.isNotEmpty()
if (hasForward) { if (hasForward) {
android.util.Log.d("ChatViewModel", "📨 Will apply ${forwardMessages.size} forward messages")
} }
// Сбрасываем состояние // Сбрасываем состояние
@@ -380,7 +364,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 📨 Применяем Forward сообщения СРАЗУ после сброса // 📨 Применяем Forward сообщения СРАЗУ после сброса
if (hasForward) { if (hasForward) {
android.util.Log.d("ChatViewModel", "📨 Applying ${forwardMessages.size} forward messages NOW")
// Конвертируем ForwardMessage в ReplyMessage // Конвертируем ForwardMessage в ReplyMessage
_replyMessages.value = forwardMessages.map { fm -> _replyMessages.value = forwardMessages.map { fm ->
ReplyMessage( ReplyMessage(
@@ -392,7 +375,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
) )
} }
_isForwardMode.value = true _isForwardMode.value = true
android.util.Log.d("ChatViewModel", "📨 isForwardMode set to TRUE, replyMessages.size = ${_replyMessages.value.size}")
// Очищаем ForwardManager после применения // Очищаем ForwardManager после применения
ForwardManager.clear() ForwardManager.clear()
} else { } else {
@@ -446,7 +428,21 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
return@launch return@launch
} }
// 🔥 Нет кэша - показываем скелетон и загружаем с задержкой для анимации // 🔥 Нет кэша - проверяем есть ли вообще сообщения в БД
// Если диалог пустой - не показываем скелетон!
val totalCount = messageDao.getMessageCount(account, dialogKey)
if (totalCount == 0) {
// Пустой диалог - сразу показываем пустое состояние без скелетона
withContext(Dispatchers.Main.immediate) {
_messages.value = emptyList()
_isLoading.value = false
}
isLoadingMessages = false
return@launch
}
// 🔥 Есть сообщения - показываем скелетон и загружаем с задержкой для анимации
if (delayMs > 0) { if (delayMs > 0) {
withContext(Dispatchers.Main.immediate) { withContext(Dispatchers.Main.immediate) {
_isLoading.value = true // Показываем скелетон _isLoading.value = true // Показываем скелетон
@@ -455,9 +451,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
// 🔍 Проверяем общее количество сообщений в диалоге
val totalCount = messageDao.getMessageCount(account, dialogKey)
// 🔥 Получаем первую страницу - БЕЗ suspend задержки // 🔥 Получаем первую страницу - БЕЗ suspend задержки
val entities = messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0) val entities = messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0)
@@ -635,10 +628,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* Как в архиве: расшифровываем при каждой загрузке * Как в архиве: расшифровываем при каждой загрузке
*/ */
private suspend fun entityToChatMessage(entity: MessageEntity): ChatMessage { 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 // Расшифровываем сообщение из content + chachaKey
var displayText = try { var displayText = try {
@@ -677,35 +666,27 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
android.util.Log.d("ReplyDebug", " - Decrypted text: ${displayText.take(50)}")
// Парсим attachments для поиска MESSAGES (цитата) // Парсим attachments для поиска MESSAGES (цитата)
// 🔥 ВАЖНО: Передаем content и chachaKey для расшифровки reply blob если нужно // 🔥 ВАЖНО: Передаем content и chachaKey для расшифровки reply blob если нужно
android.util.Log.d("ReplyDebug", " - Calling parseReplyFromAttachments...")
var replyData = parseReplyFromAttachments( var replyData = parseReplyFromAttachments(
attachmentsJson = entity.attachments, attachmentsJson = entity.attachments,
isFromMe = entity.fromMe == 1, isFromMe = entity.fromMe == 1,
content = entity.content, content = entity.content,
chachaKey = entity.chachaKey chachaKey = entity.chachaKey
) )
android.util.Log.d("ReplyDebug", " - parseReplyFromAttachments returned: ${if (replyData != null) "NOT NULL" else "NULL"}")
// Если не нашли reply в attachments, пробуем распарсить из текста // Если не нашли reply в attachments, пробуем распарсить из текста
if (replyData == null) { if (replyData == null) {
android.util.Log.d("ReplyDebug", " - Reply is null, trying to parse from text...")
val parseResult = parseReplyFromText(displayText) val parseResult = parseReplyFromText(displayText)
if (parseResult != null) { if (parseResult != null) {
android.util.Log.d("ReplyDebug", " - ✅ Parsed reply from text")
replyData = parseResult.first replyData = parseResult.first
displayText = parseResult.second displayText = parseResult.second
} else { } else {
android.util.Log.d("ReplyDebug", " - ❌ Failed to parse reply from text")
} }
} else { } 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( return ChatMessage(
id = entity.messageId, id = entity.messageId,
@@ -758,139 +739,98 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
content: String, content: String,
chachaKey: String chachaKey: String
): ReplyData? { ): ReplyData? {
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 == "[]") { if (attachmentsJson.isEmpty() || attachmentsJson == "[]") {
android.util.Log.d("ReplyDebug", " - Early return: attachments empty or []")
return null return null
} }
return try { return try {
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) val attachments = JSONArray(attachmentsJson)
android.util.Log.d("ReplyDebug", " - Attachments count: ${attachments.length()}")
for (i in 0 until attachments.length()) { for (i in 0 until attachments.length()) {
android.util.Log.d("ReplyDebug", " - Processing attachment $i...")
val attachment = attachments.getJSONObject(i) val attachment = attachments.getJSONObject(i)
val type = attachment.optInt("type", 0) val type = attachment.optInt("type", 0)
android.util.Log.d("ReplyDebug", " - type: $type")
// MESSAGES = 1 (цитата) // MESSAGES = 1 (цитата)
if (type == 1) { if (type == 1) {
android.util.Log.d("ReplyDebug", " - ✅ Found MESSAGES type!")
// Данные могут быть в blob или preview // Данные могут быть в blob или preview
var dataJson = attachment.optString("blob", "") var dataJson = attachment.optString("blob", "")
android.util.Log.d("ReplyDebug", " - blob from JSON: ${if (dataJson.isEmpty()) "EMPTY" else "length=${dataJson.length}"}")
if (dataJson.isEmpty()) { if (dataJson.isEmpty()) {
dataJson = attachment.optString("preview", "") 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()) { if (dataJson.isEmpty()) {
android.util.Log.e("ReplyDebug", " - ❌ Data is empty, skipping")
continue continue
} }
// 🔥 Проверяем формат blob - если содержит ":", то это зашифрованный формат "iv:ciphertext" // 🔥 Проверяем формат blob - если содержит ":", то это зашифрованный формат "iv:ciphertext"
val colonCount = dataJson.count { it == ':' } val colonCount = dataJson.count { it == ':' }
android.util.Log.d("ReplyDebug", " - Colon count in data: $colonCount")
if (dataJson.contains(":") && dataJson.split(":").size == 2) { if (dataJson.contains(":") && dataJson.split(":").size == 2) {
android.util.Log.d("ReplyDebug", " - 🔒 Blob is ENCRYPTED (iv:ciphertext format)")
val privateKey = myPrivateKey val privateKey = myPrivateKey
var decryptionSuccess = false var decryptionSuccess = false
// 🔥 Способ 1: Пробуем расшифровать с приватным ключом (для исходящих сообщений) // 🔥 Способ 1: Пробуем расшифровать с приватным ключом (для исходящих сообщений)
if (privateKey != null) { if (privateKey != null) {
android.util.Log.d("ReplyDebug", " - Attempting to decrypt with private key...")
try { try {
val decrypted = CryptoManager.decryptWithPassword(dataJson, privateKey) val decrypted = CryptoManager.decryptWithPassword(dataJson, privateKey)
if (decrypted != null) { if (decrypted != null) {
android.util.Log.d("ReplyDebug", " - ✅ Decrypted with private key")
android.util.Log.d("ReplyDebug", " - Decrypted preview: ${decrypted.take(200)}")
dataJson = decrypted dataJson = decrypted
decryptionSuccess = true decryptionSuccess = true
} }
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.d("ReplyDebug", " - Private key decryption failed: ${e.message}")
} }
} }
// 🔥 Способ 2: Пробуем расшифровать с ChaCha ключом сообщения (для входящих) // 🔥 Способ 2: Пробуем расшифровать с ChaCha ключом сообщения (для входящих)
if (!decryptionSuccess && content.isNotEmpty() && chachaKey.isNotEmpty() && privateKey != null) { if (!decryptionSuccess && content.isNotEmpty() && chachaKey.isNotEmpty() && privateKey != null) {
android.util.Log.d("ReplyDebug", " - Attempting to decrypt with ChaCha key...")
try { try {
val decrypted = MessageCrypto.decryptAttachmentBlob(dataJson, chachaKey, privateKey) val decrypted = MessageCrypto.decryptAttachmentBlob(dataJson, chachaKey, privateKey)
if (decrypted != null) { if (decrypted != null) {
android.util.Log.d("ReplyDebug", " - ✅ Decrypted with ChaCha key")
android.util.Log.d("ReplyDebug", " - Decrypted preview: ${decrypted.take(200)}")
dataJson = decrypted dataJson = decrypted
decryptionSuccess = true decryptionSuccess = true
} }
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.d("ReplyDebug", " - ChaCha decryption failed: ${e.message}")
} }
} }
// 🔥 Способ 3: Пробуем decryptReplyBlob с plainKeyAndNonce // 🔥 Способ 3: Пробуем decryptReplyBlob с plainKeyAndNonce
if (!decryptionSuccess && content.isNotEmpty() && chachaKey.isNotEmpty() && privateKey != null) { if (!decryptionSuccess && content.isNotEmpty() && chachaKey.isNotEmpty() && privateKey != null) {
android.util.Log.d("ReplyDebug", " - Attempting to decrypt with plainKeyAndNonce...")
try { try {
val decryptResult = MessageCrypto.decryptIncomingFull(content, chachaKey, privateKey) val decryptResult = MessageCrypto.decryptIncomingFull(content, chachaKey, privateKey)
val plainKeyAndNonce = decryptResult.plainKeyAndNonce val plainKeyAndNonce = decryptResult.plainKeyAndNonce
val decrypted = MessageCrypto.decryptReplyBlob(dataJson, plainKeyAndNonce) val decrypted = MessageCrypto.decryptReplyBlob(dataJson, plainKeyAndNonce)
if (decrypted.isNotEmpty()) { if (decrypted.isNotEmpty()) {
android.util.Log.d("ReplyDebug", " - ✅ Decrypted with plainKeyAndNonce")
android.util.Log.d("ReplyDebug", " - Decrypted preview: ${decrypted.take(200)}")
dataJson = decrypted dataJson = decrypted
decryptionSuccess = true decryptionSuccess = true
} }
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce decryption failed: ${e.message}")
} }
} }
if (!decryptionSuccess) { if (!decryptionSuccess) {
android.util.Log.e("ReplyDebug", " - ❌ All decryption methods failed")
continue continue
} }
} else { } else {
android.util.Log.d("ReplyDebug", " - 📄 Blob is PLAINTEXT (not encrypted)")
} }
android.util.Log.d("ReplyDebug", " - Attempting to parse as JSONArray...")
val messagesArray = try { val messagesArray = try {
JSONArray(dataJson) JSONArray(dataJson)
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("ReplyDebug", " - ❌ Failed to parse as JSONArray: ${e.message}")
android.util.Log.e("ReplyDebug", " - Data was: $dataJson")
continue continue
} }
android.util.Log.d("ReplyDebug", " - Messages array length: ${messagesArray.length()}")
if (messagesArray.length() > 0) { if (messagesArray.length() > 0) {
android.util.Log.d("ReplyDebug", " - Extracting first message from array...")
val replyMessage = messagesArray.getJSONObject(0) val replyMessage = messagesArray.getJSONObject(0)
val replyPublicKey = replyMessage.optString("publicKey", "") val replyPublicKey = replyMessage.optString("publicKey", "")
val replyText = replyMessage.optString("message", "") val replyText = replyMessage.optString("message", "")
val replyMessageIdFromJson = replyMessage.optString("message_id", "") val replyMessageIdFromJson = replyMessage.optString("message_id", "")
val replyTimestamp = replyMessage.optLong("timestamp", 0L) val replyTimestamp = replyMessage.optLong("timestamp", 0L)
android.util.Log.d("ReplyDebug", " - replyMessageId from JSON: $replyMessageIdFromJson")
android.util.Log.d("ReplyDebug", " - replyPublicKey: ${replyPublicKey.take(20)}...")
android.util.Log.d("ReplyDebug", " - replyText: ${replyText.take(50)}")
android.util.Log.d("ReplyDebug", " - replyTimestamp: $replyTimestamp")
// 🔥 ВАЖНО: message_id из JSON может не совпадать с messageId в Android БД! // 🔥 ВАЖНО: message_id из JSON может не совпадать с messageId в Android БД!
// Пытаемся найти реальный messageId в текущих сообщениях по тексту и timestamp // Пытаемся найти реальный messageId в текущих сообщениях по тексту и timestamp
@@ -906,15 +846,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
timestampTo = replyTimestamp + 5000 timestampTo = replyTimestamp + 5000
)?.messageId ?: replyMessageIdFromJson )?.messageId ?: replyMessageIdFromJson
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.w("ReplyDebug", " - ⚠️ Could not find real messageId, using JSON id")
replyMessageIdFromJson replyMessageIdFromJson
} }
android.util.Log.d("ReplyDebug", " - Real messageId: $realMessageId")
// Определяем, кто автор цитируемого сообщения // Определяем, кто автор цитируемого сообщения
val isReplyFromMe = replyPublicKey == myPublicKey val isReplyFromMe = replyPublicKey == myPublicKey
android.util.Log.d("ReplyDebug", " - isReplyFromMe: $isReplyFromMe")
val result = ReplyData( val result = ReplyData(
messageId = realMessageId, messageId = realMessageId,
@@ -922,20 +859,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
text = replyText, text = replyText,
isFromMe = isReplyFromMe isFromMe = isReplyFromMe
) )
android.util.Log.d("ReplyDebug", " - ✅ Created ReplyData: senderName=${result.senderName}, messageId=${result.messageId}")
android.util.Log.d("ReplyDebug", "✅ [DB LOAD] Reply data parsed successfully from DB - RETURNING")
return result return result
} else { } else {
android.util.Log.e("ReplyDebug", " - ❌ Messages array is empty")
} }
} else { } else {
android.util.Log.d("ReplyDebug", " - ⏭️ Skipping attachment: type != 1")
} }
} }
android.util.Log.d("ReplyDebug", "💾 [DB LOAD] No MESSAGES attachment found")
null null
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("ReplyDebug", "❌ [DB LOAD] Failed to parse reply from attachments:", e)
null null
} }
} }
@@ -1124,13 +1055,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
var replyBlobForDatabase = "" // Зашифрованный blob для БД (приватным ключом) var replyBlobForDatabase = "" // Зашифрованный blob для БД (приватным ключом)
if (replyMsgsToSend.isNotEmpty()) { if (replyMsgsToSend.isNotEmpty()) {
android.util.Log.d("ReplyDebug", "📤 [SEND] Creating reply attachment:")
android.util.Log.d("ReplyDebug", " - Reply messages count: ${replyMsgsToSend.size}")
// Формируем JSON массив с цитируемыми сообщениями (как в RN) // Формируем JSON массив с цитируемыми сообщениями (как в RN)
val replyJsonArray = JSONArray() val replyJsonArray = JSONArray()
replyMsgsToSend.forEach { msg -> replyMsgsToSend.forEach { msg ->
android.util.Log.d("ReplyDebug", " - Adding reply: id=${msg.messageId}, publicKey=${msg.publicKey.take(20)}..., text=${msg.text.take(30)}")
val replyJson = JSONObject().apply { val replyJson = JSONObject().apply {
put("message_id", msg.messageId) put("message_id", msg.messageId)
put("publicKey", msg.publicKey) put("publicKey", msg.publicKey)
@@ -1142,18 +1070,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
val replyBlobPlaintext = replyJsonArray.toString() val replyBlobPlaintext = replyJsonArray.toString()
android.util.Log.d("ReplyDebug", " - Reply blob plaintext length: ${replyBlobPlaintext.length}")
android.util.Log.d("ReplyDebug", " - Reply blob preview: ${replyBlobPlaintext.take(100)}")
// 🔥 Шифруем reply blob (для network transmission) с ChaCha ключом // 🔥 Шифруем reply blob (для network transmission) с ChaCha ключом
android.util.Log.d("ReplyDebug", " - Encrypting reply blob with plainKeyAndNonce (${plainKeyAndNonce.size} bytes)")
val encryptedReplyBlob = MessageCrypto.encryptReplyBlob(replyBlobPlaintext, plainKeyAndNonce) val encryptedReplyBlob = MessageCrypto.encryptReplyBlob(replyBlobPlaintext, plainKeyAndNonce)
android.util.Log.d("ReplyDebug", " - Encrypted blob length: ${encryptedReplyBlob.length}")
android.util.Log.d("ReplyDebug", " - Encrypted blob preview: ${encryptedReplyBlob.take(100)}")
// 🔥 Re-encrypt с приватным ключом для хранения в БД (как в Desktop Архиве) // 🔥 Re-encrypt с приватным ключом для хранения в БД (как в Desktop Архиве)
replyBlobForDatabase = CryptoManager.encryptWithPassword(replyBlobPlaintext, privateKey) replyBlobForDatabase = CryptoManager.encryptWithPassword(replyBlobPlaintext, privateKey)
android.util.Log.d("ReplyDebug", " - Re-encrypted for DB length: ${replyBlobForDatabase.length}")
val replyAttachmentId = "reply_${timestamp}" val replyAttachmentId = "reply_${timestamp}"
messageAttachments.add(MessageAttachment( messageAttachments.add(MessageAttachment(
@@ -1162,7 +1084,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
type = AttachmentType.MESSAGES, type = AttachmentType.MESSAGES,
preview = "" preview = ""
)) ))
android.util.Log.d("ReplyDebug", "✅ [SEND] Reply attachment added to packet (encrypted)")
} }
val packet = PacketMessage().apply { val packet = PacketMessage().apply {
@@ -1177,17 +1098,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
// 🔥 DEBUG: Log packet before sending // 🔥 DEBUG: Log packet before sending
android.util.Log.d("ReplyDebug", "📤 [SEND] About to send packet:")
android.util.Log.d("ReplyDebug", " - messageId: $messageId")
android.util.Log.d("ReplyDebug", " - from: ${sender.take(20)}...")
android.util.Log.d("ReplyDebug", " - to: ${recipient.take(20)}...")
android.util.Log.d("ReplyDebug", " - attachments count: ${packet.attachments.size}")
packet.attachments.forEachIndexed { idx, att -> packet.attachments.forEachIndexed { idx, att ->
android.util.Log.d("ReplyDebug", " - Attachment $idx:")
android.util.Log.d("ReplyDebug", " type: ${att.type.value}")
android.util.Log.d("ReplyDebug", " id: ${att.id}")
android.util.Log.d("ReplyDebug", " blob length: ${att.blob.length}")
android.util.Log.d("ReplyDebug", " blob preview: ${att.blob.take(100)}")
} }
// Отправляем пакет // Отправляем пакет

View File

@@ -186,7 +186,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
"$opponentKey:$currentAccount" "$opponentKey:$currentAccount"
} }
android.util.Log.d("ChatsListViewModel", "Deleting dialog with key: $dialogKey")
// Удаляем все сообщения из диалога по dialog_key // Удаляем все сообщения из диалога по dialog_key
database.messageDao().deleteDialog( database.messageDao().deleteDialog(
@@ -205,9 +204,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
opponentKey = opponentKey opponentKey = opponentKey
) )
android.util.Log.d("ChatsListViewModel", "Dialog deleted successfully: $opponentKey")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error deleting dialog", e)
// В случае ошибки - возвращаем диалог обратно (откатываем оптимистичное обновление) // В случае ошибки - возвращаем диалог обратно (откатываем оптимистичное обновление)
// Flow обновится автоматически из БД // Flow обновится автоматически из БД
} }
@@ -227,7 +224,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
) )
) )
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error blocking user", e)
} }
} }
@@ -240,7 +236,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
try { try {
database.blacklistDao().unblockUser(publicKey, currentAccount) database.blacklistDao().unblockUser(publicKey, currentAccount)
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error unblocking user", e)
} }
} }

View File

@@ -269,11 +269,7 @@ fun AppleEmojiTextField(
editTextView = this editTextView = this
// Подключаем callback для изменения фокуса // Подключаем callback для изменения фокуса
setOnFocusChangeListener { _, hasFocus -> setOnFocusChangeListener { _, hasFocus ->
android.util.Log.d("AppleEmojiTextField", "═══════════════════════════════════════════════════════")
android.util.Log.d("AppleEmojiTextField", "🎯 Native EditText focus changed: hasFocus=$hasFocus")
android.util.Log.d("AppleEmojiTextField", " 📍 Calling onFocusChanged callback...")
onFocusChanged?.invoke(hasFocus) onFocusChanged?.invoke(hasFocus)
android.util.Log.d("AppleEmojiTextField", " ✅ onFocusChanged callback completed")
} }
// Уведомляем о создании view // Уведомляем о создании view
onViewCreated?.invoke(this) onViewCreated?.invoke(this)

View File

@@ -36,11 +36,6 @@ object KeyboardHeightProvider {
val savedPx = prefs.getInt(KEY_KEYBOARD_HEIGHT, defaultPx) val savedPx = prefs.getInt(KEY_KEYBOARD_HEIGHT, defaultPx)
val isDefault = savedPx == defaultPx val isDefault = savedPx == defaultPx
android.util.Log.d(TAG, "═══════════════════════════════════════")
android.util.Log.d(TAG, "📖 getSavedKeyboardHeight()")
android.util.Log.d(TAG, " 📏 Height: ${savedPx}px (${pxToDp(context, savedPx)}dp)")
android.util.Log.d(TAG, " 📦 Source: ${if (isDefault) "DEFAULT" else "SAVED"}")
android.util.Log.d(TAG, "═══════════════════════════════════════")
return savedPx return savedPx
} }
@@ -48,8 +43,6 @@ object KeyboardHeightProvider {
* Сохранить высоту клавиатуры * Сохранить высоту клавиатуры
*/ */
fun saveKeyboardHeight(context: Context, heightPx: Int) { fun saveKeyboardHeight(context: Context, heightPx: Int) {
android.util.Log.d(TAG, "═══════════════════════════════════════")
android.util.Log.d(TAG, "💾 saveKeyboardHeight($heightPx px)")
if (heightPx > 0) { if (heightPx > 0) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
@@ -58,19 +51,12 @@ object KeyboardHeightProvider {
prefs.edit().putInt(KEY_KEYBOARD_HEIGHT, heightPx).apply() prefs.edit().putInt(KEY_KEYBOARD_HEIGHT, heightPx).apply()
android.util.Log.d(TAG, " 📏 New: ${heightPx}px (${pxToDp(context, heightPx)}dp)")
android.util.Log.d(TAG, " 📏 Old: ${oldHeight}px (${pxToDp(context, oldHeight)}dp)")
android.util.Log.d(TAG, " 🔄 Changed: $changed")
if (changed) { if (changed) {
android.util.Log.d(TAG, " ✅ SAVED successfully!")
} else { } else {
android.util.Log.d(TAG, " ⏭️ Same height, no change")
} }
} else { } else {
android.util.Log.w(TAG, " ⚠️ INVALID height: ${heightPx}px - NOT saved!")
} }
android.util.Log.d(TAG, "═══════════════════════════════════════")
} }
/** /**
@@ -78,7 +64,6 @@ object KeyboardHeightProvider {
* Telegram использует: AdjustPanLayoutHelper.keyboardDuration (250ms обычно) * Telegram использует: AdjustPanLayoutHelper.keyboardDuration (250ms обычно)
*/ */
fun getKeyboardAnimationDuration(): Long { fun getKeyboardAnimationDuration(): Long {
android.util.Log.d(TAG, "⏱️ getKeyboardAnimationDuration() = 250ms")
return 250L return 250L
} }
@@ -107,9 +92,6 @@ fun rememberSavedKeyboardHeight(): Dp {
val density = context.resources.displayMetrics.density val density = context.resources.displayMetrics.density
val heightDp = (heightPx / density).dp val heightDp = (heightPx / density).dp
android.util.Log.d("KeyboardHeight", "🎯 rememberSavedKeyboardHeight()")
android.util.Log.d("KeyboardHeight", " 📏 Result: $heightDp (${heightPx}px)")
android.util.Log.d("KeyboardHeight", " 📱 Density: $density")
return heightDp return heightDp
} }

View File

@@ -61,11 +61,9 @@ object OptimizedEmojiCache {
loadProgress = 1f loadProgress = 1f
} }
android.util.Log.d("EmojiCache", "✅ Предзагрузка завершена за $duration ms")
isLoaded = true isLoaded = true
isPreloading = false isPreloading = false
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("EmojiCache", "❌ Ошибка предзагрузки", e)
allEmojis = emptyList() allEmojis = emptyList()
emojisByCategory = emptyMap() emojisByCategory = emptyMap()
isLoaded = true isLoaded = true
@@ -85,7 +83,6 @@ object OptimizedEmojiCache {
?: emptyList() ?: emptyList()
allEmojis = emojis allEmojis = emojis
android.util.Log.d("EmojiCache", "📦 Загружено ${emojis.size} эмодзи")
} }
/** /**
@@ -125,7 +122,6 @@ object OptimizedEmojiCache {
} }
} }
android.util.Log.d("EmojiCache", "🗂️ Эмодзи сгруппированы по ${result.size} категориям")
} }
/** /**
@@ -140,7 +136,6 @@ object OptimizedEmojiCache {
?.take(PRELOAD_COUNT) ?.take(PRELOAD_COUNT)
?: emptyList() ?: emptyList()
android.util.Log.d("EmojiCache", "🎨 Предзагружаем ${smileysToPreload.size} популярных эмодзи...")
// Предзагружаем параллельно, но с ограничением // Предзагружаем параллельно, но с ограничением
val jobs = smileysToPreload.chunked(20).map { chunk -> val jobs = smileysToPreload.chunked(20).map { chunk ->
@@ -164,7 +159,6 @@ object OptimizedEmojiCache {
} }
jobs.awaitAll() jobs.awaitAll()
android.util.Log.d("EmojiCache", "✅ Предзагружено $preloadedCount изображений")
isPreloading = false isPreloading = false
} }

View File

@@ -70,7 +70,6 @@ fun OptimizedEmojiPicker(
// 🔥 Логирование изменений видимости // 🔥 Логирование изменений видимости
LaunchedEffect(isVisible) { LaunchedEffect(isVisible) {
android.util.Log.d("EmojiPicker", "🎭 OptimizedEmojiPicker visibility: $isVisible (height=${savedKeyboardHeight})")
} }
// 🔥 Рендерим контент напрямую без AnimatedVisibility // 🔥 Рендерим контент напрямую без AnimatedVisibility
@@ -102,19 +101,15 @@ private fun EmojiPickerContent(
var shouldRenderContent by remember { mutableStateOf(false) } var shouldRenderContent by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
android.util.Log.d("EmojiPicker", "🚀 EmojiPickerContent started, keyboardHeight=$keyboardHeight")
// Ждём 1 кадр чтобы анимация началась плавно // Ждём 1 кадр чтобы анимация началась плавно
kotlinx.coroutines.delay(16) // ~1 frame at 60fps kotlinx.coroutines.delay(16) // ~1 frame at 60fps
shouldRenderContent = true shouldRenderContent = true
android.util.Log.d("EmojiPicker", "✅ Content rendering enabled after 16ms delay")
// Загружаем эмодзи если еще не загружены // Загружаем эмодзи если еще не загружены
if (!OptimizedEmojiCache.isLoaded) { if (!OptimizedEmojiCache.isLoaded) {
android.util.Log.d("EmojiPicker", "📦 Starting emoji preload...")
OptimizedEmojiCache.preload(context) OptimizedEmojiCache.preload(context)
} else { } else {
android.util.Log.d("EmojiPicker", "✅ Emojis already loaded")
} }
} }
@@ -131,11 +126,9 @@ private fun EmojiPickerContent(
// 🚀 При смене категории плавно скроллим наверх // 🚀 При смене категории плавно скроллим наверх
LaunchedEffect(selectedCategory) { LaunchedEffect(selectedCategory) {
android.util.Log.d("EmojiPicker", "📂 Category changed: ${selectedCategory.key} (${displayedEmojis.size} emojis)")
if (displayedEmojis.isNotEmpty()) { if (displayedEmojis.isNotEmpty()) {
scope.launch { scope.launch {
gridState.animateScrollToItem(0) gridState.animateScrollToItem(0)
android.util.Log.v("EmojiPicker", "⬆️ Scrolled to top")
} }
} }
} }