diff --git a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt index fcc988f..a9d1210 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -62,6 +62,10 @@ class MessageRepository private constructor(private val context: Context) { private val _dialogs = MutableStateFlow>(emptyList()) val dialogs: StateFlow> = _dialogs.asStateFlow() + // 🔔 События новых сообщений для обновления UI в реальном времени + private val _newMessageEvents = MutableSharedFlow(replay = 0, extraBufferCapacity = 10) + val newMessageEvents: SharedFlow = _newMessageEvents.asSharedFlow() + // Текущий аккаунт private var currentAccount: String? = null private var currentPrivateKey: String? = null @@ -321,6 +325,9 @@ class MessageRepository private constructor(private val context: Context) { if (!stillExists) { val message = entity.toMessage() updateMessageCache(dialogKey, message) + + // 🔔 Уведомляем UI о новом сообщении (emit dialogKey для обновления) + _newMessageEvents.tryEmit(dialogKey) } } catch (e: Exception) { 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 565ff7c..2885d07 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 @@ -40,6 +40,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { private val dialogDao = database.dialogDao() private val messageDao = database.messageDao() + // MessageRepository для подписки на события новых сообщений + private val messageRepository = com.rosetta.messenger.data.MessageRepository.getInstance(application) + // 🔥 Кэш расшифрованных сообщений (messageId -> plainText) private val decryptionCache = ConcurrentHashMap() @@ -120,19 +123,33 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { init { setupPacketListeners() + setupNewMessageListener() + } + + /** + * 🔔 Подписка на события новых сообщений от MessageRepository + * Обновляет UI в реальном времени когда приходит новое сообщение + */ + private fun setupNewMessageListener() { + viewModelScope.launch { + messageRepository.newMessageEvents.collect { dialogKey -> + // Проверяем что это сообщение для текущего диалога + val account = myPublicKey ?: return@collect + val opponent = opponentKey ?: return@collect + val currentDialogKey = getDialogKey(account, opponent) + + if (dialogKey == currentDialogKey) { + // Обновляем сообщения из БД без показа скелетона + loadMessagesFromDatabase(delayMs = 0L) + } + } + } } private fun setupPacketListeners() { - // Входящие сообщения - ProtocolManager.waitPacket(0x06) { packet -> - val msgPacket = packet as PacketMessage - if (msgPacket.fromPublicKey == opponentKey || msgPacket.toPublicKey == opponentKey) { - viewModelScope.launch(Dispatchers.IO) { - handleIncomingMessage(msgPacket) - } - } else { - } - } + // ✅ Входящие сообщения обрабатываются ТОЛЬКО в MessageRepository (ProtocolManager) + // Здесь НЕ обрабатываем 0x06 чтобы избежать дублирования! + // ChatViewModel получает обновления через loadMessagesFromDatabase() после сохранения в БД // Доставка ProtocolManager.waitPacket(0x08) { packet -> @@ -193,115 +210,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } } - private fun handleIncomingMessage(packet: PacketMessage) { - // 🚀 Обработка входящего сообщения в IO потоке - viewModelScope.launch(Dispatchers.IO) { - try { - val privateKey = myPrivateKey ?: return@launch - val account = myPublicKey ?: return@launch - - // 🔥 Проверяем блокировку (как в Архиве) - val isBlocked = database.blacklistDao().isUserBlocked(packet.fromPublicKey, account) - if (isBlocked) { - return@launch - } - - // Расшифровываем в фоне - получаем и текст и plainKeyAndNonce - val decryptResult = MessageCrypto.decryptIncomingFull( - packet.content, - packet.chachaKey, - privateKey - ) - val decryptedText = decryptResult.plaintext - val plainKeyAndNonce = decryptResult.plainKeyAndNonce - - // Кэшируем расшифрованный текст - decryptionCache[packet.messageId] = decryptedText - - - // 🔥 Парсим reply из attachments (как в React Native) - var replyData: ReplyData? = null - val attachmentsJson = if (packet.attachments.isNotEmpty()) { - val jsonArray = JSONArray() - for (att in packet.attachments) { - - // Если это MESSAGES (reply) - парсим и расшифровываем данные - var blobToStore = att.blob // По умолчанию сохраняем оригинальный blob - if (att.type == AttachmentType.MESSAGES && att.blob.isNotEmpty()) { - try { - - // 🔥 Расшифровываем с полным plainKeyAndNonce (56 bytes) - // Desktop использует chachaDecryptedKey.toString('utf-8') = полные 56 байт! - val decryptedBlob = MessageCrypto.decryptReplyBlob(att.blob, plainKeyAndNonce) - - // 🔥 Сохраняем расшифрованный blob в БД - blobToStore = decryptedBlob - - // Парсим JSON массив с цитируемыми сообщениями - val replyArray = JSONArray(decryptedBlob) - if (replyArray.length() > 0) { - val firstReply = replyArray.getJSONObject(0) - val replyPublicKey = firstReply.optString("publicKey", "") - val replyText = firstReply.optString("message", "") - val replyMessageId = firstReply.optString("message_id", "") - - // Определяем автора цитаты - val isReplyFromMe = replyPublicKey == myPublicKey - - replyData = ReplyData( - messageId = replyMessageId, - senderName = if (isReplyFromMe) "You" else opponentTitle.ifEmpty { "User" }, - text = replyText, - isFromMe = isReplyFromMe - ) - } - } catch (e: Exception) { - } - } - - jsonArray.put(JSONObject().apply { - put("id", att.id) - put("type", att.type.value) - put("preview", att.preview) - put("blob", blobToStore) // 🔥 Сохраняем расшифрованный blob для MESSAGES - }) - } - jsonArray.toString() - } else "[]" - - // Обновляем UI в Main потоке (как в архиве - просто добавляем без лишних проверок) - withContext(Dispatchers.Main) { - val message = ChatMessage( - id = packet.messageId, - text = decryptedText, - isOutgoing = packet.fromPublicKey == myPublicKey, - timestamp = Date(packet.timestamp), - status = MessageStatus.DELIVERED, - replyData = replyData - ) - // Просто добавляем как в архиве: setMessages((prev) => ([...prev, newMessage])) - _messages.value = _messages.value + message - - // 🔥 Обновляем кэш чтобы при перезаходе сообщение уже было - val dialogKey = getDialogKey(account, packet.fromPublicKey) - updateDialogCache(dialogKey, message) - } - - // ✅ НЕ сохраняем в БД здесь - это делает MessageRepository.handleIncomingMessage()! - // Убираем дублирование: одно сообщение не должно сохраняться дважды - - // 🔥 Обновляем диалог - используем fromPublicKey - val senderKey = packet.fromPublicKey - updateDialog(senderKey, decryptedText, packet.timestamp, incrementUnread = !isDialogActive) - - // 👁️ НЕ отправляем read receipt автоматически! - // Read receipt отправляется только когда пользователь видит сообщение - // (через markVisibleMessagesAsRead вызываемый из ChatDetailScreen) - - } catch (e: Exception) { - } - } - } + // ✅ handleIncomingMessage удалён - обработка входящих сообщений теперь ТОЛЬКО в MessageRepository + // Это предотвращает дублирование сообщений private fun updateMessageStatus(messageId: String, status: MessageStatus) { _messages.value = _messages.value.map { msg -> @@ -563,20 +473,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } } - /** - * 🔥 Обновить кэш после отправки/получения сообщения - */ - private fun updateDialogCache(dialogKey: String, newMessage: ChatMessage) { - val current = dialogMessagesCache[dialogKey]?.toMutableList() ?: mutableListOf() - // 🔥 Проверяем на дубликат по ID - if (current.none { it.id == newMessage.id }) { - current.add(newMessage) - dialogMessagesCache[dialogKey] = current - } - } + // ✅ updateDialogCache удалён - кэш обновляется через loadMessagesFromDatabase после сохранения в БД /** - * �🚀 Загрузка следующей страницы (для бесконечной прокрутки) + * 🚀 Загрузка следующей страницы (для бесконечной прокрутки) */ fun loadMoreMessages() { val account = myPublicKey ?: return