diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index f433bbf..7a02231 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -215,7 +215,7 @@ private fun Message.toChatMessage() = ) /** Экран детального чата с пользователем */ -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun ChatDetailScreen( user: SearchUser, @@ -1179,7 +1179,16 @@ fun ChatDetailScreen( nextMessage.isOutgoing != message.isOutgoing || (message.timestamp.time - nextMessage.timestamp.time) > 60_000 // 1 минута - Column { + // 🚀 ОПТИМИЗАЦИЯ: animateItemPlacement() для плавной анимации при добавлении/удалении + // Это предотвращает "прыжки" пузырьков при изменении списка + Column( + modifier = Modifier.animateItemPlacement( + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + ) + ) { // В reversed layout: дата показывается ПОСЛЕ сообщения // (визуально СВЕРХУ группы сообщений) if (showDate) { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 4918550..8d798ae 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -129,6 +129,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { /** * 🔔 Подписка на события новых сообщений от MessageRepository * Обновляет UI в реальном времени когда приходит новое сообщение + * 🚀 ОПТИМИЗАЦИЯ: Добавляем сообщение инкрементально, а не перезагружаем весь список + * Это предотвращает "прыгание" пузырьков при добавлении нового сообщения */ private fun setupNewMessageListener() { viewModelScope.launch { @@ -139,13 +141,60 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val currentDialogKey = getDialogKey(account, opponent) if (dialogKey == currentDialogKey) { - // Обновляем сообщения из БД без показа скелетона - loadMessagesFromDatabase(delayMs = 0L) + // 🚀 ОПТИМИЗАЦИЯ: Загружаем только последнее сообщение и добавляем инкрементально + addLatestMessageFromDb(account, currentDialogKey) } } } } + /** + * 🚀 Инкрементальное добавление последнего сообщения из БД + * Вместо полной перезагрузки списка - добавляем только новое сообщение + * Это предотвращает "прыгание" пузырьков в Compose + */ + private fun addLatestMessageFromDb(account: String, dialogKey: String) { + viewModelScope.launch(Dispatchers.IO) { + try { + // Получаем последнее сообщение из БД + val latestEntity = messageDao.getMessages(account, dialogKey, limit = 1, offset = 0).firstOrNull() + ?: return@launch + + // Проверяем, есть ли это сообщение уже в списке + val existingIds = _messages.value.map { it.id }.toSet() + if (latestEntity.messageId in existingIds) { + return@launch // Сообщение уже есть, не добавляем + } + + // Конвертируем в ChatMessage + val newMessage = entityToChatMessage(latestEntity) + + // 🚀 КЛЮЧЕВОЕ: Добавляем в конец списка (новые сообщения = больший timestamp) + // НЕ пересортировываем весь список - просто добавляем! + withContext(Dispatchers.Main.immediate) { + val currentList = _messages.value + _messages.value = currentList + newMessage + } + + // Обновляем кэш + dialogMessagesCache[dialogKey] = _messages.value + + // 👁️ Фоновые операции + if (isDialogActive) { + messageDao.markDialogAsRead(account, dialogKey) + // Отправляем read receipt + if (!newMessage.isOutgoing) { + sendReadReceiptToOpponent() + } + } + dialogDao.updateDialogFromMessages(account, opponentKey ?: return@launch) + + } catch (e: Exception) { + android.util.Log.e(TAG, "Error adding latest message", e) + } + } + } + private fun setupPacketListeners() { // ✅ Входящие сообщения обрабатываются ТОЛЬКО в MessageRepository (ProtocolManager) // Здесь НЕ обрабатываем 0x06 чтобы избежать дублирования!