feat: Update RosettaDatabase to include Message and Dialog entities, increment version to 2

feat: Implement message packets for sending and receiving messages, including delivery and read notifications

feat: Enhance ProtocolManager to handle message sending, delivery, and typing status with appropriate logging

feat: Refactor ChatDetailScreen to utilize ChatViewModel for managing chat state and message input

feat: Create ChatViewModel to manage chat messages, input state, and packet listeners for incoming messages

build: Add KSP plugin for annotation processing and configure Java 17 for the build environment
This commit is contained in:
k1ngsterr1
2026-01-10 22:15:27 +05:00
parent 286706188b
commit 6014d23d69
12 changed files with 1643 additions and 142 deletions

View File

@@ -0,0 +1,416 @@
package com.rosetta.messenger.data
import android.content.Context
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.crypto.MessageCrypto
import com.rosetta.messenger.database.*
import com.rosetta.messenger.network.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.UUID
/**
* UI модель сообщения
*/
data class Message(
val id: Long = 0,
val messageId: String,
val fromPublicKey: String,
val toPublicKey: String,
val content: String, // Расшифрованный текст
val timestamp: Long,
val isFromMe: Boolean,
val isRead: Boolean,
val deliveryStatus: DeliveryStatus,
val attachments: List<MessageAttachment> = emptyList(),
val replyToMessageId: String? = null
)
/**
* UI модель диалога
*/
data class Dialog(
val opponentKey: String,
val opponentTitle: String,
val opponentUsername: String,
val lastMessage: String,
val lastMessageTimestamp: Long,
val unreadCount: Int,
val isOnline: Boolean,
val lastSeen: Long,
val verified: Boolean
)
/**
* Repository для работы с сообщениями
* Оптимизированная версия с кэшированием и Optimistic UI
*/
class MessageRepository private constructor(private val context: Context) {
private val database = RosettaDatabase.getDatabase(context)
private val messageDao = database.messageDao()
private val dialogDao = database.dialogDao()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Кэш сообщений по диалогам
private val messageCache = mutableMapOf<String, MutableStateFlow<List<Message>>>()
// Кэш диалогов
private val _dialogs = MutableStateFlow<List<Dialog>>(emptyList())
val dialogs: StateFlow<List<Dialog>> = _dialogs.asStateFlow()
// Текущий аккаунт
private var currentAccount: String? = null
private var currentPrivateKey: String? = null
companion object {
@Volatile
private var INSTANCE: MessageRepository? = null
fun getInstance(context: Context): MessageRepository {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: MessageRepository(context.applicationContext).also { INSTANCE = it }
}
}
}
/**
* Инициализация с текущим аккаунтом
*/
fun initialize(publicKey: String, privateKey: String) {
currentAccount = publicKey
currentPrivateKey = privateKey
// Загрузка диалогов
scope.launch {
dialogDao.getDialogsFlow(publicKey).collect { entities ->
_dialogs.value = entities.map { it.toDialog() }
}
}
}
/**
* Получить поток сообщений для диалога
*/
fun getMessagesFlow(opponentKey: String): StateFlow<List<Message>> {
val dialogKey = getDialogKey(opponentKey)
return messageCache.getOrPut(dialogKey) {
MutableStateFlow<List<Message>>(emptyList()).also { flow ->
scope.launch {
currentAccount?.let { account ->
messageDao.getMessagesFlow(account, dialogKey).collect { entities ->
flow.value = entities.map { it.toMessage() }
}
}
}
}
}
}
/**
* Отправка сообщения с Optimistic UI
* Возвращает сразу, шифрование и отправка в фоне
*/
suspend fun sendMessage(
toPublicKey: String,
text: String,
attachments: List<MessageAttachment> = emptyList(),
replyToMessageId: String? = null
): Message {
val account = currentAccount ?: throw IllegalStateException("Not initialized")
val privateKey = currentPrivateKey ?: throw IllegalStateException("Not initialized")
val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
val timestamp = System.currentTimeMillis()
val dialogKey = getDialogKey(toPublicKey)
// 1. Создаем оптимистичное сообщение
val optimisticMessage = Message(
messageId = messageId,
fromPublicKey = account,
toPublicKey = toPublicKey,
content = text.trim(),
timestamp = timestamp,
isFromMe = true,
isRead = account == toPublicKey, // Если сам себе - сразу прочитано
deliveryStatus = DeliveryStatus.WAITING,
attachments = attachments,
replyToMessageId = replyToMessageId
)
// 2. Обновляем UI сразу (Optimistic Update)
updateMessageCache(dialogKey, optimisticMessage)
// 3. Фоновая обработка
scope.launch {
try {
// Шифрование
val (encryptedContent, encryptedKey) = MessageCrypto.encryptForSending(
text.trim(),
toPublicKey
)
// Сохраняем в БД
val entity = MessageEntity(
account = account,
fromPublicKey = account,
toPublicKey = toPublicKey,
content = encryptedContent,
timestamp = timestamp,
chachaKey = encryptedKey,
read = if (account == toPublicKey) 1 else 0,
fromMe = 1,
delivered = DeliveryStatus.WAITING.value,
messageId = messageId,
plainMessage = text.trim(),
attachments = "[]", // TODO: JSON serialize
replyToMessageId = replyToMessageId,
dialogKey = dialogKey
)
messageDao.insertMessage(entity)
// Обновляем диалог
updateDialog(toPublicKey, text.trim(), timestamp)
// Отправляем пакет
val packet = PacketMessage().apply {
this.fromPublicKey = account
this.toPublicKey = toPublicKey
this.content = encryptedContent
this.chachaKey = encryptedKey
this.timestamp = timestamp
this.privateKey = CryptoManager.generatePrivateKeyHash(privateKey)
this.messageId = messageId
this.attachments = attachments
}
ProtocolManager.send(packet)
} catch (e: Exception) {
// При ошибке обновляем статус
messageDao.updateDeliveryStatus(account, messageId, DeliveryStatus.ERROR.value)
updateMessageStatus(dialogKey, messageId, DeliveryStatus.ERROR)
}
}
return optimisticMessage
}
/**
* Обработка входящего сообщения
*/
suspend fun handleIncomingMessage(packet: PacketMessage) {
val account = currentAccount ?: return
val privateKey = currentPrivateKey ?: return
// Проверяем, не дубликат ли
if (messageDao.messageExists(account, packet.messageId)) return
val dialogKey = getDialogKey(packet.fromPublicKey)
try {
// Расшифровываем
val plainText = MessageCrypto.decryptIncoming(
packet.content,
packet.chachaKey,
privateKey
)
// Сохраняем в БД
val entity = MessageEntity(
account = account,
fromPublicKey = packet.fromPublicKey,
toPublicKey = packet.toPublicKey,
content = packet.content,
timestamp = packet.timestamp,
chachaKey = packet.chachaKey,
read = 0,
fromMe = 0,
delivered = DeliveryStatus.DELIVERED.value,
messageId = packet.messageId,
plainMessage = plainText,
attachments = "[]", // TODO
dialogKey = dialogKey
)
messageDao.insertMessage(entity)
// Обновляем диалог
updateDialog(packet.fromPublicKey, plainText, packet.timestamp, incrementUnread = true)
// Обновляем кэш
val message = entity.toMessage()
updateMessageCache(dialogKey, message)
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* Обработка подтверждения доставки
*/
suspend fun handleDelivery(packet: PacketDelivery) {
val account = currentAccount ?: return
messageDao.updateDeliveryStatus(account, packet.messageId, DeliveryStatus.DELIVERED.value)
// Обновляем кэш
val dialogKey = getDialogKey(packet.toPublicKey)
updateMessageStatus(dialogKey, packet.messageId, DeliveryStatus.DELIVERED)
}
/**
* Обработка прочтения
*/
suspend fun handleRead(packet: PacketRead) {
val account = currentAccount ?: return
messageDao.markAsRead(account, packet.messageId)
// Обновляем кэш
val dialogKey = getDialogKey(packet.fromPublicKey)
messageCache[dialogKey]?.let { flow ->
flow.value = flow.value.map { msg ->
if (msg.messageId == packet.messageId) msg.copy(isRead = true)
else msg
}
}
}
/**
* Отметить диалог как прочитанный
*/
suspend fun markDialogAsRead(opponentKey: String) {
val account = currentAccount ?: return
val dialogKey = getDialogKey(opponentKey)
messageDao.markDialogAsRead(account, dialogKey)
dialogDao.clearUnreadCount(account, opponentKey)
}
/**
* Отправить уведомление "печатает"
*/
fun sendTyping(toPublicKey: String) {
val account = currentAccount ?: return
val privateKey = currentPrivateKey ?: return
scope.launch {
val packet = PacketTyping().apply {
this.fromPublicKey = account
this.toPublicKey = toPublicKey
this.privateKey = CryptoManager.generatePrivateKeyHash(privateKey)
}
ProtocolManager.send(packet)
}
}
/**
* Создать или обновить диалог
*/
suspend fun createOrUpdateDialog(
opponentKey: String,
title: String = "",
username: String = "",
verified: Boolean = false
) {
val account = currentAccount ?: return
val existing = dialogDao.getDialog(account, opponentKey)
if (existing != null) {
dialogDao.updateOpponentInfo(account, opponentKey, title, username, if (verified) 1 else 0)
} else {
dialogDao.insertDialog(DialogEntity(
account = account,
opponentKey = opponentKey,
opponentTitle = title,
opponentUsername = username,
verified = if (verified) 1 else 0
))
}
}
// ===============================
// Private helpers
// ===============================
private fun getDialogKey(opponentKey: String): String {
val account = currentAccount ?: return opponentKey
return if (account < opponentKey) "$account:$opponentKey"
else "$opponentKey:$account"
}
private fun updateMessageCache(dialogKey: String, message: Message) {
messageCache[dialogKey]?.let { flow ->
val currentList = flow.value.toMutableList()
val existingIndex = currentList.indexOfFirst { it.messageId == message.messageId }
if (existingIndex >= 0) {
currentList[existingIndex] = message
} else {
currentList.add(message)
currentList.sortBy { it.timestamp }
}
flow.value = currentList
}
}
private fun updateMessageStatus(dialogKey: String, messageId: String, status: DeliveryStatus) {
messageCache[dialogKey]?.let { flow ->
flow.value = flow.value.map { msg ->
if (msg.messageId == messageId) msg.copy(deliveryStatus = status)
else msg
}
}
}
private suspend fun updateDialog(
opponentKey: String,
lastMessage: String,
timestamp: Long,
incrementUnread: Boolean = false
) {
val account = currentAccount ?: return
val existing = dialogDao.getDialog(account, opponentKey)
if (existing != null) {
dialogDao.updateLastMessage(account, opponentKey, lastMessage, timestamp)
if (incrementUnread) {
dialogDao.incrementUnreadCount(account, opponentKey)
}
} else {
dialogDao.insertDialog(DialogEntity(
account = account,
opponentKey = opponentKey,
lastMessage = lastMessage,
lastMessageTimestamp = timestamp,
unreadCount = if (incrementUnread) 1 else 0
))
}
}
// Extension functions
private fun MessageEntity.toMessage() = Message(
id = id,
messageId = messageId,
fromPublicKey = fromPublicKey,
toPublicKey = toPublicKey,
content = plainMessage,
timestamp = timestamp,
isFromMe = fromMe == 1,
isRead = read == 1,
deliveryStatus = DeliveryStatus.fromInt(delivered),
replyToMessageId = replyToMessageId
)
private fun DialogEntity.toDialog() = Dialog(
opponentKey = opponentKey,
opponentTitle = opponentTitle,
opponentUsername = opponentUsername,
lastMessage = lastMessage,
lastMessageTimestamp = lastMessageTimestamp,
unreadCount = unreadCount,
isOnline = isOnline == 1,
lastSeen = lastSeen,
verified = verified == 1
)
}