feat: Implement real-time UI updates for new messages in MessageRepository; refactor ChatViewModel to avoid message duplication

This commit is contained in:
k1ngsterr1
2026-01-16 16:43:15 +05:00
parent b1046f88e5
commit 6386164ae7
2 changed files with 38 additions and 131 deletions

View File

@@ -62,6 +62,10 @@ class MessageRepository private constructor(private val context: Context) {
private val _dialogs = MutableStateFlow<List<Dialog>>(emptyList())
val dialogs: StateFlow<List<Dialog>> = _dialogs.asStateFlow()
// 🔔 События новых сообщений для обновления UI в реальном времени
private val _newMessageEvents = MutableSharedFlow<String>(replay = 0, extraBufferCapacity = 10)
val newMessageEvents: SharedFlow<String> = _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) {

View File

@@ -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<String, String>()
@@ -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 после сохранения в БД
/**
* <EFBFBD>🚀 Загрузка следующей страницы (для бесконечной прокрутки)
* 🚀 Загрузка следующей страницы (для бесконечной прокрутки)
*/
fun loadMoreMessages() {
val account = myPublicKey ?: return