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 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())
val debugLogs: StateFlow<List<String>> = _debugLogs.asStateFlow()
@@ -34,11 +35,23 @@ object ProtocolManager {
private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
// 🚀 Флаг для включения UI логов (по умолчанию ВЫКЛЮЧЕНО - это вызывало ANR!)
private var uiLogsEnabled = false
fun addLog(message: String) {
val timestamp = dateFormat.format(Date())
val logLine = "[$timestamp] $message"
// Только Logcat - быстро и не блокирует UI
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() {

View File

@@ -296,7 +296,12 @@ fun ChatDetailScreen(
// Состояние показа логов
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) }

View File

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

View File

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

View File

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