feat: Optimize ChatViewModel with pagination and enhanced message loading
This commit is contained in:
@@ -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) {
|
||||
try {
|
||||
val message = ChatMessage(
|
||||
id = packet.messageId,
|
||||
text = "[Encrypted] ${packet.content.take(20)}...",
|
||||
isOutgoing = packet.fromPublicKey == myPublicKey,
|
||||
timestamp = Date(packet.timestamp),
|
||||
status = MessageStatus.DELIVERED
|
||||
)
|
||||
_messages.value = _messages.value + message
|
||||
ProtocolManager.addLog("📩 Incoming: ${packet.messageId.take(8)}...")
|
||||
} catch (e: Exception) {
|
||||
ProtocolManager.addLog("❌ Error: ${e.message}")
|
||||
// 🚀 Обработка входящего сообщения в 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 = 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)}...")
|
||||
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")
|
||||
|
||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||
ProtocolManager.addLog("✅ Message status updated to SENT")
|
||||
ProtocolManager.addLog("📤 === SENDING COMPLETE ===")
|
||||
// 3. 🎯 UI обновление в Main потоке
|
||||
withContext(Dispatchers.Main) {
|
||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user