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,
hasExistingAccount = screen == "auth_unlock",
accounts = accountInfoList,
accountManager = accountManager,
onAuthComplete = { account ->
currentAccount = account
hasExistingAccount = true

View File

@@ -332,11 +332,6 @@ object CryptoManager {
// Decompress (zlib inflate - совместимо с pako.inflate в JS)
String(decompress(decrypted), Charsets.UTF_8)
} 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
}
}

View File

@@ -606,15 +606,10 @@ object MessageCrypto {
*/
fun encryptReplyBlob(replyJson: String, plainKeyAndNonce: ByteArray): String {
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
// which replaces invalid UTF-8 sequences with U+FFFD
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)
val deflater = java.util.zip.Deflater()
@@ -817,37 +812,25 @@ object MessageCrypto {
*/
fun decryptReplyBlob(encryptedBlob: String, plainKeyAndNonce: ByteArray): String {
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 ':')
if (!encryptedBlob.contains(':')) {
android.util.Log.d("ReplyDebug", " - No ':' found, returning as-is")
return encryptedBlob
}
// Parse ivBase64:ciphertextBase64
val parts = encryptedBlob.split(':')
if (parts.size != 2) {
android.util.Log.d("ReplyDebug", " - Invalid format (not 2 parts), returning as-is")
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 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
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
// CRITICAL: Must use SHA256 to match React Native (not SHA1!)
@@ -861,7 +844,6 @@ object MessageCrypto {
val secretKey = factory.generateSecret(spec)
val keyBytes = secretKey.encoded
android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}")
// AES-CBC decryption
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
@@ -870,7 +852,6 @@ object MessageCrypto {
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val decompressed = cipher.doFinal(ciphertext)
android.util.Log.d("ReplyDebug", " - Decrypted (compressed) size: ${decompressed.size}")
// Decompress with inflate
val inflater = java.util.zip.Inflater()
@@ -880,14 +861,9 @@ object MessageCrypto {
inflater.end()
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
} 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
encryptedBlob
}

View File

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

View File

@@ -5,6 +5,7 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.runtime.*
import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.AccountManager
enum class AuthScreen {
SELECT_ACCOUNT,
@@ -21,6 +22,7 @@ fun AuthFlow(
isDarkTheme: Boolean,
hasExistingAccount: Boolean,
accounts: List<AccountInfo> = emptyList(),
accountManager: AccountManager,
onAuthComplete: (DecryptedAccount?) -> Unit,
onLogout: () -> Unit = {}
) {
@@ -33,7 +35,12 @@ fun AuthFlow(
)
}
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) }
// Handle system back button

View File

@@ -333,18 +333,12 @@ fun ChatDetailScreen(
// Логирование изменений selection mode
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 активировался
// (клавиатура уже должна быть закрыта в onLongClick, это только backup)
LaunchedEffect(isSelectionMode) {
if (isSelectionMode) {
android.util.Log.d("ChatDetailScreen", "⚠️ Backup keyboard hide triggered")
// Backup закрытие клавиатуры (основное в onLongClick)
keyboardController?.hide()
}
@@ -445,13 +439,9 @@ fun ChatDetailScreen(
// 🔥 Функция для скролла к сообщению с подсветкой
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 сообщений для отладки
messagesWithDates.forEachIndexed { index, pair ->
android.util.Log.d("ChatDetail", " - [$index] id='${pair.first.id}', text='${pair.first.text.take(20)}...'")
}
scope.launch {
@@ -461,18 +451,15 @@ fun ChatDetailScreen(
// Находим индекс сообщения в списке
val messageIndex = messagesWithDates.indexOfFirst { it.first.id == messageId }
android.util.Log.d("ChatDetail", " - Found at index: $messageIndex")
if (messageIndex != -1) {
// Скроллим к сообщению
listState.animateScrollToItem(messageIndex)
android.util.Log.d("ChatDetail", " ✅ Scrolled to message")
// Подсвечиваем на 2 секунды
highlightedMessageId = messageId
delay(2000)
highlightedMessageId = null
} else {
android.util.Log.d("ChatDetail", " ❌ Message not found in list")
}
}
}
@@ -1018,14 +1005,6 @@ fun ChatDetailScreen(
// Логирование состояния
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) {
@@ -1057,7 +1036,6 @@ fun ChatDetailScreen(
},
label = "bottomBarContent"
) { selectionMode ->
android.util.Log.d("ChatDetailScreen", "🎬 AnimatedContent to selectionMode=$selectionMode")
if (selectionMode) {
// SELECTION ACTION BAR - Reply/Forward
@@ -1360,19 +1338,13 @@ fun ChatDetailScreen(
isSelected = selectedMessages.contains(selectionKey),
isHighlighted = highlightedMessageId == message.id,
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)
if (!isSelectionMode) {
android.util.Log.d("ChatDetailScreen", " ⌨️ Closing keyboard...")
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
focusManager.clearFocus()
showEmojiPicker = false
android.util.Log.d("ChatDetailScreen", " ✅ Keyboard closed")
}
// Toggle selection on long press
selectedMessages = if (selectedMessages.contains(selectionKey)) {
@@ -1381,8 +1353,6 @@ fun ChatDetailScreen(
selectedMessages + selectionKey
}
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size AFTER: ${selectedMessages.size}")
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
},
onClick = {
// If in selection mode, toggle selection
@@ -1905,7 +1875,6 @@ private fun MessageBubble(
isOutgoing = message.isOutgoing,
isDarkTheme = isDarkTheme,
onClick = {
android.util.Log.d("ChatDetail", "🖱️ Reply clicked: ${reply.messageId}")
onReplyClick(reply.messageId)
}
)
@@ -2238,22 +2207,16 @@ private fun MessageInputBar(
// 🔥 Автофокус при открытии reply панели
LaunchedEffect(hasReply, editTextView) {
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
kotlinx.coroutines.delay(50)
editTextView?.let { editText ->
// 🔥 НЕ открываем клавиатуру если emoji уже открыт
if (!showEmojiPicker) {
android.util.Log.d("EmojiPicker", " ⌨️ Requesting focus and keyboard for reply...")
editText.requestFocus()
// Открываем клавиатуру
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
android.util.Log.d("EmojiPicker", " ✅ Auto-opened keyboard for reply")
} 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
if (timeSinceLastToggle < toggleCooldownMs) {
android.util.Log.d("EmojiPicker", "⏸️ Toggle blocked: ${timeSinceLastToggle}ms < ${toggleCooldownMs}ms")
return
}
@@ -2364,45 +2326,34 @@ private fun MessageInputBar(
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.isEmojiVisible вместо showEmojiPicker для более точного состояния
if (coordinator.isEmojiVisible) {
// ========== EMOJI → KEYBOARD ==========
android.util.Log.d("EmojiPicker", "🔄 Switching: Emoji → Keyboard")
coordinator.requestShowKeyboard(
showKeyboard = {
editTextView?.let { editText ->
editText.requestFocus()
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
android.util.Log.d("EmojiPicker", "📱 Keyboard show requested")
}
},
hideEmoji = {
onToggleEmojiPicker(false)
android.util.Log.d("EmojiPicker", "😊 Emoji panel hidden")
}
)
} else {
// ========== KEYBOARD → EMOJI ==========
android.util.Log.d("EmojiPicker", "🔄 Switching: Keyboard → Emoji")
coordinator.requestShowEmoji(
hideKeyboard = {
imm.hideSoftInputFromWindow(view.windowToken, 0)
android.util.Log.d("EmojiPicker", "⌨️ Keyboard hide requested")
},
showEmoji = {
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)
@@ -2643,24 +2594,12 @@ private fun MessageInputBar(
editTextView = view
},
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
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)
android.util.Log.d("EmojiPicker", " ✅ Emoji close requested")
} else if (hasFocus && !showEmojiPicker) {
android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior")
} 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
if (att.type == AttachmentType.MESSAGES && att.blob.isNotEmpty()) {
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)
// Desktop использует chachaDecryptedKey.toString('utf-8') = полные 56 байт!
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 в БД
blobToStore = decryptedBlob
// Парсим JSON массив с цитируемыми сообщениями
val replyArray = JSONArray(decryptedBlob)
android.util.Log.d("ReplyDebug", " - Reply array length: ${replyArray.length()}")
if (replyArray.length() > 0) {
val firstReply = replyArray.getJSONObject(0)
val replyPublicKey = firstReply.optString("publicKey", "")
val replyText = firstReply.optString("message", "")
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
android.util.Log.d("ReplyDebug", " - Is reply from me: $isReplyFromMe")
replyData = ReplyData(
messageId = replyMessageId,
@@ -266,11 +254,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
text = replyText,
isFromMe = isReplyFromMe
)
android.util.Log.d("ReplyDebug", "✅ [RECEIVE] Reply data created successfully")
}
} 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 hasForward = forwardMessages.isNotEmpty()
if (hasForward) {
android.util.Log.d("ChatViewModel", "📨 Will apply ${forwardMessages.size} forward messages")
}
// Сбрасываем состояние
@@ -380,7 +364,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 📨 Применяем Forward сообщения СРАЗУ после сброса
if (hasForward) {
android.util.Log.d("ChatViewModel", "📨 Applying ${forwardMessages.size} forward messages NOW")
// Конвертируем ForwardMessage в ReplyMessage
_replyMessages.value = forwardMessages.map { fm ->
ReplyMessage(
@@ -392,7 +375,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
)
}
_isForwardMode.value = true
android.util.Log.d("ChatViewModel", "📨 isForwardMode set to TRUE, replyMessages.size = ${_replyMessages.value.size}")
// Очищаем ForwardManager после применения
ForwardManager.clear()
} else {
@@ -446,7 +428,21 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
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) {
withContext(Dispatchers.Main.immediate) {
_isLoading.value = true // Показываем скелетон
@@ -455,9 +451,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
// 🔍 Проверяем общее количество сообщений в диалоге
val totalCount = messageDao.getMessageCount(account, dialogKey)
// 🔥 Получаем первую страницу - БЕЗ suspend задержки
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 {
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 {
@@ -677,35 +666,27 @@ 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,
@@ -758,139 +739,98 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
content: String,
chachaKey: String
): 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 == "[]") {
android.util.Log.d("ReplyDebug", " - Early return: attachments empty or []")
return null
}
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)
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", " - type: $type")
// MESSAGES = 1 (цитата)
if (type == 1) {
android.util.Log.d("ReplyDebug", " - ✅ Found MESSAGES type!")
// Данные могут быть в blob или 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
}
// 🔥 Проверяем формат blob - если содержит ":", то это зашифрованный формат "iv:ciphertext"
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 (iv:ciphertext format)")
val privateKey = myPrivateKey
var decryptionSuccess = false
// 🔥 Способ 1: Пробуем расшифровать с приватным ключом (для исходящих сообщений)
if (privateKey != null) {
android.util.Log.d("ReplyDebug", " - Attempting to decrypt with private key...")
try {
val decrypted = CryptoManager.decryptWithPassword(dataJson, privateKey)
if (decrypted != null) {
android.util.Log.d("ReplyDebug", " - ✅ Decrypted with private key")
android.util.Log.d("ReplyDebug", " - Decrypted preview: ${decrypted.take(200)}")
dataJson = decrypted
decryptionSuccess = true
}
} catch (e: Exception) {
android.util.Log.d("ReplyDebug", " - Private key decryption failed: ${e.message}")
}
}
// 🔥 Способ 2: Пробуем расшифровать с ChaCha ключом сообщения (для входящих)
if (!decryptionSuccess && content.isNotEmpty() && chachaKey.isNotEmpty() && privateKey != null) {
android.util.Log.d("ReplyDebug", " - Attempting to decrypt with ChaCha key...")
try {
val decrypted = MessageCrypto.decryptAttachmentBlob(dataJson, chachaKey, privateKey)
if (decrypted != null) {
android.util.Log.d("ReplyDebug", " - ✅ Decrypted with ChaCha key")
android.util.Log.d("ReplyDebug", " - Decrypted preview: ${decrypted.take(200)}")
dataJson = decrypted
decryptionSuccess = true
}
} catch (e: Exception) {
android.util.Log.d("ReplyDebug", " - ChaCha decryption failed: ${e.message}")
}
}
// 🔥 Способ 3: Пробуем decryptReplyBlob с plainKeyAndNonce
if (!decryptionSuccess && content.isNotEmpty() && chachaKey.isNotEmpty() && privateKey != null) {
android.util.Log.d("ReplyDebug", " - Attempting to decrypt with plainKeyAndNonce...")
try {
val decryptResult = MessageCrypto.decryptIncomingFull(content, chachaKey, privateKey)
val plainKeyAndNonce = decryptResult.plainKeyAndNonce
val decrypted = MessageCrypto.decryptReplyBlob(dataJson, plainKeyAndNonce)
if (decrypted.isNotEmpty()) {
android.util.Log.d("ReplyDebug", " - ✅ Decrypted with plainKeyAndNonce")
android.util.Log.d("ReplyDebug", " - Decrypted preview: ${decrypted.take(200)}")
dataJson = decrypted
decryptionSuccess = true
}
} catch (e: Exception) {
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce decryption failed: ${e.message}")
}
}
if (!decryptionSuccess) {
android.util.Log.e("ReplyDebug", " - ❌ All decryption methods failed")
continue
}
} else {
android.util.Log.d("ReplyDebug", " - 📄 Blob is PLAINTEXT (not encrypted)")
}
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 replyMessageIdFromJson = replyMessage.optString("message_id", "")
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 БД!
// Пытаемся найти реальный messageId в текущих сообщениях по тексту и timestamp
@@ -906,15 +846,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
timestampTo = replyTimestamp + 5000
)?.messageId ?: replyMessageIdFromJson
} catch (e: Exception) {
android.util.Log.w("ReplyDebug", " - ⚠️ Could not find real messageId, using JSON id")
replyMessageIdFromJson
}
android.util.Log.d("ReplyDebug", " - Real messageId: $realMessageId")
// Определяем, кто автор цитируемого сообщения
val isReplyFromMe = replyPublicKey == myPublicKey
android.util.Log.d("ReplyDebug", " - isReplyFromMe: $isReplyFromMe")
val result = ReplyData(
messageId = realMessageId,
@@ -922,20 +859,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
text = replyText,
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
} 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 LOAD] No MESSAGES attachment found")
null
} catch (e: Exception) {
android.util.Log.e("ReplyDebug", "❌ [DB LOAD] Failed to parse reply from attachments:", e)
null
}
}
@@ -1124,13 +1055,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
var replyBlobForDatabase = "" // Зашифрованный blob для БД (приватным ключом)
if (replyMsgsToSend.isNotEmpty()) {
android.util.Log.d("ReplyDebug", "📤 [SEND] Creating reply attachment:")
android.util.Log.d("ReplyDebug", " - Reply messages count: ${replyMsgsToSend.size}")
// Формируем JSON массив с цитируемыми сообщениями (как в RN)
val replyJsonArray = JSONArray()
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 {
put("message_id", msg.messageId)
put("publicKey", msg.publicKey)
@@ -1142,18 +1070,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
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 ключом
android.util.Log.d("ReplyDebug", " - Encrypting reply blob with plainKeyAndNonce (${plainKeyAndNonce.size} bytes)")
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 Архиве)
replyBlobForDatabase = CryptoManager.encryptWithPassword(replyBlobPlaintext, privateKey)
android.util.Log.d("ReplyDebug", " - Re-encrypted for DB length: ${replyBlobForDatabase.length}")
val replyAttachmentId = "reply_${timestamp}"
messageAttachments.add(MessageAttachment(
@@ -1162,7 +1084,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
type = AttachmentType.MESSAGES,
preview = ""
))
android.util.Log.d("ReplyDebug", "✅ [SEND] Reply attachment added to packet (encrypted)")
}
val packet = PacketMessage().apply {
@@ -1177,17 +1098,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
// 🔥 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 ->
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"
}
android.util.Log.d("ChatsListViewModel", "Deleting dialog with key: $dialogKey")
// Удаляем все сообщения из диалога по dialog_key
database.messageDao().deleteDialog(
@@ -205,9 +204,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
opponentKey = opponentKey
)
android.util.Log.d("ChatsListViewModel", "Dialog deleted successfully: $opponentKey")
} catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error deleting dialog", e)
// В случае ошибки - возвращаем диалог обратно (откатываем оптимистичное обновление)
// Flow обновится автоматически из БД
}
@@ -227,7 +224,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
)
)
} catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error blocking user", e)
}
}
@@ -240,7 +236,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
try {
database.blacklistDao().unblockUser(publicKey, currentAccount)
} catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error unblocking user", e)
}
}

