feat: Enhance chat functionality by updating dialog handling and adding ChatsListViewModel for database integration

This commit is contained in:
k1ngsterr1
2026-01-10 23:06:41 +05:00
parent 7216cc0d0b
commit 219158ac7d
5 changed files with 403 additions and 51 deletions

View File

@@ -274,83 +274,89 @@ object MessageCrypto {
/**
* ECDH шифрование ключа для получателя
* Использует secp256k1 + AES как в RN версии
* Формат: Base64(iv:ciphertext:ephemeralPrivateKeyHex)
*/
fun encryptKeyForRecipient(keyAndNonce: ByteArray, recipientPublicKeyHex: String): String {
val secureRandom = SecureRandom()
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
// Генерируем эфемерный приватный ключ
// Генерируем эфемерный приватный ключ (32 байта)
val ephemeralPrivateKeyBytes = ByteArray(32)
secureRandom.nextBytes(ephemeralPrivateKeyBytes)
val ephemeralPrivateKey = BigInteger(1, ephemeralPrivateKeyBytes)
val ephemeralPrivateKeyHex = ephemeralPrivateKeyBytes.toHex()
// Получаем эфемерный публичный ключ
val ephemeralPublicKey = ecSpec.g.multiply(ephemeralPrivateKey)
val ephemeralPublicKeyHex = ephemeralPublicKey.getEncoded(false).toHex()
// Парсим публичный ключ получателя
val recipientPublicKeyBytes = recipientPublicKeyHex.hexToBytes()
val recipientPublicKey = ecSpec.curve.decodePoint(recipientPublicKeyBytes)
// ECDH: получаем общий секрет
// ECDH: ephemeralPrivate * recipientPublic = sharedSecret
val sharedPoint = recipientPublicKey.multiply(ephemeralPrivateKey)
val sharedSecret = sharedPoint.normalize().xCoord.encoded
val sharedSecretHex = sharedSecret.toHex()
// Derive AES key from shared secret
val aesKey = MessageDigest.getInstance("SHA-256").digest(sharedSecret)
// Генерируем IV для AES
// Генерируем IV для AES (16 байт)
val iv = ByteArray(16)
secureRandom.nextBytes(iv)
val ivHex = iv.toHex()
// Шифруем keyAndNonce с AES-CBC
val secretKey = SecretKeySpec(aesKey, "AES")
// Шифруем keyAndNonce с AES-CBC используя sharedSecret как ключ
// ВАЖНО: Используем сам sharedSecret напрямую (32 байта) без SHA-256!
val aesKey = SecretKeySpec(sharedSecret, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
cipher.init(Cipher.ENCRYPT_MODE, aesKey, IvParameterSpec(iv))
val encryptedKey = cipher.doFinal(keyAndNonce)
val encryptedKeyHex = encryptedKey.toHex()
// Формат: iv:ciphertext:ephemeralPublicKey (Base64)
val result = ByteArray(iv.size + encryptedKey.size + recipientPublicKeyBytes.size)
System.arraycopy(iv, 0, result, 0, iv.size)
System.arraycopy(encryptedKey, 0, result, iv.size, encryptedKey.size)
// Формат как в RN: btoa(ivHex:encryptedHex:ephemeralPrivateHex)
val combined = "$ivHex:$encryptedKeyHex:$ephemeralPrivateKeyHex"
// Возвращаем как Base64 с ephemeral public key в конце
return Base64.encodeToString(iv, Base64.NO_WRAP) + ":" +
Base64.encodeToString(encryptedKey, Base64.NO_WRAP) + ":" +
ephemeralPublicKeyHex
ProtocolManager.addLog("🔐 ECDH Encrypt:")
ProtocolManager.addLog(" - Shared secret: ${sharedSecretHex.take(40)}...")
ProtocolManager.addLog(" - IV: ${ivHex.take(32)}...")
ProtocolManager.addLog(" - Ephemeral private: ${ephemeralPrivateKeyHex.take(40)}...")
ProtocolManager.addLog(" - Recipient public: ${recipientPublicKeyHex.take(40)}...")
return Base64.encodeToString(combined.toByteArray(), Base64.NO_WRAP)
}
/**
* ECDH расшифровка ключа
* Формат: Base64(ivHex:encryptedHex:ephemeralPrivateHex)
*/
fun decryptKeyFromSender(encryptedKeyBase64: String, myPrivateKeyHex: String): ByteArray {
val parts = encryptedKeyBase64.split(":")
if (parts.size != 3) throw IllegalArgumentException("Invalid encrypted key format")
val combined = String(Base64.decode(encryptedKeyBase64, Base64.NO_WRAP))
val parts = combined.split(":")
if (parts.size != 3) throw IllegalArgumentException("Invalid encrypted key format: expected 3 parts, got ${parts.size}")
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
val encryptedKey = Base64.decode(parts[1], Base64.NO_WRAP)
val ephemeralPublicKeyHex = parts[2]
val ivHex = parts[0]
val encryptedKeyHex = parts[1]
val ephemeralPrivateKeyHex = parts[2]
val iv = ivHex.hexToBytes()
val encryptedKey = encryptedKeyHex.hexToBytes()
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
// Парсим эфемерный приватный ключ
val ephemeralPrivateKey = BigInteger(ephemeralPrivateKeyHex, 16)
val ephemeralPublicKey = ecSpec.g.multiply(ephemeralPrivateKey)
// Парсим мой приватный ключ
val myPrivateKey = BigInteger(myPrivateKeyHex, 16)
val myPublicKey = ecSpec.g.multiply(myPrivateKey)
// Парсим эфемерный публичный ключ отправителя
val ephemeralPublicKeyBytes = ephemeralPublicKeyHex.hexToBytes()
val ephemeralPublicKey = ecSpec.curve.decodePoint(ephemeralPublicKeyBytes)
// ECDH: получаем общий секрет
val sharedPoint = ephemeralPublicKey.multiply(myPrivateKey)
// ECDH: ephemeralPrivate * myPublic = sharedSecret
val sharedPoint = myPublicKey.multiply(ephemeralPrivateKey)
val sharedSecret = sharedPoint.normalize().xCoord.encoded
// Derive AES key from shared secret
val aesKey = MessageDigest.getInstance("SHA-256").digest(sharedSecret)
// Расшифровываем
val secretKey = SecretKeySpec(aesKey, "AES")
// Расшифровываем используя sharedSecret как AES ключ
val aesKey = SecretKeySpec(sharedSecret, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
return cipher.doFinal(encryptedKey)
}
@@ -359,15 +365,31 @@ object MessageCrypto {
* Полное шифрование сообщения для отправки
*/
fun encryptForSending(plaintext: String, recipientPublicKey: String): Pair<String, String> {
android.util.Log.d("MessageCrypto", "🔐 === START ENCRYPTION ===")
android.util.Log.d("MessageCrypto", "📝 Plaintext: '$plaintext'")
android.util.Log.d("MessageCrypto", "📝 Plaintext length: ${plaintext.length}")
android.util.Log.d("MessageCrypto", "🔑 Recipient public key: ${recipientPublicKey.take(20)}...")
// 1. Шифруем текст
android.util.Log.d("MessageCrypto", "⚡ Step 1: Encrypting message with XChaCha20-Poly1305...")
val encrypted = encryptMessage(plaintext)
android.util.Log.d("MessageCrypto", "✅ Ciphertext: ${encrypted.ciphertext.take(40)}...")
android.util.Log.d("MessageCrypto", "✅ Ciphertext length: ${encrypted.ciphertext.length}")
android.util.Log.d("MessageCrypto", "✅ Key: ${encrypted.key.take(20)}...")
android.util.Log.d("MessageCrypto", "✅ Nonce: ${encrypted.nonce.take(20)}...")
// 2. Собираем key + nonce
android.util.Log.d("MessageCrypto", "⚡ Step 2: Combining key + nonce...")
val keyAndNonce = encrypted.key.hexToBytes() + encrypted.nonce.hexToBytes()
android.util.Log.d("MessageCrypto", "✅ Combined keyAndNonce length: ${keyAndNonce.size} bytes")
// 3. Шифруем ключ для получателя
android.util.Log.d("MessageCrypto", "⚡ Step 3: Encrypting key with ECDH + AES...")
val encryptedKey = encryptKeyForRecipient(keyAndNonce, recipientPublicKey)
android.util.Log.d("MessageCrypto", "✅ Encrypted key: ${encryptedKey.take(40)}...")
android.util.Log.d("MessageCrypto", "✅ Encrypted key length: ${encryptedKey.length}")
android.util.Log.d("MessageCrypto", "🔐 === ENCRYPTION COMPLETE ===")
return Pair(encrypted.ciphertext, encryptedKey)
}

View File

@@ -86,7 +86,7 @@ private fun Message.toChatMessage() = ChatMessage(
/**
* Экран детального чата с пользователем
*/
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun ChatDetailScreen(
user: SearchUser,
@@ -97,6 +97,7 @@ fun ChatDetailScreen(
onUserProfileClick: () -> Unit = {},
viewModel: ChatViewModel = viewModel()
) {
val keyboardController = LocalSoftwareKeyboardController.current
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
@@ -122,7 +123,7 @@ fun ChatDetailScreen(
// Инициализируем ViewModel с ключами и открываем диалог
LaunchedEffect(user.publicKey) {
viewModel.setUserKeys(currentUserPublicKey, currentUserPrivateKey)
viewModel.openDialog(user.publicKey)
viewModel.openDialog(user.publicKey, user.title, user.username)
}
// Прокрутка при новых сообщениях
@@ -154,7 +155,10 @@ fun ChatDetailScreen(
verticalAlignment = Alignment.CenterVertically
) {
// Кнопка назад
IconButton(onClick = onBack) {
IconButton(onClick = {
keyboardController?.hide()
onBack()
}) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back",
@@ -545,7 +549,7 @@ private fun MessageInputBar(
Column(
modifier = Modifier
.fillMaxWidth()
.imePadding()
.then(if (!showEmojiPicker) Modifier.imePadding() else Modifier)
) {
Box(
modifier = Modifier
@@ -709,8 +713,8 @@ private fun MessageInputBar(
// Apple Emoji Picker
AnimatedVisibility(
visible = showEmojiPicker,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut()
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut()
) {
AppleEmojiPickerPanel(
isDarkTheme = isDarkTheme,

View File

@@ -6,6 +6,8 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.crypto.MessageCrypto
import com.rosetta.messenger.database.DialogEntity
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@@ -22,6 +24,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
private const val TAG = "ChatViewModel"
}
// Database
private val database = RosettaDatabase.getDatabase(application)
private val dialogDao = database.dialogDao()
// Информация о собеседнике
private var opponentTitle: String = ""
private var opponentUsername: String = ""
// Текущий диалог
private var opponentKey: String? = null
private var myPublicKey: String? = null
@@ -120,14 +130,16 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
/**
* Открыть диалог
*/
fun openDialog(publicKey: String) {
fun openDialog(publicKey: String, title: String = "", username: String = "") {
if (opponentKey == publicKey) {
ProtocolManager.addLog("💬 Dialog already open: ${publicKey.take(16)}...")
return
}
opponentKey = publicKey
opponentTitle = title
opponentUsername = username
_messages.value = emptyList()
ProtocolManager.addLog("💬 Dialog opened: ${publicKey.take(16)}...")
ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...")
}
/**
@@ -179,15 +191,26 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
_messages.value = _messages.value + optimisticMessage
_inputText.value = ""
ProtocolManager.addLog("📤 Send: \"${text.take(20)}...\"")
ProtocolManager.addLog("📋 Messages count: ${_messages.value.size}")
ProtocolManager.addLog("📤 === START SENDING MESSAGE ===")
ProtocolManager.addLog("📤 Text: \"${text.take(20)}...\"")
ProtocolManager.addLog("📤 Text length: ${text.length}")
ProtocolManager.addLog("📤 Recipient: ${recipient.take(20)}...")
ProtocolManager.addLog("📤 Sender: ${sender.take(20)}...")
ProtocolManager.addLog("📤 Message ID: $messageId")
ProtocolManager.addLog("📋 Current messages count: ${_messages.value.size}")
// 2. Отправка в фоне
viewModelScope.launch {
try {
ProtocolManager.addLog("🔐 Encrypting...")
ProtocolManager.addLog("🔐 Starting encryption...")
val (encryptedContent, encryptedKey) = MessageCrypto.encryptForSending(text, recipient)
ProtocolManager.addLog(" Encrypted")
ProtocolManager.addLog(" Encryption complete")
ProtocolManager.addLog(" - Encrypted content length: ${encryptedContent.length}")
ProtocolManager.addLog(" - Encrypted key length: ${encryptedKey.length}")
ProtocolManager.addLog("📦 Creating PacketMessage...")
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
ProtocolManager.addLog("🔑 Private key hash: ${privateKeyHash.take(20)}...")
val packet = PacketMessage().apply {
fromPublicKey = sender
@@ -195,16 +218,29 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
content = encryptedContent
chachaKey = encryptedKey
this.timestamp = timestamp
this.privateKey = CryptoManager.generatePrivateKeyHash(privateKey)
this.privateKey = privateKeyHash
this.messageId = messageId
attachments = emptyList()
}
ProtocolManager.addLog("📡 Sending packet...")
ProtocolManager.addLog("<EFBFBD> Packet created:")
ProtocolManager.addLog(" - From: ${sender.take(20)}...")
ProtocolManager.addLog(" - To: ${recipient.take(20)}...")
ProtocolManager.addLog(" - Content: ${encryptedContent.take(40)}...")
ProtocolManager.addLog(" - ChaCha Key: ${encryptedKey.take(40)}...")
ProtocolManager.addLog(" - Timestamp: $timestamp")
ProtocolManager.addLog(" - Message ID: $messageId")
ProtocolManager.addLog("📡 Sending packet to server...")
ProtocolManager.send(packet)
ProtocolManager.addLog("📡 Packet sent to ProtocolManager")
updateMessageStatus(messageId, MessageStatus.SENT)
ProtocolManager.addLog("✓ Sent!")
ProtocolManager.addLog("✅ Message status updated to SENT")
ProtocolManager.addLog("📤 === SENDING COMPLETE ===")
// Сохраняем/обновляем диалог в базе
saveDialog(text, timestamp)
} catch (e: Exception) {
ProtocolManager.addLog("❌ Error: ${e.message}")
@@ -215,6 +251,36 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
}
/**
* Сохранить диалог в базу данных
*/
private suspend fun saveDialog(lastMessage: String, timestamp: Long) {
val account = myPublicKey ?: return
val opponent = opponentKey ?: return
try {
val existingDialog = dialogDao.getDialog(account, opponent)
if (existingDialog != null) {
// Обновляем последнее сообщение
dialogDao.updateLastMessage(account, opponent, lastMessage, timestamp)
} else {
// Создаём новый диалог
dialogDao.insertDialog(DialogEntity(
account = account,
opponentKey = opponent,
opponentTitle = opponentTitle,
opponentUsername = opponentUsername,
lastMessage = lastMessage,
lastMessageTimestamp = timestamp
))
}
ProtocolManager.addLog("💾 Dialog saved")
} catch (e: Exception) {
ProtocolManager.addLog("❌ Failed to save dialog: ${e.message}")
}
}
private fun showTypingIndicator() {
_opponentTyping.value = true
viewModelScope.launch {

View File

@@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.lottie.compose.*
import com.rosetta.messenger.R
import com.rosetta.messenger.database.DialogEntity
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.network.ProtocolState
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
@@ -148,6 +149,7 @@ fun ChatsListScreen(
onSearchClick: () -> Unit,
onNewChat: () -> Unit,
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {},
chatsViewModel: ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
onLogout: () -> Unit
) {
// Theme transition state
@@ -187,6 +189,16 @@ fun ChatsListScreen(
val protocolState by ProtocolManager.state.collectAsState()
val debugLogs by ProtocolManager.debugLogs.collectAsState()
// Dialogs from database
val dialogsList by chatsViewModel.dialogs.collectAsState()
// Load dialogs when account is available
LaunchedEffect(accountPublicKey) {
if (accountPublicKey.isNotEmpty()) {
chatsViewModel.setAccount(accountPublicKey)
}
}
// Dev console state
var showDevConsole by remember { mutableStateOf(false) }
var titleClickCount by remember { mutableStateOf(0) }
@@ -776,12 +788,28 @@ fun ChatsListScreen(
}
)
}
} else {
} else if (dialogsList.isEmpty()) {
// Empty state with Lottie animation
EmptyChatsState(
isDarkTheme = isDarkTheme,
modifier = Modifier.fillMaxSize()
)
} else {
// Show dialogs list
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(dialogsList, key = { it.opponentKey }) { dialog ->
DialogItem(
dialog = dialog,
isDarkTheme = isDarkTheme,
onClick = {
val user = chatsViewModel.dialogToSearchUser(dialog)
onUserSelect(user)
}
)
}
}
}
// Console button - always visible at bottom left
@@ -1096,3 +1124,138 @@ private fun formatTime(date: Date): String {
}
}
}
/**
* Элемент диалога из базы данных
*/
@Composable
fun DialogItem(
dialog: DialogEntity,
isDarkTheme: Boolean,
onClick: () -> Unit
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
val avatarColors = getAvatarColor(dialog.opponentKey, isDarkTheme)
val displayName = dialog.opponentTitle.ifEmpty { dialog.opponentKey.take(8) }
val initials = if (dialog.opponentTitle.isNotEmpty()) {
dialog.opponentTitle.split(" ")
.take(2)
.mapNotNull { it.firstOrNull()?.uppercase() }
.joinToString("")
} else {
dialog.opponentKey.take(2).uppercase()
}
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
) {
Text(
text = initials,
color = avatarColors.textColor,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp
)
// Online indicator
if (dialog.isOnline == 1) {
Box(
modifier = Modifier
.size(14.dp)
.align(Alignment.BottomEnd)
.offset(x = (-2).dp, y = (-2).dp)
.clip(CircleShape)
.background(if (isDarkTheme) Color(0xFF1A1A1A) else Color.White)
.padding(2.dp)
.clip(CircleShape)
.background(Color(0xFF4CAF50))
)
}
}
Spacer(modifier = Modifier.width(12.dp))
// Name and last message
Column(modifier = Modifier.weight(1f)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = displayName,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Text(
text = formatTime(Date(dialog.lastMessageTimestamp)),
fontSize = 13.sp,
color = if (dialog.unreadCount > 0) PrimaryBlue else secondaryTextColor
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = dialog.lastMessage.ifEmpty { "No messages" },
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
// Unread badge
if (dialog.unreadCount > 0) {
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.size(22.dp)
.clip(CircleShape)
.background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Text(
text = if (dialog.unreadCount > 99) "99+" else dialog.unreadCount.toString(),
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
}
}
}
}
}
Divider(
modifier = Modifier.padding(start = 84.dp),
color = dividerColor,
thickness = 0.5.dp
)
}
}

View File

@@ -0,0 +1,97 @@
package com.rosetta.messenger.ui.chats
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.rosetta.messenger.database.DialogEntity
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.network.SearchUser
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
/**
* ViewModel для списка чатов
* Загружает диалоги из базы данных
*/
class ChatsListViewModel(application: Application) : AndroidViewModel(application) {
private val database = RosettaDatabase.getDatabase(application)
private val dialogDao = database.dialogDao()
private var currentAccount: String = ""
// Список диалогов из базы
private val _dialogs = MutableStateFlow<List<DialogEntity>>(emptyList())
val dialogs: StateFlow<List<DialogEntity>> = _dialogs.asStateFlow()
// Загрузка
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
/**
* Установить текущий аккаунт и загрузить диалоги
*/
fun setAccount(publicKey: String) {
if (currentAccount == publicKey) return
currentAccount = publicKey
viewModelScope.launch {
dialogDao.getDialogsFlow(publicKey)
.collect { dialogsList ->
_dialogs.value = dialogsList
ProtocolManager.addLog("📋 Dialogs loaded: ${dialogsList.size}")
}
}
}
/**
* Создать или обновить диалог после отправки/получения сообщения
*/
suspend fun upsertDialog(
opponentKey: String,
opponentTitle: String,
opponentUsername: String = "",
lastMessage: String,
timestamp: Long,
verified: Int = 0,
isOnline: Int = 0
) {
if (currentAccount.isEmpty()) return
val existingDialog = dialogDao.getDialog(currentAccount, opponentKey)
if (existingDialog != null) {
// Обновляем
dialogDao.updateLastMessage(currentAccount, opponentKey, lastMessage, timestamp)
if (opponentTitle.isNotEmpty()) {
dialogDao.updateOpponentInfo(currentAccount, opponentKey, opponentTitle, opponentUsername, verified)
}
} else {
// Создаём новый
dialogDao.insertDialog(DialogEntity(
account = currentAccount,
opponentKey = opponentKey,
opponentTitle = opponentTitle,
opponentUsername = opponentUsername,
lastMessage = lastMessage,
lastMessageTimestamp = timestamp,
verified = verified,
isOnline = isOnline
))
}
}
/**
* Конвертировать DialogEntity в SearchUser для навигации
*/
fun dialogToSearchUser(dialog: DialogEntity): SearchUser {
return SearchUser(
title = dialog.opponentTitle,
username = dialog.opponentUsername,
publicKey = dialog.opponentKey,
verified = dialog.verified,
online = dialog.isOnline
)
}
}