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.MessageCrypto
import com.rosetta.messenger.database.DialogEntity
import com.rosetta.messenger.database.MessageEntity
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.util.UUID
import java.util.Date
import java.util.concurrent.ConcurrentHashMap
/**
* ViewModel для экрана чата - упрощенная рабочая версия
* Без зависимости от MessageRepository
* ViewModel для экрана чата - оптимизированная версия
* 🚀 Особенности:
* - Dispatchers.IO для всех тяжёлых операций
* - Пагинация сообщений
* - Chunked decryption (расшифровка пачками)
* - Кэширование расшифрованных сообщений
* - Flow для реактивных обновлений без блокировки UI
*/
class ChatViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private const val TAG = "ChatViewModel"
private const val PAGE_SIZE = 30
private const val DECRYPT_CHUNK_SIZE = 5 // Расшифровываем по 5 сообщений за раз
}
// Database
private val database = RosettaDatabase.getDatabase(application)
private val dialogDao = database.dialogDao()
private val messageDao = database.messageDao()
// 🔥 Кэш расшифрованных сообщений (messageId -> plainText)
private val decryptionCache = ConcurrentHashMap<String, String>()
// Информация о собеседнике
private var opponentTitle: String = ""
@@ -37,13 +50,16 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
private var myPublicKey: String? = null
private var myPrivateKey: String? = null
// UI State - сообщения хранятся локально в памяти
// UI State
private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private val _isLoadingMore = MutableStateFlow(false)
val isLoadingMore: StateFlow<Boolean> = _isLoadingMore.asStateFlow()
private val _opponentTyping = MutableStateFlow(false)
val opponentTyping: StateFlow<Boolean> = _opponentTyping.asStateFlow()
@@ -51,9 +67,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
private val _inputText = MutableStateFlow("")
val inputText: StateFlow<String> = _inputText.asStateFlow()
// Пагинация
private var currentOffset = 0
private var hasMoreMessages = true
private var isLoadingMessages = false
// Защита от двойной отправки
private var isSending = false
// Job для отмены загрузки при смене диалога
private var loadingJob: Job? = null
init {
setupPacketListeners()
}
@@ -97,18 +121,62 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
private fun handleIncomingMessage(packet: PacketMessage) {
// 🚀 Обработка входящего сообщения в IO потоке
viewModelScope.launch(Dispatchers.IO) {
try {
val privateKey = myPrivateKey ?: return@launch
ProtocolManager.addLog("📩 Incoming message: ${packet.messageId.take(8)}...")
// Расшифровываем в фоне
val decryptedText = MessageCrypto.decryptIncoming(
packet.content,
packet.chachaKey,
privateKey
)
// Кэшируем расшифрованный текст
decryptionCache[packet.messageId] = decryptedText
ProtocolManager.addLog("✅ Decrypted: ${decryptedText.take(20)}...")
// Обновляем UI в Main потоке
withContext(Dispatchers.Main) {
val message = ChatMessage(
id = packet.messageId,
text = "[Encrypted] ${packet.content.take(20)}...",
text = decryptedText,
isOutgoing = packet.fromPublicKey == myPublicKey,
timestamp = Date(packet.timestamp),
status = MessageStatus.DELIVERED
)
_messages.value = _messages.value + message
ProtocolManager.addLog("📩 Incoming: ${packet.messageId.take(8)}...")
}
// Сохраняем в БД (уже в 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: ${e.message}")
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)}...")
return
}
// Отменяем предыдущую загрузку
loadingJob?.cancel()
opponentKey = publicKey
opponentTitle = title
opponentUsername = username
// Сбрасываем состояние
_messages.value = emptyList()
currentOffset = 0
hasMoreMessages = true
isLoadingMessages = false
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() {
val text = _inputText.value.trim()
@@ -180,7 +394,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
val timestamp = System.currentTimeMillis()
// 1. Optimistic UI
// 1. 🚀 Optimistic UI - мгновенно показываем сообщение
val optimisticMessage = ChatMessage(
id = messageId,
text = text,
@@ -191,26 +405,18 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
_messages.value = _messages.value + optimisticMessage
_inputText.value = ""
ProtocolManager.addLog("📤 === START SENDING MESSAGE ===")
ProtocolManager.addLog("📤 Text: \"${text.take(20)}...\"")
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}")
// Кэшируем текст
decryptionCache[messageId] = text
// 2. Отправка в фоне
viewModelScope.launch {
ProtocolManager.addLog("📤 Sending: \"${text.take(20)}...\"")
// 2. 🔥 Шифрование и отправка в IO потоке
viewModelScope.launch(Dispatchers.IO) {
try {
ProtocolManager.addLog("🔐 Starting encryption...")
// Шифрование (тяжёлая операция)
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)
ProtocolManager.addLog("🔑 Private key hash: ${privateKeyHash.take(20)}...")
val packet = PacketMessage().apply {
fromPublicKey = sender
@@ -223,30 +429,31 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
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.addLog("📡 Packet sent to ProtocolManager")
// 3. 🎯 UI обновление в Main потоке
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
ProtocolManager.addLog("✅ Message status updated to SENT")
ProtocolManager.addLog("📤 === SENDING COMPLETE ===")
}
// 4. 💾 Сохранение в БД (уже в IO потоке)
saveMessageToDatabase(
messageId = messageId,
text = text,
encryptedContent = encryptedContent,
encryptedKey = encryptedKey,
timestamp = timestamp,
isFromMe = true
)
// Сохраняем/обновляем диалог в базе
saveDialog(text, timestamp)
} catch (e: Exception) {
ProtocolManager.addLog("❌ Error: ${e.message}")
Log.e(TAG, "Send error", e)
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT) // Changed from ERROR
}
} finally {
isSending = false
}
@@ -279,7 +486,52 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
ProtocolManager.addLog("💾 Dialog saved")
} 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)
}
}