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())
|
private val _dialogs = MutableStateFlow<List<Dialog>>(emptyList())
|
||||||
val dialogs: StateFlow<List<Dialog>> = _dialogs.asStateFlow()
|
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 currentAccount: String? = null
|
||||||
private var currentPrivateKey: String? = null
|
private var currentPrivateKey: String? = null
|
||||||
@@ -321,6 +325,9 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
if (!stillExists) {
|
if (!stillExists) {
|
||||||
val message = entity.toMessage()
|
val message = entity.toMessage()
|
||||||
updateMessageCache(dialogKey, message)
|
updateMessageCache(dialogKey, message)
|
||||||
|
|
||||||
|
// 🔔 Уведомляем UI о новом сообщении (emit dialogKey для обновления)
|
||||||
|
_newMessageEvents.tryEmit(dialogKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private val dialogDao = database.dialogDao()
|
private val dialogDao = database.dialogDao()
|
||||||
private val messageDao = database.messageDao()
|
private val messageDao = database.messageDao()
|
||||||
|
|
||||||
|
// MessageRepository для подписки на события новых сообщений
|
||||||
|
private val messageRepository = com.rosetta.messenger.data.MessageRepository.getInstance(application)
|
||||||
|
|
||||||
// 🔥 Кэш расшифрованных сообщений (messageId -> plainText)
|
// 🔥 Кэш расшифрованных сообщений (messageId -> plainText)
|
||||||
private val decryptionCache = ConcurrentHashMap<String, String>()
|
private val decryptionCache = ConcurrentHashMap<String, String>()
|
||||||
|
|
||||||
@@ -120,19 +123,33 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
setupPacketListeners()
|
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() {
|
private fun setupPacketListeners() {
|
||||||
// Входящие сообщения
|
// ✅ Входящие сообщения обрабатываются ТОЛЬКО в MessageRepository (ProtocolManager)
|
||||||
ProtocolManager.waitPacket(0x06) { packet ->
|
// Здесь НЕ обрабатываем 0x06 чтобы избежать дублирования!
|
||||||
val msgPacket = packet as PacketMessage
|
// ChatViewModel получает обновления через loadMessagesFromDatabase() после сохранения в БД
|
||||||
if (msgPacket.fromPublicKey == opponentKey || msgPacket.toPublicKey == opponentKey) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
handleIncomingMessage(msgPacket)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Доставка
|
// Доставка
|
||||||
ProtocolManager.waitPacket(0x08) { packet ->
|
ProtocolManager.waitPacket(0x08) { packet ->
|
||||||
@@ -193,115 +210,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIncomingMessage(packet: PacketMessage) {
|
// ✅ handleIncomingMessage удалён - обработка входящих сообщений теперь ТОЛЬКО в MessageRepository
|
||||||
// 🚀 Обработка входящего сообщения в 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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateMessageStatus(messageId: String, status: MessageStatus) {
|
private fun updateMessageStatus(messageId: String, status: MessageStatus) {
|
||||||
_messages.value = _messages.value.map { msg ->
|
_messages.value = _messages.value.map { msg ->
|
||||||
@@ -563,20 +473,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ✅ updateDialogCache удалён - кэш обновляется через loadMessagesFromDatabase после сохранения в БД
|
||||||
* 🔥 Обновить кэш после отправки/получения сообщения
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <EFBFBD>🚀 Загрузка следующей страницы (для бесконечной прокрутки)
|
* 🚀 Загрузка следующей страницы (для бесконечной прокрутки)
|
||||||
*/
|
*/
|
||||||
fun loadMoreMessages() {
|
fun loadMoreMessages() {
|
||||||
val account = myPublicKey ?: return
|
val account = myPublicKey ?: return
|
||||||
|
|||||||
Reference in New Issue
Block a user