feat: Implement FCM token handling and dialog cache management; enhance user experience and performance

This commit is contained in:
k1ngsterr1
2026-01-17 05:53:27 +05:00
parent c9724b3bb7
commit 52ffc22763
9 changed files with 333 additions and 65 deletions

View File

@@ -43,8 +43,10 @@ import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.PreferencesManager
import com.rosetta.messenger.data.RecentSearchesManager
import com.rosetta.messenger.network.PacketPushToken
import com.rosetta.messenger.network.PacketPushNotification
import com.rosetta.messenger.network.PushNotificationAction
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.network.ProtocolState
import com.rosetta.messenger.ui.auth.AccountInfo
import com.rosetta.messenger.ui.auth.AuthFlow
import com.rosetta.messenger.ui.chats.ChatsListScreen
@@ -57,6 +59,7 @@ import com.rosetta.messenger.ui.onboarding.OnboardingScreen
import com.rosetta.messenger.ui.splash.SplashScreen
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
class MainActivity : ComponentActivity() {
private lateinit var preferencesManager: PreferencesManager
@@ -331,19 +334,32 @@ class MainActivity : ComponentActivity() {
return@launch
}
Log.d(TAG, "📤 Sending FCM token to server...")
Log.d(TAG, " Token (short): ${token.take(20)}...")
Log.d(TAG, " PublicKey: ${account.publicKey.take(20)}...")
// 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED
var waitAttempts = 0
while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED && waitAttempts < 50) {
Log.d(TAG, "⏳ Waiting for protocol to be authenticated... (attempt ${waitAttempts + 1}/50)")
delay(100) // Ждем 100ms
waitAttempts++
}
val packet = PacketPushToken().apply {
this.privateKey = account.privateKey
this.publicKey = account.publicKey
this.pushToken = token
this.platform = "android"
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
Log.e(TAG, "❌ Cannot send FCM token: Protocol not authenticated after 5 seconds")
return@launch
}
Log.d(TAG, "📤 Sending FCM token to server (new format 0x10)...")
Log.d(TAG, " Token (short): ${token.take(20)}...")
Log.d(TAG, " Token (FULL): $token")
Log.d(TAG, " Action: SUBSCRIBE")
Log.d(TAG, " Protocol state: ${ProtocolManager.state.value}")
val packet = PacketPushNotification().apply {
this.notificationsToken = token
this.action = PushNotificationAction.SUBSCRIBE
}
ProtocolManager.send(packet)
Log.d(TAG, "✅ FCM token sent to server")
Log.d(TAG, "✅ FCM token sent to server (packet ID: 0x10)")
} catch (e: Exception) {
Log.e(TAG, "❌ Error sending FCM token to server", e)
}

View File

@@ -551,6 +551,23 @@ class MessageRepository private constructor(private val context: Context) {
}
}
/**
* Очистить кэш сообщений для конкретного диалога
* 🔥 ВАЖНО: Устанавливаем пустой список, а не просто удаляем -
* чтобы подписчики Flow увидели что диалог пуст
*/
fun clearDialogCache(opponentKey: String) {
val dialogKey = getDialogKey(opponentKey)
android.util.Log.d("MessageRepo", "🗑️ clearDialogCache: dialogKey=$dialogKey")
// Сначала устанавливаем пустой список чтобы все подписчики увидели
messageCache[dialogKey]?.value = emptyList()
// Затем удаляем из кэша
messageCache.remove(dialogKey)
android.util.Log.d("MessageRepo", "🗑️ Cache cleared for dialogKey=$dialogKey")
}
/**
* Запросить информацию о пользователе с сервера
*/

View File

