feat: Optimize coroutine usage in ChatViewModel for improved performance and responsiveness & FIX LAGS

This commit is contained in:
k1ngsterr1
2026-01-13 21:56:15 +05:00
parent 145a3621a1
commit 14ef342e80
6 changed files with 45 additions and 135 deletions

View File

@@ -24,7 +24,8 @@ object ProtocolManager {
private var messageRepository: MessageRepository? = null private var messageRepository: MessageRepository? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Debug logs for dev console // Debug logs for dev console - 🚀 ОТКЛЮЧЕНО для производительности
// Логи только в Logcat, не в StateFlow (это вызывало ANR!)
private val _debugLogs = MutableStateFlow<List<String>>(emptyList()) private val _debugLogs = MutableStateFlow<List<String>>(emptyList())
val debugLogs: StateFlow<List<String>> = _debugLogs.asStateFlow() val debugLogs: StateFlow<List<String>> = _debugLogs.asStateFlow()
@@ -34,11 +35,23 @@ object ProtocolManager {
private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()) private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
// 🚀 Флаг для включения UI логов (по умолчанию ВЫКЛЮЧЕНО - это вызывало ANR!)
private var uiLogsEnabled = false
fun addLog(message: String) { fun addLog(message: String) {
val timestamp = dateFormat.format(Date()) val timestamp = dateFormat.format(Date())
val logLine = "[$timestamp] $message" val logLine = "[$timestamp] $message"
// Только Logcat - быстро и не блокирует UI
Log.d(TAG, logLine) Log.d(TAG, logLine)
_debugLogs.value = (_debugLogs.value + logLine).takeLast(100)
// UI логи отключены по умолчанию - вызывали ANR из-за перекомпозиций
if (uiLogsEnabled) {
_debugLogs.value = (_debugLogs.value + logLine).takeLast(50)
}
}
fun enableUILogs(enabled: Boolean) {
uiLogsEnabled = enabled
} }
fun clearLogs() { fun clearLogs() {

View File

@@ -296,7 +296,12 @@ fun ChatDetailScreen(
// Состояние показа логов // Состояние показа логов
var showLogs by remember { mutableStateOf(false) } var showLogs by remember { mutableStateOf(false) }
val debugLogs by com.rosetta.messenger.network.ProtocolManager.debugLogs.collectAsState() // 🚀 Собираем логи ТОЛЬКО когда они показываются - иначе каждый лог вызывает перекомпозицию!
val debugLogs = if (showLogs) {
com.rosetta.messenger.network.ProtocolManager.debugLogs.collectAsState().value
} else {
emptyList()
}
// Состояние выпадающего меню // Состояние выпадающего меню
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }

View File

@@ -126,15 +126,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Входящие сообщения // Входящие сообщения
ProtocolManager.waitPacket(0x06) { packet -> ProtocolManager.waitPacket(0x06) { packet ->
val msgPacket = packet as PacketMessage val msgPacket = packet as PacketMessage
ProtocolManager.addLog("📨 ChatVM got packet 0x06: from=${msgPacket.fromPublicKey.take(16)}, to=${msgPacket.toPublicKey.take(16)}")
ProtocolManager.addLog("📨 opponentKey=${opponentKey?.take(16) ?: "NULL"}")
if (msgPacket.fromPublicKey == opponentKey || msgPacket.toPublicKey == opponentKey) { if (msgPacket.fromPublicKey == opponentKey || msgPacket.toPublicKey == opponentKey) {
ProtocolManager.addLog("📨 ✅ Match! Processing message...") viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch {
handleIncomingMessage(msgPacket) handleIncomingMessage(msgPacket)
} }
} else { } else {
ProtocolManager.addLog("📨 ❌ No match, ignoring")
} }
} }
@@ -148,7 +144,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
updateMessageStatus(deliveryPacket.messageId, MessageStatus.DELIVERED) updateMessageStatus(deliveryPacket.messageId, MessageStatus.DELIVERED)
} }
ProtocolManager.addLog("✓ Delivered: ${deliveryPacket.messageId.take(8)}...")
} }
} }
@@ -172,7 +167,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} else msg } else msg
} }
} }
ProtocolManager.addLog("✓✓ Read receipt from: ${readPacket.fromPublicKey.take(8)}...")
} }
} }
} }
@@ -180,34 +174,24 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Typing // Typing
ProtocolManager.waitPacket(0x0B) { packet -> ProtocolManager.waitPacket(0x0B) { packet ->
val typingPacket = packet as PacketTyping val typingPacket = packet as PacketTyping
ProtocolManager.addLog("⌨️ TYPING received from: ${typingPacket.fromPublicKey.take(16)}...")
ProtocolManager.addLog(" My opponent: ${opponentKey?.take(16)}...")
if (typingPacket.fromPublicKey == opponentKey) { if (typingPacket.fromPublicKey == opponentKey) {
ProtocolManager.addLog(" ✅ Match! Showing typing indicator")
showTypingIndicator() showTypingIndicator()
} else { } else {
ProtocolManager.addLog(" ❌ No match, ignoring")
} }
} }
// 🟢 Онлайн статус (массив publicKey+state как в React Native) // 🟢 Онлайн статус (массив publicKey+state как в React Native)
ProtocolManager.waitPacket(0x05) { packet -> ProtocolManager.waitPacket(0x05) { packet ->
val onlinePacket = packet as PacketOnlineState val onlinePacket = packet as PacketOnlineState
ProtocolManager.addLog("🟢 ONLINE STATUS received: ${onlinePacket.publicKeysState.size} entries")
onlinePacket.publicKeysState.forEach { item -> onlinePacket.publicKeysState.forEach { item ->
ProtocolManager.addLog(" Key: ${item.publicKey.take(16)}... State: ${item.state}")
ProtocolManager.addLog(" My opponent: ${opponentKey?.take(16)}...")
if (item.publicKey == opponentKey) { if (item.publicKey == opponentKey) {
ProtocolManager.addLog(" ✅ Match! Updating UI - online: ${item.state == OnlineState.ONLINE}")
viewModelScope.launch {
_opponentOnline.value = item.state == OnlineState.ONLINE _opponentOnline.value = item.state == OnlineState.ONLINE
} }
} }
} }
} }
}
private fun handleIncomingMessage(packet: PacketMessage) { private fun handleIncomingMessage(packet: PacketMessage) {
// 🚀 Обработка входящего сообщения в IO потоке // 🚀 Обработка входящего сообщения в IO потоке
@@ -216,8 +200,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val privateKey = myPrivateKey ?: return@launch val privateKey = myPrivateKey ?: return@launch
val account = myPublicKey ?: return@launch val account = myPublicKey ?: return@launch
ProtocolManager.addLog("📩 Incoming message: ${packet.messageId.take(8)}...")
ProtocolManager.addLog("📎 Attachments count: ${packet.attachments.size}")
// Расшифровываем в фоне - получаем и текст и plainKeyAndNonce // Расшифровываем в фоне - получаем и текст и plainKeyAndNonce
val decryptResult = MessageCrypto.decryptIncomingFull( val decryptResult = MessageCrypto.decryptIncomingFull(
@@ -231,14 +213,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Кэшируем расшифрованный текст // Кэшируем расшифрованный текст
decryptionCache[packet.messageId] = decryptedText decryptionCache[packet.messageId] = decryptedText
ProtocolManager.addLog("✅ Decrypted: ${decryptedText.take(20)}...")
// 🔥 Парсим reply из attachments (как в React Native) // 🔥 Парсим reply из attachments (как в React Native)
var replyData: ReplyData? = null var replyData: ReplyData? = null
val attachmentsJson = if (packet.attachments.isNotEmpty()) { val attachmentsJson = if (packet.attachments.isNotEmpty()) {
val jsonArray = JSONArray() val jsonArray = JSONArray()
for (att in packet.attachments) { for (att in packet.attachments) {
ProtocolManager.addLog("📎 Attachment type: ${att.type}, blob size: ${att.blob.length}")
// Если это MESSAGES (reply) - парсим и расшифровываем данные // Если это MESSAGES (reply) - парсим и расшифровываем данные
var blobToStore = att.blob // По умолчанию сохраняем оригинальный blob var blobToStore = att.blob // По умолчанию сохраняем оригинальный blob
@@ -246,7 +226,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
try { try {
// 🔥 Сначала расшифровываем blob (он зашифрован!) // 🔥 Сначала расшифровываем blob (он зашифрован!)
val decryptedBlob = MessageCrypto.decryptReplyBlob(att.blob, plainKeyAndNonce) val decryptedBlob = MessageCrypto.decryptReplyBlob(att.blob, plainKeyAndNonce)
ProtocolManager.addLog("📎 Decrypted reply blob: ${decryptedBlob.take(100)}")
// 🔥 Сохраняем расшифрованный blob в БД // 🔥 Сохраняем расшифрованный blob в БД
blobToStore = decryptedBlob blobToStore = decryptedBlob
@@ -268,10 +247,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
text = replyText, text = replyText,
isFromMe = isReplyFromMe isFromMe = isReplyFromMe
) )
ProtocolManager.addLog("✅ Parsed reply: from=${replyData?.senderName}, text=${replyText.take(30)}")
} }
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Failed to parse reply: ${e.message}")
} }
} }
@@ -297,7 +274,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
) )
// Просто добавляем как в архиве: setMessages((prev) => ([...prev, newMessage])) // Просто добавляем как в архиве: setMessages((prev) => ([...prev, newMessage]))
_messages.value = _messages.value + message _messages.value = _messages.value + message
ProtocolManager.addLog("✅ Added to UI: ${packet.messageId.take(8)}... text: ${decryptedText.take(20)}")
} }
// 🔥 Сохраняем в БД здесь (в ChatViewModel) // 🔥 Сохраняем в БД здесь (в ChatViewModel)
@@ -308,7 +284,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔥 FIX: Если messageId пустой - генерируем новый UUID // 🔥 FIX: Если messageId пустой - генерируем новый UUID
val finalMessageId = if (packet.messageId.isNullOrEmpty()) { val finalMessageId = if (packet.messageId.isNullOrEmpty()) {
UUID.randomUUID().toString().replace("-", "").take(32).also { UUID.randomUUID().toString().replace("-", "").take(32).also {
ProtocolManager.addLog("⚠️ Empty messageId from server, generated: ${it.take(8)}...")
} }
} else { } else {
packet.messageId packet.messageId
@@ -334,7 +309,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// (через markVisibleMessagesAsRead вызываемый из ChatDetailScreen) // (через markVisibleMessagesAsRead вызываемый из ChatDetailScreen)
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Error handling incoming message: ${e.message}")
Log.e(TAG, "Incoming message error", e) Log.e(TAG, "Incoming message error", e)
} }
} }
@@ -364,7 +338,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
fun setUserKeys(publicKey: String, privateKey: String) { fun setUserKeys(publicKey: String, privateKey: String) {
myPublicKey = publicKey myPublicKey = publicKey
myPrivateKey = privateKey myPrivateKey = privateKey
ProtocolManager.addLog("🔑 Keys set: ${publicKey.take(16)}...")
} }
/** /**
@@ -373,7 +346,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
fun openDialog(publicKey: String, title: String = "", username: String = "") { fun openDialog(publicKey: String, title: String = "", username: String = "") {
// 🔥 ВСЕГДА перезагружаем данные - не кешируем, т.к. диалог мог быть удалён // 🔥 ВСЕГДА перезагружаем данные - не кешируем, т.к. диалог мог быть удалён
// if (opponentKey == publicKey) { // if (opponentKey == publicKey) {
// ProtocolManager.addLog("💬 Dialog already open: ${publicKey.take(16)}...")
// return // return
// } // }
@@ -395,7 +367,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
readReceiptSentForCurrentDialog = false readReceiptSentForCurrentDialog = false
isDialogActive = true // 🔥 Диалог активен! isDialogActive = true // 🔥 Диалог активен!
ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...")
// Подписываемся на онлайн статус // Подписываемся на онлайн статус
subscribeToOnlineStatus() subscribeToOnlineStatus()
@@ -411,7 +382,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
*/ */
fun closeDialog() { fun closeDialog() {
isDialogActive = false isDialogActive = false
ProtocolManager.addLog("💬 Dialog closed (isDialogActive = false)")
} }
/** /**
@@ -431,7 +401,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔥 МГНОВЕННАЯ загрузка из кэша если есть! // 🔥 МГНОВЕННАЯ загрузка из кэша если есть!
val cachedMessages = dialogMessagesCache[dialogKey] val cachedMessages = dialogMessagesCache[dialogKey]
if (cachedMessages != null && cachedMessages.isNotEmpty()) { if (cachedMessages != null && cachedMessages.isNotEmpty()) {
ProtocolManager.addLog("⚡ Loading ${cachedMessages.size} messages from CACHE (instant!)")
withContext(Dispatchers.Main.immediate) { withContext(Dispatchers.Main.immediate) {
_messages.value = cachedMessages _messages.value = cachedMessages
_isLoading.value = false _isLoading.value = false
@@ -452,16 +421,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
delay(delayMs) delay(delayMs)
} }
ProtocolManager.addLog("📂 Loading messages from DB for dialog: $dialogKey")
// 🔍 Проверяем общее количество сообщений в диалоге // 🔍 Проверяем общее количество сообщений в диалоге
val totalCount = messageDao.getMessageCount(account, dialogKey) val totalCount = messageDao.getMessageCount(account, dialogKey)
ProtocolManager.addLog("📂 Total messages in DB: $totalCount")
// 🔥 Получаем первую страницу - БЕЗ suspend задержки // 🔥 Получаем первую страницу - БЕЗ suspend задержки
val entities = messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0) val entities = messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0)
ProtocolManager.addLog("📂 Loaded ${entities.size} messages from DB (offset: 0, limit: $PAGE_SIZE)")
hasMoreMessages = entities.size >= PAGE_SIZE hasMoreMessages = entities.size >= PAGE_SIZE
currentOffset = entities.size currentOffset = entities.size
@@ -480,11 +446,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
ProtocolManager.addLog("📋 Decrypted and loaded ${messages.size} messages from DB")
// 🔥 Сохраняем в кэш для мгновенной повторной загрузки! // 🔥 Сохраняем в кэш для мгновенной повторной загрузки!
dialogMessagesCache[dialogKey] = messages.toList() dialogMessagesCache[dialogKey] = messages.toList()
ProtocolManager.addLog("💾 Cached ${messages.size} messages for dialog $dialogKey")
// 🔥 СРАЗУ обновляем UI - пользователь видит сообщения мгновенно // 🔥 СРАЗУ обновляем UI - пользователь видит сообщения мгновенно
// НО сохраняем оптимистичные сообщения (SENDING), которые ещё не в БД // НО сохраняем оптимистичные сообщения (SENDING), которые ещё не в БД
@@ -501,7 +465,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Отмечаем как прочитанные в БД // Отмечаем как прочитанные в БД
messageDao.markDialogAsRead(account, dialogKey) messageDao.markDialogAsRead(account, dialogKey)
dialogDao.clearUnreadCount(account, opponent) dialogDao.clearUnreadCount(account, opponent)
ProtocolManager.addLog("👁️ Marked all incoming messages as read in DB, cleared unread count")
// Отправляем read receipt собеседнику // Отправляем read receipt собеседнику
if (messages.isNotEmpty()) { if (messages.isNotEmpty()) {
@@ -515,7 +478,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
isLoadingMessages = false isLoadingMessages = false
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Error loading messages: ${e.message}")
Log.e(TAG, "Error loading messages", e) Log.e(TAG, "Error loading messages", e)
withContext(Dispatchers.Main.immediate) { withContext(Dispatchers.Main.immediate) {
_isLoading.value = false _isLoading.value = false
@@ -550,7 +512,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
withContext(Dispatchers.Main.immediate) { withContext(Dispatchers.Main.immediate) {
_messages.value = messages _messages.value = messages
} }
ProtocolManager.addLog("🔄 Refreshed: found ${messages.size - cachedMessages.size} new messages")
} }
hasMoreMessages = entities.size >= PAGE_SIZE hasMoreMessages = entities.size >= PAGE_SIZE
@@ -561,7 +522,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
dialogDao.clearUnreadCount(account, opponent) dialogDao.clearUnreadCount(account, opponent)
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Error refreshing messages: ${e.message}")
} }
} }
@@ -639,7 +599,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
encryptedKey = entity.chachaKey, encryptedKey = entity.chachaKey,
myPrivateKey = privateKey myPrivateKey = privateKey
) )
ProtocolManager.addLog("🔓 Decrypted from DB: ${decrypted.take(20)}...")
decrypted decrypted
} else { } else {
// Fallback на расшифровку plainMessage с приватным ключом // Fallback на расшифровку plainMessage с приватным ключом
@@ -647,7 +606,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
try { try {
CryptoManager.decryptWithPassword(entity.plainMessage, privateKey) ?: entity.plainMessage CryptoManager.decryptWithPassword(entity.plainMessage, privateKey) ?: entity.plainMessage
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("⚠️ plainMessage decrypt error: ${e.message}")
entity.plainMessage entity.plainMessage
} }
} else { } else {
@@ -655,7 +613,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Decrypt error: ${e.message}, trying plainMessage")
// Пробуем расшифровать plainMessage // Пробуем расшифровать plainMessage
val privateKey = myPrivateKey val privateKey = myPrivateKey
if (privateKey != null && entity.plainMessage.isNotEmpty()) { if (privateKey != null && entity.plainMessage.isNotEmpty()) {
@@ -806,7 +763,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
) )
} }
_isForwardMode.value = false _isForwardMode.value = false
ProtocolManager.addLog("📝 Reply set: ${messages.size} messages")
} }
/** /**
@@ -826,7 +782,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
) )
} }
_isForwardMode.value = true _isForwardMode.value = true
ProtocolManager.addLog("➡️ Forward set: ${messages.size} messages")
} }
/** /**
@@ -841,34 +796,30 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* 🔥 Удалить сообщение (для ошибки отправки) * 🔥 Удалить сообщение (для ошибки отправки)
*/ */
fun deleteMessage(messageId: String) { fun deleteMessage(messageId: String) {
viewModelScope.launch { // Удаляем из UI сразу на main
// Удаляем из UI
_messages.value = _messages.value.filter { it.id != messageId } _messages.value = _messages.value.filter { it.id != messageId }
// Удаляем из БД // Удаляем из БД в IO
viewModelScope.launch(Dispatchers.IO) {
val account = myPublicKey ?: return@launch val account = myPublicKey ?: return@launch
withContext(Dispatchers.IO) {
messageDao.deleteMessage(account, messageId) messageDao.deleteMessage(account, messageId)
} }
ProtocolManager.addLog("🗑️ Message deleted: ${messageId.take(8)}...")
}
} }
/** /**
* 🔥 Повторить отправку сообщения (для ошибки) * 🔥 Повторить отправку сообщения (для ошибки)
*/ */
fun retryMessage(message: ChatMessage) { fun retryMessage(message: ChatMessage) {
viewModelScope.launch {
// Удаляем старое сообщение // Удаляем старое сообщение
deleteMessage(message.id) deleteMessage(message.id)
// Устанавливаем текст в инпут и отправляем // Устанавливаем текст в инпут и отправляем
_inputText.value = message.text _inputText.value = message.text
// Небольшая задержка чтобы UI обновился // Отправляем с небольшой задержкой
viewModelScope.launch {
delay(100) delay(100)
sendMessage() sendMessage()
ProtocolManager.addLog("🔄 Retrying message: ${message.text.take(20)}...")
} }
} }
@@ -881,7 +832,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
*/ */
fun sendMessage() { fun sendMessage() {
Log.d(TAG, "🚀🚀🚀 sendMessage() CALLED 🚀🚀🚀") Log.d(TAG, "🚀🚀🚀 sendMessage() CALLED 🚀🚀🚀")
ProtocolManager.addLog("🚀🚀🚀 sendMessage() CALLED")
val text = _inputText.value.trim() val text = _inputText.value.trim()
val recipient = opponentKey val recipient = opponentKey
@@ -896,35 +846,26 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
Log.d(TAG, "🔑 PrivateKey exists: ${privateKey != null}") Log.d(TAG, "🔑 PrivateKey exists: ${privateKey != null}")
Log.d(TAG, "💬 ReplyMsgs: ${replyMsgs.size}") Log.d(TAG, "💬 ReplyMsgs: ${replyMsgs.size}")
ProtocolManager.addLog("📝 Text: '$text'")
ProtocolManager.addLog("📧 Recipient: ${recipient?.take(16)}")
ProtocolManager.addLog("👤 Sender: ${sender?.take(16)}")
ProtocolManager.addLog("🔑 PrivateKey exists: ${privateKey != null}")
// Разрешаем отправку пустого текста если есть reply/forward // Разрешаем отправку пустого текста если есть reply/forward
if (text.isEmpty() && replyMsgs.isEmpty()) { if (text.isEmpty() && replyMsgs.isEmpty()) {
Log.e(TAG, "❌ Empty text and no reply") Log.e(TAG, "❌ Empty text and no reply")
ProtocolManager.addLog("❌ Empty text and no reply")
return return
} }
if (recipient == null) { if (recipient == null) {
Log.e(TAG, "❌ No recipient") Log.e(TAG, "❌ No recipient")
ProtocolManager.addLog("❌ No recipient")
return return
} }
if (sender == null || privateKey == null) { if (sender == null || privateKey == null) {
Log.e(TAG, "❌ No keys - sender: $sender, privateKey: $privateKey") Log.e(TAG, "❌ No keys - sender: $sender, privateKey: $privateKey")
ProtocolManager.addLog("❌ No keys - set via setUserKeys()")
return return
} }
if (isSending) { if (isSending) {
Log.w(TAG, "⏳ Already sending...") Log.w(TAG, "⏳ Already sending...")
ProtocolManager.addLog("⏳ Already sending...")
return return
} }
Log.d(TAG, "✅ All checks passed, starting send...") Log.d(TAG, "✅ All checks passed, starting send...")
ProtocolManager.addLog("✅ All checks passed, starting send...")
isSending = true isSending = true
@@ -964,7 +905,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Кэшируем текст // Кэшируем текст
decryptionCache[messageId] = text decryptionCache[messageId] = text
ProtocolManager.addLog("📤 Sending: \"${text.take(20)}...\" with ${replyMsgsToSend.size} reply attachments")
// 2. 🔥 Шифрование и отправка в IO потоке // 2. 🔥 Шифрование и отправка в IO потоке
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -1007,8 +947,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
preview = "" preview = ""
)) ))
ProtocolManager.addLog("📎 Reply attachment created: ${replyBlobPlaintext.take(50)}...")
ProtocolManager.addLog("📎 Encrypted reply blob: ${encryptedReplyBlob.take(50)}...")
} }
val packet = PacketMessage().apply { val packet = PacketMessage().apply {
@@ -1023,10 +961,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
// 🔥 Log packet details before sending // 🔥 Log packet details before sending
ProtocolManager.addLog("📤📤📤 SENDING PACKET 📤📤📤")
ProtocolManager.addLog(" - Attachments count: ${packet.attachments.size}")
packet.attachments.forEach { att -> packet.attachments.forEach { att ->
ProtocolManager.addLog(" - Attachment: type=${att.type}, id=${att.id}, blob=${att.blob.take(50)}...")
} }
// Отправляем пакет // Отправляем пакет
@@ -1089,13 +1024,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔒 Шифруем lastMessage перед сохранением // 🔒 Шифруем lastMessage перед сохранением
val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey) val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey)
ProtocolManager.addLog("💾 Saving dialog: ${lastMessage.take(20)}... (encrypted)")
val existingDialog = dialogDao.getDialog(account, opponent) val existingDialog = dialogDao.getDialog(account, opponent)
if (existingDialog != null) { if (existingDialog != null) {
// Обновляем последнее сообщение // Обновляем последнее сообщение
dialogDao.updateLastMessage(account, opponent, encryptedLastMessage, timestamp) dialogDao.updateLastMessage(account, opponent, encryptedLastMessage, timestamp)
ProtocolManager.addLog("✅ Dialog updated (existing)")
} else { } else {
// Создаём новый диалог // Создаём новый диалог
dialogDao.insertDialog(DialogEntity( dialogDao.insertDialog(DialogEntity(
@@ -1106,10 +1039,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
lastMessage = encryptedLastMessage, lastMessage = encryptedLastMessage,
lastMessageTimestamp = timestamp lastMessageTimestamp = timestamp
)) ))
ProtocolManager.addLog("✅ Dialog created (new)")
} }
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Dialog save error: ${e.message}")
Log.e(TAG, "Dialog save error", e) Log.e(TAG, "Dialog save error", e)
} }
} }
@@ -1134,9 +1065,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Инкрементируем непрочитанные если нужно // Инкрементируем непрочитанные если нужно
if (incrementUnread) { if (incrementUnread) {
dialogDao.incrementUnreadCount(account, opponentKey) dialogDao.incrementUnreadCount(account, opponentKey)
ProtocolManager.addLog("📬 Unread incremented for: ${opponentKey.take(16)}...")
} }
ProtocolManager.addLog("✅ Dialog updated: ${lastMessage.take(20)}...")
} else { } else {
// Создаём новый диалог // Создаём новый диалог
dialogDao.insertDialog(DialogEntity( dialogDao.insertDialog(DialogEntity(
@@ -1148,10 +1077,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
lastMessageTimestamp = timestamp, lastMessageTimestamp = timestamp,
unreadCount = if (incrementUnread) 1 else 0 unreadCount = if (incrementUnread) 1 else 0
)) ))
ProtocolManager.addLog("✅ Dialog created (new)")
} }
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ updateDialog error: ${e.message}")
Log.e(TAG, "updateDialog error", e) Log.e(TAG, "updateDialog error", e)
} }
} }
@@ -1183,7 +1110,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔒 Проверяем messageId - если пустой, генерируем новый // 🔒 Проверяем messageId - если пустой, генерируем новый
val finalMessageId = if (messageId.isEmpty()) { val finalMessageId = if (messageId.isEmpty()) {
val generated = UUID.randomUUID().toString().replace("-", "").take(32) val generated = UUID.randomUUID().toString().replace("-", "").take(32)
ProtocolManager.addLog("⚠️ Empty messageId detected, generated new: $generated")
generated generated
} else { } else {
messageId messageId
@@ -1194,11 +1120,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Проверяем существует ли сообщение // Проверяем существует ли сообщение
val exists = messageDao.messageExists(account, finalMessageId) val exists = messageDao.messageExists(account, finalMessageId)
ProtocolManager.addLog("💾 Saving message to DB:")
ProtocolManager.addLog(" messageId: $finalMessageId")
ProtocolManager.addLog(" exists in DB: $exists")
ProtocolManager.addLog(" dialogKey: $dialogKey")
ProtocolManager.addLog(" text: ${text.take(20)}...")
val entity = MessageEntity( val entity = MessageEntity(
account = account, account = account,
@@ -1218,17 +1139,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
) )
val insertedId = messageDao.insertMessage(entity) val insertedId = messageDao.insertMessage(entity)
ProtocolManager.addLog("✅ Message saved with DB id: $insertedId")
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Message save error: ${e.message}")
Log.e(TAG, "Message save error", e) Log.e(TAG, "Message save error", e)
} }
} }
private fun showTypingIndicator() { private fun showTypingIndicator() {
_opponentTyping.value = true _opponentTyping.value = true
viewModelScope.launch { viewModelScope.launch(Dispatchers.Default) {
kotlinx.coroutines.delay(3000) kotlinx.coroutines.delay(3000)
_opponentTyping.value = false _opponentTyping.value = false
} }
@@ -1243,15 +1162,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
if (now - lastTypingSentTime < TYPING_THROTTLE_MS) return if (now - lastTypingSentTime < TYPING_THROTTLE_MS) return
val opponent = opponentKey ?: run { val opponent = opponentKey ?: run {
ProtocolManager.addLog("❌ Typing: No opponent key")
return return
} }
val sender = myPublicKey ?: run { val sender = myPublicKey ?: run {
ProtocolManager.addLog("❌ Typing: No sender key")
return return
} }
val privateKey = myPrivateKey ?: run { val privateKey = myPrivateKey ?: run {
ProtocolManager.addLog("❌ Typing: No private key")
return return
} }
@@ -1261,10 +1177,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
try { try {
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey) val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
ProtocolManager.addLog("⌨️ Sending typing...")
ProtocolManager.addLog(" From: ${sender.take(16)}...")
ProtocolManager.addLog(" To: ${opponent.take(16)}...")
ProtocolManager.addLog(" PrivateHash: ${privateKeyHash.take(16)}...")
val packet = PacketTyping().apply { val packet = PacketTyping().apply {
this.privateKey = privateKeyHash this.privateKey = privateKeyHash
@@ -1273,10 +1185,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
ProtocolManager.send(packet) ProtocolManager.send(packet)
ProtocolManager.addLog("⌨️ Typing indicator sent ✅")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Typing send error", e) Log.e(TAG, "Typing send error", e)
ProtocolManager.addLog("❌ Typing send error: ${e.message}")
} }
} }
} }
@@ -1289,7 +1199,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
private fun sendReadReceiptToOpponent() { private fun sendReadReceiptToOpponent() {
// 🔥 Не отправляем read receipt если диалог не активен (как в архиве) // 🔥 Не отправляем read receipt если диалог не активен (как в архиве)
if (!isDialogActive) { if (!isDialogActive) {
ProtocolManager.addLog("👁️ Read receipt skipped - dialog not active")
return return
} }
@@ -1315,7 +1224,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
ProtocolManager.send(packet) ProtocolManager.send(packet)
ProtocolManager.addLog("👁️ Read receipt sent to: ${opponent.take(8)}...")
readReceiptSentForCurrentDialog = true readReceiptSentForCurrentDialog = true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Read receipt send error", e) Log.e(TAG, "Read receipt send error", e)
@@ -1330,7 +1238,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
fun markVisibleMessagesAsRead() { fun markVisibleMessagesAsRead() {
// 🔥 Не читаем если диалог не активен // 🔥 Не читаем если диалог не активен
if (!isDialogActive) { if (!isDialogActive) {
ProtocolManager.addLog("👁️ markVisibleMessagesAsRead skipped - dialog not active")
return return
} }
@@ -1344,7 +1251,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Если timestamp не изменился - не отправляем повторно // Если timestamp не изменился - не отправляем повторно
if (lastIncoming.timestamp.time <= lastReadMessageTimestamp) return if (lastIncoming.timestamp.time <= lastReadMessageTimestamp) return
ProtocolManager.addLog("👁️ markVisibleMessagesAsRead: new message detected")
// Отмечаем в БД и очищаем счетчик непрочитанных // Отмечаем в БД и очищаем счетчик непрочитанных
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -1352,7 +1258,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val dialogKey = getDialogKey(account, opponent) val dialogKey = getDialogKey(account, opponent)
messageDao.markDialogAsRead(account, dialogKey) messageDao.markDialogAsRead(account, dialogKey)
dialogDao.clearUnreadCount(account, opponent) dialogDao.clearUnreadCount(account, opponent)
ProtocolManager.addLog("👁️ Marked dialog as read in DB, cleared unread count")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Mark as read error", e) Log.e(TAG, "Mark as read error", e)
} }
@@ -1379,10 +1284,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
ProtocolManager.send(packet) ProtocolManager.send(packet)
ProtocolManager.addLog("🟢 Subscribed to online status: ${opponent.take(16)}...")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Online subscribe error", e) Log.e(TAG, "Online subscribe error", e)
ProtocolManager.addLog("❌ Online subscribe error: ${e.message}")
} }
} }
} }

