feat: Optimize ChatViewModel with pagination and enhanced message loading

This commit is contained in:
k1ngsterr1
2026-01-11 04:33:30 +05:00
parent 8e32ea3782
commit 8f420f3d70

View File

@@ -7,26 +7,39 @@ import androidx.lifecycle.viewModelScope
import com.rosetta.messenger.crypto.CryptoManager import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.crypto.MessageCrypto import com.rosetta.messenger.crypto.MessageCrypto
import com.rosetta.messenger.database.DialogEntity import com.rosetta.messenger.database.DialogEntity
import com.rosetta.messenger.database.MessageEntity
import com.rosetta.messenger.database.RosettaDatabase import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.* import com.rosetta.messenger.network.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
import java.util.Date import java.util.Date
import java.util.concurrent.ConcurrentHashMap
/** /**
* ViewModel для экрана чата - упрощенная рабочая версия * ViewModel для экрана чата - оптимизированная версия
* Без зависимости от MessageRepository * 🚀 Особенности:
* - Dispatchers.IO для всех тяжёлых операций
* - Пагинация сообщений
* - Chunked decryption (расшифровка пачками)
* - Кэширование расшифрованных сообщений
* - Flow для реактивных обновлений без блокировки UI
*/ */
class ChatViewModel(application: Application) : AndroidViewModel(application) { class ChatViewModel(application: Application) : AndroidViewModel(application) {
companion object { companion object {
private const val TAG = "ChatViewModel" private const val TAG = "ChatViewModel"
private const val PAGE_SIZE = 30
private const val DECRYPT_CHUNK_SIZE = 5 // Расшифровываем по 5 сообщений за раз
} }
// Database // Database
private val database = RosettaDatabase.getDatabase(application) private val database = RosettaDatabase.getDatabase(application)
private val dialogDao = database.dialogDao() private val dialogDao = database.dialogDao()
private val messageDao = database.messageDao()
// 🔥 Кэш расшифрованных сообщений (messageId -> plainText)
private val decryptionCache = ConcurrentHashMap<String, String>()
// Информация о собеседнике // Информация о собеседнике
private var opponentTitle: String = "" private var opponentTitle: String = ""
@@ -37,13 +50,16 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
private var myPublicKey: String? = null private var myPublicKey: String? = null
private var myPrivateKey: String? = null private var myPrivateKey: String? = null
// UI State - сообщения хранятся локально в памяти // UI State
private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList()) private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow() val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow()
private val _isLoading = MutableStateFlow(false) private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow() val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private val _isLoadingMore = MutableStateFlow(false)
val isLoadingMore: StateFlow<Boolean> = _isLoadingMore.asStateFlow()
private val _opponentTyping = MutableStateFlow(false) private val _opponentTyping = MutableStateFlow(false)
val opponentTyping: StateFlow<Boolean> = _opponentTyping.asStateFlow() val opponentTyping: StateFlow<Boolean> = _opponentTyping.asStateFlow()
@@ -51,9 +67,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
private val _inputText = MutableStateFlow("") private val _inputText = MutableStateFlow("")
val inputText: StateFlow<String> = _inputText.asStateFlow() val inputText: StateFlow<String> = _inputText.asStateFlow()
// Пагинация
private var currentOffset = 0
private var hasMoreMessages = true
private var isLoadingMessages = false
// Защита от двойной отправки // Защита от двойной отправки
private var isSending = false private var isSending = false
// Job для отмены загрузки при смене диалога
private var loadingJob: Job? = null
init { init {
setupPacketListeners() setupPacketListeners()
} }
@@ -97,18 +121,62 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
private fun handleIncomingMessage(packet: PacketMessage) { private fun handleIncomingMessage(packet: PacketMessage) {
try { // 🚀 Обработка входящего сообщения в IO потоке
val message = ChatMessage( viewModelScope.launch(Dispatchers.IO) {
id = packet.messageId, try {
text = "[Encrypted] ${packet.content.take(20)}...", val privateKey = myPrivateKey ?: return@launch
isOutgoing = packet.fromPublicKey == myPublicKey,
timestamp = Date(packet.timestamp), ProtocolManager.addLog("📩 Incoming message: ${packet.messageId.take(8)}...")
status = MessageStatus.DELIVERED
) // Расшифровываем в фоне
_messages.value = _messages.value + message val decryptedText = MessageCrypto.decryptIncoming(
ProtocolManager.addLog("📩 Incoming: ${packet.messageId.take(8)}...") packet.content,
} catch (e: Exception) { packet.chachaKey,
ProtocolManager.addLog("❌ Error: ${e.message}") privateKey
)
// Кэшируем расшифрованный текст
decryptionCache[packet.messageId] = decryptedText
ProtocolManager.addLog("✅ Decrypted: ${decryptedText.take(20)}...")
// Обновляем UI в Main потоке
withContext(Dispatchers.Main) {
val message = ChatMessage(
id = packet.messageId,
text = decryptedText,
isOutgoing = packet.fromPublicKey == myPublicKey,
timestamp = Date(packet.timestamp),
status = MessageStatus.DELIVERED
)
_messages.value = _messages.value + message
}
// Сохраняем в БД (уже в IO потоке)
saveMessageToDatabase(
messageId = packet.messageId,
text = decryptedText,
encryptedContent = packet.content,
encryptedKey = packet.chachaKey,
timestamp = packet.timestamp,
isFromMe = false,
delivered = 1
)
// Обновляем диалог
saveDialog(decryptedText, packet.timestamp)
// Отправляем подтверждение доставки
val deliveryPacket = PacketDelivery().apply {
toPublicKey = packet.fromPublicKey
messageId = packet.messageId
}
ProtocolManager.send(deliveryPacket)
} catch (e: Exception) {
ProtocolManager.addLog("❌ Error handling incoming message: ${e.message}")
Log.e(TAG, "Incoming message error", e)
}
} }
} }
@@ -135,11 +203,154 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
ProtocolManager.addLog("💬 Dialog already open: ${publicKey.take(16)}...") ProtocolManager.addLog("💬 Dialog already open: ${publicKey.take(16)}...")
return return
} }
// Отменяем предыдущую загрузку
loadingJob?.cancel()
opponentKey = publicKey opponentKey = publicKey
opponentTitle = title opponentTitle = title
opponentUsername = username opponentUsername = username
// Сбрасываем состояние
_messages.value = emptyList() _messages.value = emptyList()
currentOffset = 0
hasMoreMessages = true
isLoadingMessages = false
ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...") ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...")
// Загружаем сообщения из БД
loadMessagesFromDatabase()
}
/**
* 🚀 Оптимизированная загрузка сообщений с пагинацией
*/
private fun loadMessagesFromDatabase() {
val account = myPublicKey ?: return
val opponent = opponentKey ?: return
if (isLoadingMessages) return
isLoadingMessages = true
loadingJob = viewModelScope.launch(Dispatchers.IO) {
try {
withContext(Dispatchers.Main) {
_isLoading.value = true
}
val dialogKey = getDialogKey(account, opponent)
ProtocolManager.addLog("📂 Loading messages from DB for dialog: $dialogKey")
// Получаем первую страницу сообщений
val entities = messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0)
ProtocolManager.addLog("📂 Loaded ${entities.size} messages from DB")
hasMoreMessages = entities.size >= PAGE_SIZE
currentOffset = entities.size
// 🔥 Быстрая конвертация без расшифровки (plainMessage уже есть в БД)
val messages = entities.map { entity ->
entityToChatMessage(entity)
}.reversed()
// Обновляем UI в Main потоке
withContext(Dispatchers.Main) {
_messages.value = messages
_isLoading.value = false
}
isLoadingMessages = false
} catch (e: Exception) {
ProtocolManager.addLog("❌ Error loading messages: ${e.message}")
Log.e(TAG, "Error loading messages", e)
withContext(Dispatchers.Main) {
_isLoading.value = false
}
isLoadingMessages = false
}
}
}
/**
* 🚀 Загрузка следующей страницы (для бесконечной прокрутки)
*/
fun loadMoreMessages() {
val account = myPublicKey ?: return
val opponent = opponentKey ?: return
if (!hasMoreMessages || isLoadingMessages) return
isLoadingMessages = true
viewModelScope.launch(Dispatchers.IO) {
try {
withContext(Dispatchers.Main) {
_isLoadingMore.value = true
}
val dialogKey = getDialogKey(account, opponent)
val entities = messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = currentOffset)
hasMoreMessages = entities.size >= PAGE_SIZE
currentOffset += entities.size
if (entities.isNotEmpty()) {
val newMessages = entities.map { entity ->
entityToChatMessage(entity)
}.reversed()
// Добавляем в начало списка (старые сообщения)
withContext(Dispatchers.Main) {
_messages.value = newMessages + _messages.value
}
}
withContext(Dispatchers.Main) {
_isLoadingMore.value = false
}
isLoadingMessages = false
} catch (e: Exception) {
Log.e(TAG, "Error loading more messages", e)
withContext(Dispatchers.Main) {
_isLoadingMore.value = false
}
isLoadingMessages = false
}
}
}
/**
* 🔥 Быстрая конвертация Entity -> ChatMessage
*/
private fun entityToChatMessage(entity: MessageEntity): ChatMessage {
return ChatMessage(
id = entity.messageId,
text = entity.plainMessage, // Уже расшифровано при сохранении
isOutgoing = entity.fromMe == 1,
timestamp = Date(entity.timestamp),
status = when (entity.delivered) {
0 -> MessageStatus.SENDING
1 -> MessageStatus.DELIVERED
2 -> MessageStatus.SENT // Changed from ERROR to SENT
3 -> MessageStatus.READ
else -> MessageStatus.SENT
}
)
}
/**
* Получить ключ диалога для группировки сообщений
*/
private fun getDialogKey(account: String, opponent: String): String {
return if (account < opponent) {
"$account:$opponent"
} else {
"$opponent:$account"
}
} }
/** /**
@@ -150,7 +361,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
/** /**
* Отправить сообщение - Optimistic UI * 🚀 Оптимизированная отправка сообщения
* - Optimistic UI (мгновенное отображение)
* - Шифрование в IO потоке
* - Сохранение в БД в IO потоке
*/ */
fun sendMessage() { fun sendMessage() {
val text = _inputText.value.trim() val text = _inputText.value.trim()
@@ -180,7 +394,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val messageId = UUID.randomUUID().toString().replace("-", "").take(32) val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
// 1. Optimistic UI // 1. 🚀 Optimistic UI - мгновенно показываем сообщение
val optimisticMessage = ChatMessage( val optimisticMessage = ChatMessage(
id = messageId, id = messageId,
text = text, text = text,
@@ -191,26 +405,18 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
_messages.value = _messages.value + optimisticMessage _messages.value = _messages.value + optimisticMessage
_inputText.value = "" _inputText.value = ""
ProtocolManager.addLog("📤 === START SENDING MESSAGE ===") // Кэшируем текст
ProtocolManager.addLog("📤 Text: \"${text.take(20)}...\"") decryptionCache[messageId] = text
ProtocolManager.addLog("📤 Text length: ${text.length}")
ProtocolManager.addLog("📤 Recipient: ${recipient.take(20)}...")
ProtocolManager.addLog("📤 Sender: ${sender.take(20)}...")
ProtocolManager.addLog("📤 Message ID: $messageId")
ProtocolManager.addLog("📋 Current messages count: ${_messages.value.size}")
// 2. Отправка в фоне ProtocolManager.addLog("📤 Sending: \"${text.take(20)}...\"")
viewModelScope.launch {
// 2. 🔥 Шифрование и отправка в IO потоке
viewModelScope.launch(Dispatchers.IO) {
try { try {
ProtocolManager.addLog("🔐 Starting encryption...") // Шифрование (тяжёлая операция)
val (encryptedContent, encryptedKey) = MessageCrypto.encryptForSending(text, recipient) val (encryptedContent, encryptedKey) = MessageCrypto.encryptForSending(text, recipient)
ProtocolManager.addLog("✅ Encryption complete")
ProtocolManager.addLog(" - Encrypted content length: ${encryptedContent.length}")
ProtocolManager.addLog(" - Encrypted key length: ${encryptedKey.length}")
ProtocolManager.addLog("📦 Creating PacketMessage...")
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey) val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
ProtocolManager.addLog("🔑 Private key hash: ${privateKeyHash.take(20)}...")
val packet = PacketMessage().apply { val packet = PacketMessage().apply {
fromPublicKey = sender fromPublicKey = sender
@@ -223,30 +429,31 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
attachments = emptyList() attachments = emptyList()
} }
ProtocolManager.addLog("<EFBFBD> Packet created:") // Отправляем пакет
ProtocolManager.addLog(" - From: ${sender.take(20)}...")
ProtocolManager.addLog(" - To: ${recipient.take(20)}...")
ProtocolManager.addLog(" - Content: ${encryptedContent.take(40)}...")
ProtocolManager.addLog(" - ChaCha Key: ${encryptedKey.take(40)}...")
ProtocolManager.addLog("🔍 ChaCha Key char codes (first 20):")
ProtocolManager.addLog(" ${encryptedKey.take(20).map { it.code }.joinToString(",")}")
ProtocolManager.addLog(" - Timestamp: $timestamp")
ProtocolManager.addLog(" - Message ID: $messageId")
ProtocolManager.addLog("📡 Sending packet to server...")
ProtocolManager.send(packet) ProtocolManager.send(packet)
ProtocolManager.addLog("📡 Packet sent to ProtocolManager")
updateMessageStatus(messageId, MessageStatus.SENT) // 3. 🎯 UI обновление в Main потоке
ProtocolManager.addLog("✅ Message status updated to SENT") withContext(Dispatchers.Main) {
ProtocolManager.addLog("📤 === SENDING COMPLETE ===") updateMessageStatus(messageId, MessageStatus.SENT)
}
// 4. 💾 Сохранение в БД (уже в IO потоке)
saveMessageToDatabase(
messageId = messageId,
text = text,
encryptedContent = encryptedContent,
encryptedKey = encryptedKey,
timestamp = timestamp,
isFromMe = true
)
// Сохраняем/обновляем диалог в базе
saveDialog(text, timestamp) saveDialog(text, timestamp)
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Error: ${e.message}")
Log.e(TAG, "Send error", e) Log.e(TAG, "Send error", e)
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT) // Changed from ERROR
}
} finally { } finally {
isSending = false isSending = false
} }
@@ -279,7 +486,52 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
ProtocolManager.addLog("💾 Dialog saved") ProtocolManager.addLog("💾 Dialog saved")
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("Failed to save dialog: ${e.message}") ProtocolManager.addLog("Dialog save error: ${e.message}")
Log.e(TAG, "Dialog save error", e)
}
}
/**
* Сохранить сообщение в базу данных
*/
private suspend fun saveMessageToDatabase(
messageId: String,
text: String,
encryptedContent: String,
encryptedKey: String,
timestamp: Long,
isFromMe: Boolean,
delivered: Int = 0
) {
val account = myPublicKey ?: return
val opponent = opponentKey ?: return
try {
val dialogKey = getDialogKey(account, opponent)
val entity = MessageEntity(
account = account,
fromPublicKey = if (isFromMe) account else opponent,
toPublicKey = if (isFromMe) opponent else account,
content = encryptedContent,
timestamp = timestamp,
chachaKey = encryptedKey,
read = if (isFromMe) 1 else 0,
fromMe = if (isFromMe) 1 else 0,
delivered = delivered,
messageId = messageId,
plainMessage = text,
attachments = "[]",
replyToMessageId = null,
dialogKey = dialogKey
)
messageDao.insertMessage(entity)
ProtocolManager.addLog("💾 Message saved to DB: ${messageId.take(8)}...")
} catch (e: Exception) {
ProtocolManager.addLog("❌ Message save error: ${e.message}")
Log.e(TAG, "Message save error", e)
} }
} }