@@ -236,13 +236,13 @@ interface MessageDao {
suspend fun getUnreadCountForDialog(account: String, opponentKey: String): Int
/**
* Удалить все сообщения диалога
* Удалить все сообщения диалога (возвращает количество удалённых)
*/
@Query("DELETE FROM messages WHERE account = :account AND dialog_key = :dialogKey")
suspend fun deleteDialog(account: String, dialogKey: String)
suspend fun deleteDialog(account: String, dialogKey: String): Int
/**
* Удалить все сообщения между двумя пользователями
* Удалить все сообщения между двумя пользователями (возвращает количество удалённых)
*/
@Query("""
DELETE FROM messages
@@ -251,7 +251,7 @@ interface MessageDao {
(from_public_key = :user2 AND to_public_key = :user1)
)
""")
suspend fun deleteMessagesBetweenUsers(account: String, user1: String, user2: String)
suspend fun deleteMessagesBetweenUsers(account: String, user1: String, user2: String): Int
/**
* Количество непрочитанных сообщений в диалоге
@@ -294,33 +294,39 @@ interface DialogDao {
/**
* Получить все диалоги отсортированные по последнему сообщению
* Исключает requests (диалоги без исходящих сообщений от нас)
* Исключает пустые диалоги (без сообщений)
*/
@Query("""
SELECT * FROM dialogs
WHERE account = :account
AND i_have_sent = 1
AND last_message_timestamp > 0
ORDER BY last_message_timestamp DESC
""")
fun getDialogsFlow(account: String): Flow<List<DialogEntity>>
/**
* Получить requests - диалоги где нам писали, но мы не отвечали
* Исключает пустые диалоги (без сообщений)
*/
@Query("""
SELECT * FROM dialogs
WHERE account = :account
AND i_have_sent = 0
AND last_message_timestamp > 0
ORDER BY last_message_timestamp DESC
""")
fun getRequestsFlow(account: String): Flow<List<DialogEntity>>
/**
* Получить количество requests
* Исключает пустые диалоги (без сообщений)
*/
@Query("""
SELECT COUNT(*) FROM dialogs
WHERE account = :account
AND i_have_sent = 0
AND last_message_timestamp > 0
""")
fun getRequestsCountFlow(account: String): Flow<Int>
@@ -419,7 +425,7 @@ interface DialogDao {
* 1. Берем последнее сообщение (по timestamp DESC)
* 2. Считаем количество непрочитанных сообщений (from_me = 0 AND read = 0)
* 3. Вычисляем i_have_sent = 1 если есть исходящие сообщения (from_me = 1) - как sended в Архиве
* 4. Обновляем диалог или создаем новый
* 4. Обновляем диалог или создаем новый ТОЛЬКО если есть сообщения!
*/
@Query("""
INSERT OR REPLACE INTO dialogs (
@@ -494,6 +500,12 @@ interface DialogDao {
0
)
END AS i_have_sent
WHERE EXISTS (
SELECT 1 FROM messages
WHERE account = :account
AND ((from_public_key = :opponentKey AND to_public_key = :account)
OR (from_public_key = :account AND to_public_key = :opponentKey))
)
""")
suspend fun updateDialogFromMessages(account: String, opponentKey: String)
}

View File

@@ -454,8 +454,8 @@ class PacketChunk : Packet() {
}
/**
* Push Token packet (ID: 0x0A)
* Отправка FCM/APNS токена на сервер для push-уведомлений
* Push Token packet (ID: 0x0A) - DEPRECATED
* Старый формат, заменен на PacketPushNotification (0x10)
*/
class PacketPushToken : Packet() {
var privateKey: String = ""
@@ -482,3 +482,39 @@ class PacketPushToken : Packet() {
return stream
}
}
/**
* Push Notification Action
*/
enum class PushNotificationAction(val value: Int) {
SUBSCRIBE(0),
UNSUBSCRIBE(1)
}
/**
* Push Notification packet (ID: 0x10)
* Отправка FCM/APNS токена на сервер для push-уведомлений (новый формат)
* Совместим с React Native версией
*/
class PacketPushNotification : Packet() {
var notificationsToken: String = ""
var action: PushNotificationAction = PushNotificationAction.SUBSCRIBE
override fun getPacketId(): Int = 0x10
override fun receive(stream: Stream) {
notificationsToken = stream.readString()
action = when (stream.readInt8()) {
1 -> PushNotificationAction.UNSUBSCRIBE
else -> PushNotificationAction.SUBSCRIBE
}
}
override fun send(): Stream {
val stream = Stream()
stream.writeInt16(getPacketId())
stream.writeString(notificationsToken)
stream.writeInt8(action.value)
return stream
}
}

View File

@@ -51,32 +51,8 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
// Сохраняем токен локально
saveFcmToken(token)
// TODO: Отправляем токен на сервер если аккаунт уже залогинен
/*
serviceScope.launch {
try {
val accountManager = AccountManager(applicationContext)
val currentAccount = accountManager.getCurrentAccount()
if (currentAccount != null) {
Log.d(TAG, "📤 Sending FCM token to server for account: ${currentAccount.publicKey.take(10)}...")
// Отправляем через протокол
val packet = PacketPushToken().apply {
this.privateKey = CryptoManager.generatePrivateKeyHash(currentAccount.privateKey)
this.publicKey = currentAccount.publicKey
this.pushToken = token
this.platform = "android"
}
ProtocolManager.send(packet)
Log.d(TAG, "✅ FCM token sent to server")
}
} catch (e: Exception) {
Log.e(TAG, "❌ Error sending FCM token to server", e)
}
}
*/
// 📤 Токен будет отправлен на сервер после успешного логина в MainActivity
Log.d(TAG, "💾 FCM token saved. Will be sent to server after login.")
}
/**

View File

@@ -1283,31 +1283,59 @@ fun ChatDetailScreen(
showDeleteConfirm = false
scope.launch {
try {
android.util.Log.d("ChatDetail", "🗑️ ========== DELETE CHAT START ==========")
android.util.Log.d("ChatDetail", "🗑️ currentUserPublicKey=${currentUserPublicKey}")
android.util.Log.d("ChatDetail", "🗑️ user.publicKey=${user.publicKey}")
// Вычисляем правильный dialog_key (отсортированная комбинация ключей)
val dialogKey = if (currentUserPublicKey < user.publicKey) {
"$currentUserPublicKey:${user.publicKey}"
} else {
"${user.publicKey}:$currentUserPublicKey"
}
android.util.Log.d("ChatDetail", "🗑️ dialogKey=$dialogKey")
// 🗑️ Очищаем ВСЕ кэши сообщений
com.rosetta.messenger.data.MessageRepository.getInstance(context).clearDialogCache(user.publicKey)
// 🗑️ Очищаем кэш ChatViewModel
ChatViewModel.clearCacheForOpponent(user.publicKey)
// Проверяем количество сообщений до удаления
val countBefore = database.messageDao().getMessageCount(currentUserPublicKey, dialogKey)
android.util.Log.d("ChatDetail", "🗑️ Messages BEFORE delete: $countBefore")
// Удаляем все сообщения из диалога по dialog_key
database.messageDao().deleteDialog(
val deletedByKey = database.messageDao().deleteDialog(
account = currentUserPublicKey,
dialogKey = dialogKey
)
android.util.Log.d("ChatDetail", "🗑️ Deleted by dialogKey: $deletedByKey")
// Также пробуем удалить по from/to ключам (на всякий случай)
database.messageDao().deleteMessagesBetweenUsers(
val deletedBetween = database.messageDao().deleteMessagesBetweenUsers(
account = currentUserPublicKey,
user1 = user.publicKey,
user2 = currentUserPublicKey
)
android.util.Log.d("ChatDetail", "🗑️ Deleted between users: $deletedBetween")
// Проверяем количество сообщений после удаления
val countAfter = database.messageDao().getMessageCount(currentUserPublicKey, dialogKey)
android.util.Log.d("ChatDetail", "🗑️ Messages AFTER delete: $countAfter")
// Очищаем кеш диалога
database.dialogDao().deleteDialog(
account = currentUserPublicKey,
opponentKey = user.publicKey
)
android.util.Log.d("ChatDetail", "🗑️ Dialog deleted from DB")
// Проверяем что диалог удален
val dialogAfter = database.dialogDao().getDialog(currentUserPublicKey, user.publicKey)
android.util.Log.d("ChatDetail", "🗑️ Dialog after: ${dialogAfter?.opponentKey ?: "NULL (deleted)"}")
android.util.Log.d("ChatDetail", "🗑️ ========== DELETE CHAT COMPLETE ==========")
} catch (e: Exception) {
// Error deleting chat
android.util.Log.e("ChatDetail", "🗑️ DELETE ERROR: ${e.message}", e)
}
// Выходим ПОСЛЕ удаления
onBack()

View File

@@ -33,6 +33,32 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
private const val TAG = "ChatViewModel"
private const val PAGE_SIZE = 30
private const val DECRYPT_CHUNK_SIZE = 5 // Расшифровываем по 5 сообщений за раз
// 🔥 ГЛОБАЛЬНЫЙ кэш сообщений на уровне диалогов (dialogKey -> List<ChatMessage>)
// Сделан глобальным чтобы можно было очистить при удалении диалога
private val dialogMessagesCache = ConcurrentHashMap<String, List<ChatMessage>>()
/**
* 🗑️ Очистить кэш сообщений для диалога
* Вызывается при удалении диалога
*/
fun clearDialogCache(dialogKey: String) {
android.util.Log.d(TAG, "🗑️ Clearing ChatViewModel cache for: $dialogKey")
dialogMessagesCache.remove(dialogKey)
}
/**
* 🗑️ Очистить кэш по publicKey собеседника
* Удаляет все ключи содержащие этот publicKey
*/
fun clearCacheForOpponent(opponentKey: String) {
android.util.Log.d(TAG, "🗑️ Clearing ChatViewModel cache for opponent: ${opponentKey.take(16)}...")
val keysToRemove = dialogMessagesCache.keys.filter { it.contains(opponentKey) }
keysToRemove.forEach {
android.util.Log.d(TAG, "🗑️ Removing cache key: $it")
dialogMessagesCache.remove(it)
}
}
}
// Database
@@ -46,10 +72,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔥 Кэш расшифрованных сообщений (messageId -> plainText)
private val decryptionCache = ConcurrentHashMap<String, String>()
// 🔥 Кэш сообщений на уровне диалогов (dialogKey -> List<ChatMessage>)
// При повторном входе в тот же чат - мгновенная загрузка из кэша!
private val dialogMessagesCache = ConcurrentHashMap<String, List<ChatMessage>>()
// Информация о собеседнике
private var opponentTitle: String = ""
private var opponentUsername: String = ""