View File

@@ -178,7 +178,6 @@ fun ChatsListScreen(
// Protocol connection state // Protocol connection state
val protocolState by ProtocolManager.state.collectAsState() val protocolState by ProtocolManager.state.collectAsState()
val debugLogs by ProtocolManager.debugLogs.collectAsState()
// Dialogs from database // Dialogs from database
val dialogsList by chatsViewModel.dialogs.collectAsState() val dialogsList by chatsViewModel.dialogs.collectAsState()

View File

@@ -60,9 +60,10 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
viewModelScope.launch { viewModelScope.launch {
dialogDao.getDialogsFlow(publicKey) dialogDao.getDialogsFlow(publicKey)
.collect { dialogsList -> .flowOn(Dispatchers.IO) // 🚀 Flow работает на IO
// 🔓 Расшифровываем lastMessage для каждого диалога .map { dialogsList ->
val decryptedDialogs = dialogsList.map { dialog -> // 🔓 Расшифровываем lastMessage на IO потоке (PBKDF2 - тяжелая операция!)
dialogsList.map { dialog ->
val decryptedLastMessage = try { val decryptedLastMessage = try {
if (privateKey.isNotEmpty() && dialog.lastMessage.isNotEmpty()) { if (privateKey.isNotEmpty() && dialog.lastMessage.isNotEmpty()) {
CryptoManager.decryptWithPassword(dialog.lastMessage, privateKey) CryptoManager.decryptWithPassword(dialog.lastMessage, privateKey)
@@ -88,12 +89,13 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
verified = dialog.verified verified = dialog.verified
) )
} }
}
.flowOn(Dispatchers.Default) // 🚀 map выполняется на Default (CPU)
.collect { decryptedDialogs ->
_dialogs.value = decryptedDialogs _dialogs.value = decryptedDialogs
ProtocolManager.addLog("📋 Dialogs loaded: ${decryptedDialogs.size} (lastMessages decrypted)")
// 🟢 Подписываемся на онлайн-статусы всех собеседников // 🟢 Подписываемся на онлайн-статусы всех собеседников
subscribeToOnlineStatuses(dialogsList.map { it.opponentKey }, privateKey) subscribeToOnlineStatuses(decryptedDialogs.map { it.opponentKey }, privateKey)
} }
} }
} }
@@ -116,9 +118,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
} }
ProtocolManager.send(packet) ProtocolManager.send(packet)
ProtocolManager.addLog("🟢 Subscribed to ${opponentKeys.size} online statuses")
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Online subscribe error: ${e.message}")
} }
} }
} }

