feat: Implement real-time UI updates for new messages in MessageRepository; refactor ChatViewModel to avoid message duplication
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user