View File

@@ -27,11 +27,14 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalContext
import android.content.Context
import com.airbnb.lottie.compose.*
import com.rosetta.messenger.R
import com.rosetta.messenger.data.RecentSearchesManager
@@ -186,9 +189,6 @@ fun ChatsListScreen(
// 🔥 Пользователи, которые сейчас печатают
val typingUsers by ProtocolManager.typingUsers.collectAsState()
// Dialogs from database
val dialogsList by chatsViewModel.dialogs.collectAsState()
// Load dialogs when account is available
LaunchedEffect(accountPublicKey, accountPrivateKey) {
if (accountPublicKey.isNotEmpty() && accountPrivateKey.isNotEmpty()) {
@@ -203,7 +203,11 @@ fun ChatsListScreen(
// Status dialog state
var showStatusDialog by remember { mutableStateOf(false) }
// 📬 Requests screen state
// <EFBFBD> FCM токен диалог
var showFcmDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
// <20>📬 Requests screen state
var showRequestsScreen by remember { mutableStateOf(false) }
// 🔥 Используем rememberSaveable чтобы сохранить состояние при навигации
@@ -277,6 +281,88 @@ fun ChatsListScreen(
containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White
)
}
// 🔔 FCM Token Dialog
if (showFcmDialog) {
val prefs = context.getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
val fcmToken = prefs.getString("fcm_token", "No token found") ?: "No token found"
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
AlertDialog(
onDismissRequest = { showFcmDialog = false },
title = {
Text(
"FCM Push Token",
fontWeight = FontWeight.Bold,
color = textColor
)
},
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Text(
"Token:",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = secondaryTextColor
)
Spacer(modifier = Modifier.height(4.dp))
Text(
fcmToken,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = textColor,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5))
.padding(8.dp)
.clickable {
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(fcmToken))
android.widget.Toast.makeText(context, "Token copied!", android.widget.Toast.LENGTH_SHORT).show()
}
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Status:",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = secondaryTextColor
)
Spacer(modifier = Modifier.height(4.dp))
Text(
if (fcmToken != "No token found") "✅ Token received" else "❌ Token not received",
fontSize = 14.sp,
color = if (fcmToken != "No token found") Color(0xFF4CAF50) else Color(0xFFF44336)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"📤 Token is sent to server after login (packet ID: 0x10)",
fontSize = 12.sp,
color = secondaryTextColor
)
}
},
confirmButton = {
Button(
onClick = { showFcmDialog = false },
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue
)
) {
Text("Close", color = Color.White)
}
},
containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White
)
}
// Simple background
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
@@ -649,6 +735,15 @@ fun ChatsListScreen(
actions = {
// Search only on main screen
if (!showRequestsScreen) {
// 🔔 FCM Debug button
IconButton(onClick = { showFcmDialog = true }) {
Icon(
Icons.Default.Info,
contentDescription = "FCM Token",
tint = textColor
)
}
IconButton(
onClick = {
if (protocolState == ProtocolState.AUTHENTICATED) {
@@ -700,9 +795,11 @@ fun ChatsListScreen(
) { paddingValues ->
// Main content
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
// 📬 Requests count from ViewModel
val requestsCount by chatsViewModel.requestsCount.collectAsState()
val requests by chatsViewModel.requests.collectAsState()
// <EFBFBD> Используем комбинированное состояние для атомарного обновления
// Это предотвращает "дергание" UI когда dialogs и requests обновляются независимо
val chatsState by chatsViewModel.chatsState.collectAsState()
val requests = chatsState.requests
val requestsCount = chatsState.requestsCount
// 🎬 Animated content transition between main list and requests
AnimatedContent(
@@ -744,8 +841,8 @@ fun ChatsListScreen(
onUserSelect(user)
}
)
} else if (dialogsList.isEmpty() && requestsCount == 0) {
// Empty state with Lottie animation
} else if (chatsState.isEmpty) {
// 🔥 Empty state - используем chatsState.isEmpty для атомарной проверки
EmptyChatsState(
isDarkTheme = isDarkTheme,
modifier = Modifier.fillMaxSize()
@@ -753,6 +850,8 @@ fun ChatsListScreen(
} else {
// Show dialogs list
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
// 🔥 Берем dialogs из chatsState для консистентности
val currentDialogs = chatsState.dialogs
LazyColumn(modifier = Modifier.fillMaxSize()) {
// 📬 Requests Section
@@ -770,7 +869,7 @@ fun ChatsListScreen(
}
}
items(dialogsList, key = { it.opponentKey }) { dialog ->
items(currentDialogs, key = { it.opponentKey }) { dialog ->
val isSavedMessages = dialog.opponentKey == accountPublicKey
// Check if user is blocked
var isBlocked by remember { mutableStateOf(false) }

View File

@@ -4,6 +4,7 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.data.MessageRepository
import com.rosetta.messenger.database.DialogEntity
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.PacketOnlineSubscribe
@@ -31,6 +32,19 @@ data class DialogUiModel(
val verified: Int
)
/**
* 🔥 Комбинированное состояние чатов для атомарного обновления UI
* Это предотвращает "дергание" когда dialogs и requests обновляются независимо
*/
data class ChatsUiState(
val dialogs: List<DialogUiModel> = emptyList(),
val requests: List<DialogUiModel> = emptyList(),
val requestsCount: Int = 0
) {
val isEmpty: Boolean get() = dialogs.isEmpty() && requestsCount == 0
val hasContent: Boolean get() = dialogs.isNotEmpty() || requestsCount > 0
}
/**
* ViewModel для списка чатов
* Загружает диалоги из базы данных и расшифровывает lastMessage
@@ -55,6 +69,19 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
private val _requestsCount = MutableStateFlow(0)
val requestsCount: StateFlow<Int> = _requestsCount.asStateFlow()
// 🔥 НОВОЕ: Комбинированное состояние - обновляется атомарно!
val chatsState: StateFlow<ChatsUiState> = combine(
_dialogs,
_requests,
_requestsCount
) { dialogs, requests, count ->
ChatsUiState(dialogs, requests, count)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
ChatsUiState()
)
// Загрузка
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
@@ -234,13 +261,24 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
/**
* Удалить диалог и все сообщения с собеседником
* 🔥 ПОЛНОСТЬЮ очищает все данные - диалог, сообщения, кэш
*/
suspend fun deleteDialog(opponentKey: String) {
if (currentAccount.isEmpty()) return
try {
android.util.Log.d("ChatsVM", "🗑️ ========== DELETE START ==========")
android.util.Log.d("ChatsVM", "🗑️ opponentKey=${opponentKey}")
android.util.Log.d("ChatsVM", "🗑️ currentAccount=${currentAccount}")
// 🚀 Сразу обновляем UI - удаляем диалог из локального списка
_dialogs.value = _dialogs.value.filter { it.opponentKey != opponentKey }
// 🔥 Также удаляем из requests!
_requests.value = _requests.value.filter { it.opponentKey != opponentKey }
// 🔥 Обновляем счетчик requests
_requestsCount.value = _requests.value.size
android.util.Log.d("ChatsVM", "🗑️ UI updated: dialogs=${_dialogs.value.size}, requests=${_requests.value.size}")
// Вычисляем правильный dialog_key (отсортированная комбинация ключей)
val dialogKey = if (currentAccount < opponentKey) {
@@ -248,26 +286,50 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
} else {
"$opponentKey:$currentAccount"
}
android.util.Log.d("ChatsVM", "🗑️ dialogKey=$dialogKey")
// 🗑️ 1. Очищаем ВСЕ кэши сообщений
MessageRepository.getInstance(getApplication()).clearDialogCache(opponentKey)
// 🗑️ Очищаем кэш ChatViewModel
ChatViewModel.clearCacheForOpponent(opponentKey)
// Удаляем все сообщения из диалога по dialog_key
database.messageDao().deleteDialog(
// 🗑️ 2. Проверяем сколько сообщений в БД до удаления
val messageCountBefore = database.messageDao().getMessageCount(currentAccount, dialogKey)
android.util.Log.d("ChatsVM", "🗑️ Messages BEFORE delete: $messageCountBefore")
// 🗑️ 3. Удаляем все сообщения из диалога по dialog_key
val deletedByDialogKey = database.messageDao().deleteDialog(
account = currentAccount,
dialogKey = dialogKey
)
// Также удаляем по from/to ключам (на всякий случай)
database.messageDao().deleteMessagesBetweenUsers(
android.util.Log.d("ChatsVM", "🗑️ Deleted by dialogKey: $deletedByDialogKey")
// 🗑️ 4. Также удаляем по from/to ключам (на всякий случай - старые сообщения)
val deletedBetweenUsers = database.messageDao().deleteMessagesBetweenUsers(
account = currentAccount,
user1 = opponentKey,
user2 = currentAccount
)
// Очищаем кеш диалога
android.util.Log.d("ChatsVM", "🗑️ Deleted between users: $deletedBetweenUsers")
// 🗑️ 5. Проверяем сколько сообщений осталось
val messageCountAfter = database.messageDao().getMessageCount(currentAccount, dialogKey)
android.util.Log.d("ChatsVM", "🗑️ Messages AFTER delete: $messageCountAfter")
// 🗑️ 6. Удаляем диалог из таблицы dialogs
database.dialogDao().deleteDialog(
account = currentAccount,
opponentKey = opponentKey
)
// 🗑️ 7. Проверяем что диалог удален
val dialogAfter = database.dialogDao().getDialog(currentAccount, opponentKey)
android.util.Log.d("ChatsVM", "🗑️ Dialog after delete: ${dialogAfter?.opponentKey ?: "NULL (deleted)"}")
android.util.Log.d("ChatsVM", "🗑️ ========== DELETE COMPLETE ==========")
} catch (e: Exception) {
android.util.Log.e("ChatsVM", "🗑️ DELETE ERROR: ${e.message}", e)
// В случае ошибки - возвращаем диалог обратно (откатываем оптимистичное обновление)
// Flow обновится автоматически из БД
}