View File

@@ -269,11 +269,7 @@ fun AppleEmojiTextField(
editTextView = this
// Подключаем callback для изменения фокуса
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)
android.util.Log.d("AppleEmojiTextField", " ✅ onFocusChanged callback completed")
}
// Уведомляем о создании view
onViewCreated?.invoke(this)

View File

@@ -36,11 +36,6 @@ object KeyboardHeightProvider {
val savedPx = prefs.getInt(KEY_KEYBOARD_HEIGHT, 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
}
@@ -48,8 +43,6 @@ object KeyboardHeightProvider {
* Сохранить высоту клавиатуры
*/
fun saveKeyboardHeight(context: Context, heightPx: Int) {
android.util.Log.d(TAG, "═══════════════════════════════════════")
android.util.Log.d(TAG, "💾 saveKeyboardHeight($heightPx px)")
if (heightPx > 0) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
@@ -58,19 +51,12 @@ object KeyboardHeightProvider {
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) {
android.util.Log.d(TAG, " ✅ SAVED successfully!")
} else {
android.util.Log.d(TAG, " ⏭️ Same height, no change")
}
} 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 обычно)
*/
fun getKeyboardAnimationDuration(): Long {
android.util.Log.d(TAG, "⏱️ getKeyboardAnimationDuration() = 250ms")
return 250L
}
@@ -107,9 +92,6 @@ fun rememberSavedKeyboardHeight(): Dp {
val density = context.resources.displayMetrics.density
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
}

View File

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

View File

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