View File

@@ -40,12 +40,7 @@ class SearchUsersViewModel : ViewModel() {
// Callback для обработки ответа поиска // Callback для обработки ответа поиска
private val searchPacketHandler: (com.rosetta.messenger.network.Packet) -> Unit = { packet -> private val searchPacketHandler: (com.rosetta.messenger.network.Packet) -> Unit = { packet ->
if (packet is PacketSearch) { if (packet is PacketSearch) {
ProtocolManager.addLog("📥 Search response received")
ProtocolManager.addLog(" Users found: ${packet.users.size}")
packet.users.forEachIndexed { index, user -> packet.users.forEachIndexed { index, user ->
ProtocolManager.addLog(" [$index] ${user.title.ifEmpty { "No title" }} (@${user.username.ifEmpty { "no username" }})")
ProtocolManager.addLog(" Key: ${user.publicKey.take(20)}...")
ProtocolManager.addLog(" Verified: ${user.verified}, Online: ${user.online}")
} }
_searchResults.value = packet.users _searchResults.value = packet.users
_isSearching.value = false _isSearching.value = false
@@ -103,7 +98,6 @@ class SearchUsersViewModel : ViewModel() {
// Проверяем состояние протокола // Проверяем состояние протокола
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) { if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
ProtocolManager.addLog("⚠️ Search failed: Not authenticated")
_isSearching.value = false _isSearching.value = false
return@launch return@launch
} }
@@ -115,8 +109,6 @@ class SearchUsersViewModel : ViewModel() {
lastSearchedText = query lastSearchedText = query
ProtocolManager.addLog("🔍 Searching for: \"$query\"")
ProtocolManager.addLog(" PrivateKeyHash: ${privateKeyHash.take(20)}...")
// Создаем и отправляем пакет поиска // Создаем и отправляем пакет поиска
val packetSearch = PacketSearch().apply { val packetSearch = PacketSearch().apply {
@@ -132,7 +124,6 @@ class SearchUsersViewModel : ViewModel() {
* Открыть панель поиска * Открыть панель поиска
*/ */
fun expandSearch() { fun expandSearch() {
ProtocolManager.addLog("🔎 Search panel opened")
_isSearchExpanded.value = true _isSearchExpanded.value = true
} }
@@ -140,7 +131,6 @@ class SearchUsersViewModel : ViewModel() {
* Закрыть панель поиска и очистить результаты * Закрыть панель поиска и очистить результаты
*/ */
fun collapseSearch() { fun collapseSearch() {
ProtocolManager.addLog("🔎 Search panel closed")
_isSearchExpanded.value = false _isSearchExpanded.value = false
_searchQuery.value = "" _searchQuery.value = ""
_searchResults.value = emptyList() _searchResults.value = emptyList()