feat: Enhance connection handling and add debug logs feature; improve user experience and troubleshooting

This commit is contained in:
k1ngsterr1
2026-01-17 06:21:26 +05:00
parent a64ee04b55
commit a9e426506b
9 changed files with 148 additions and 145 deletions

View File

@@ -85,28 +85,16 @@ class KeyboardTransitionCoordinator {
hideKeyboard: () -> Unit,
showEmoji: () -> Unit
) {
Log.d(TAG, "═══════════════════════════════════════════════════════")
Log.d(TAG, "📱 requestShowEmoji() START")
Log.d(TAG, "🔄 Keyboard → Emoji transition started")
Log.d(TAG, " 📊 Current state:")
Log.d(TAG, " - currentState=$currentState")
Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
Log.d(TAG, " - maxKeyboardHeight=$maxKeyboardHeight")
currentState = TransitionState.KEYBOARD_TO_EMOJI
isTransitioning = true
// 🔥 Гарантируем что emojiHeight = maxKeyboardHeight (не меняется при закрытии клавиатуры)
if (maxKeyboardHeight > 0.dp) {
emojiHeight = maxKeyboardHeight
Log.d(TAG, " 📌 Locked emojiHeight to maxKeyboardHeight: $emojiHeight")
}
// 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры
Log.d(TAG, " 🚀 IMMEDIATELY showing emoji (no waiting for keyboard close)")
showEmoji()
isEmojiVisible = true
Log.d(TAG, " ✅ showEmoji() called, isEmojiVisible=true")
// Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji)
Log.d(TAG, " ⌨️ Hiding keyboard...")
@@ -114,7 +102,6 @@ class KeyboardTransitionCoordinator {
hideKeyboard()
Log.d(TAG, " ✅ hideKeyboard() completed")
} catch (e: Exception) {
Log.e(TAG, "❌ Error hiding keyboard", e)
}
isKeyboardVisible = false
@@ -123,8 +110,6 @@ class KeyboardTransitionCoordinator {
// Очищаем pending callback - больше не нужен
pendingShowEmojiCallback = null
Log.d(TAG, "✅ requestShowEmoji() completed")
}
// ============ Главный метод: Emoji → Keyboard ============
@@ -137,33 +122,18 @@ class KeyboardTransitionCoordinator {
showKeyboard: () -> Unit,
hideEmoji: () -> Unit
) {
Log.d(TAG, "═══════════════════════════════════════════════════════")
Log.d(TAG, "⌨️ requestShowKeyboard() START")
Log.d(TAG, "🔄 Emoji → Keyboard transition started")
Log.d(TAG, " 📊 Current state:")
Log.d(TAG, " - currentState=$currentState")
Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
Log.d(TAG, " - isTransitioning=$isTransitioning")
Log.d(TAG, " - isKeyboardVisible=$isKeyboardVisible, isEmojiVisible=$isEmojiVisible")
Log.d(TAG, " - pendingShowEmojiCallback=${if (pendingShowEmojiCallback != null) "EXISTS" else "null"}")
// 🔥 Отменяем pending emoji callback если он есть (предотвращаем конфликт)
if (pendingShowEmojiCallback != null) {
Log.d(TAG, "⚠️ Cancelling pending emoji callback (switching to keyboard)")
pendingShowEmojiCallback = null
}
Log.d(TAG, " 📞 Setting currentState = EMOJI_TO_KEYBOARD")
currentState = TransitionState.EMOJI_TO_KEYBOARD
isTransitioning = true
// Шаг 1: Показать системную клавиатуру
Log.d(TAG, " 📞 Calling showKeyboard()...")
try {
showKeyboard()
Log.d(TAG, " ✅ showKeyboard() completed")
} catch (e: Exception) {
Log.e(TAG, "❌ Error showing keyboard", e)
}
// Шаг 2: Через небольшую задержку скрыть emoji
@@ -177,10 +147,8 @@ class KeyboardTransitionCoordinator {
Handler(Looper.getMainLooper()).postDelayed({
currentState = TransitionState.IDLE
isTransitioning = false
Log.d(TAG, "✅ Keyboard visible, emoji hidden")
}, TRANSITION_DURATION)
} catch (e: Exception) {
Log.e(TAG, "❌ Error in requestShowKeyboard transition", e)
currentState = TransitionState.IDLE
isTransitioning = false
}
@@ -193,8 +161,6 @@ class KeyboardTransitionCoordinator {
* Открыть только emoji панель (без клавиатуры).
*/
fun openEmojiOnly(showEmoji: () -> Unit) {
Log.d(TAG, "😊 Opening emoji panel only")
currentState = TransitionState.EMOJI_OPENING
isTransitioning = true
@@ -209,7 +175,6 @@ class KeyboardTransitionCoordinator {
Handler(Looper.getMainLooper()).postDelayed({
currentState = TransitionState.IDLE
isTransitioning = false
Log.d(TAG, "✅ Emoji panel opened")
}, TRANSITION_DURATION)
}
@@ -217,8 +182,6 @@ class KeyboardTransitionCoordinator {
* Закрыть emoji панель.
*/
fun closeEmoji(hideEmoji: () -> Unit) {
Log.d(TAG, "😊 Closing emoji panel")
currentState = TransitionState.EMOJI_CLOSING
isTransitioning = true
@@ -228,7 +191,6 @@ class KeyboardTransitionCoordinator {
Handler(Looper.getMainLooper()).postDelayed({
currentState = TransitionState.IDLE
isTransitioning = false
Log.d(TAG, "✅ Emoji panel closed")
}, TRANSITION_DURATION)
}
@@ -236,8 +198,6 @@ class KeyboardTransitionCoordinator {
* Закрыть системную клавиатуру.
*/
fun closeKeyboard(hideKeyboard: () -> Unit) {
Log.d(TAG, "⌨️ Closing keyboard")
currentState = TransitionState.KEYBOARD_CLOSING
isTransitioning = true
@@ -247,7 +207,6 @@ class KeyboardTransitionCoordinator {
Handler(Looper.getMainLooper()).postDelayed({
currentState = TransitionState.IDLE
isTransitioning = false
Log.d(TAG, "✅ Keyboard closed")
}, TRANSITION_DURATION)
}
@@ -262,7 +221,6 @@ class KeyboardTransitionCoordinator {
// Логируем раз в 50ms ИЛИ при значительном изменении высоты (>5dp)
if (heightChanged && (now - lastLogTime > 50 || lastLoggedHeight < 0)) {
Log.d(TAG, "⌨️ KB: ${height.value.toInt()}dp, emoji: ${emojiHeight.value.toInt()}dp, visible=$isKeyboardVisible")
lastLogTime = now
lastLoggedHeight = height.value
}
@@ -351,7 +309,6 @@ class KeyboardTransitionCoordinator {
* Сброс состояния (для отладки).
*/
fun reset() {
Log.d(TAG, "🔄 Reset coordinator state")
currentState = TransitionState.IDLE
isTransitioning = false
isKeyboardVisible = false
@@ -363,14 +320,6 @@ class KeyboardTransitionCoordinator {
* Логирование текущего состояния.
*/
fun logState() {
Log.d(TAG, """
📊 Coordinator State:
- state: $currentState
- transitioning: $isTransitioning
- keyboardVisible: $isKeyboardVisible (height=$keyboardHeight)
- emojiVisible: $isEmojiVisible (height=$emojiHeight)
- progress: $transitionProgress
""".trimIndent())
}
}

View File

@@ -91,10 +91,6 @@ class MainActivity : ComponentActivity() {
val notificationPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { isGranted ->
Log.d(TAG, "📬 Notification permission result: $isGranted")
if (!isGranted) {
Log.w(TAG, "⚠️ User denied notification permission")
}
}
)
@@ -107,10 +103,7 @@ class MainActivity : ComponentActivity() {
) == PackageManager.PERMISSION_GRANTED
if (!hasPermission) {
Log.d(TAG, "📬 Requesting notification permission...")
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
Log.d(TAG, "✅ Notification permission already granted")
}
}
}
@@ -289,13 +282,10 @@ class MainActivity : ComponentActivity() {
// Получаем FCM токен
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "❌ Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
Log.d(TAG, "🔔 FCM token (short): ${token?.take(20)}...")
Log.d(TAG, "🔔 FCM token (FULL): $token")
// Сохраняем токен локально
token?.let { saveFcmToken(it) }
@@ -304,9 +294,7 @@ class MainActivity : ComponentActivity() {
// (см. вызов sendFcmTokenToServer в onAccountLogin)
}
Log.d(TAG, "✅ Firebase initialized successfully")
} catch (e: Exception) {
Log.e(TAG, "❌ Error initializing Firebase", e)
}
}
@@ -316,7 +304,6 @@ class MainActivity : ComponentActivity() {
private fun saveFcmToken(token: String) {
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
prefs.edit().putString("fcm_token", token).apply()
Log.d(TAG, "💾 FCM token saved locally")
}
/**
@@ -330,38 +317,27 @@ class MainActivity : ComponentActivity() {
val token = prefs.getString("fcm_token", null)
if (token == null) {
Log.d(TAG, "⚠️ Cannot send FCM token: Token not found")
return@launch
}
// 🔥 КРИТИЧНО: Ждем пока протокол станет 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++
}
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 (packet ID: 0x10)")
} catch (e: Exception) {
Log.e(TAG, "❌ Error sending FCM token to server", e)
}
}
}

View File

@@ -151,10 +151,8 @@ class MessageRepository private constructor(private val context: Context) {
attachments: List<MessageAttachment> = emptyList(),
replyToMessageId: String? = null
): Message {
android.util.Log.d("MessageRepo", "📤 sendMessage START: to=${toPublicKey.take(16)}...")
val account = currentAccount ?: throw IllegalStateException("Not initialized")
val privateKey = currentPrivateKey ?: throw IllegalStateException("Not initialized")
android.util.Log.d("MessageRepo", "📤 sendMessage: account=${account.take(16)}...")
val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
val timestamp = System.currentTimeMillis()
@@ -220,7 +218,6 @@ class MessageRepository private constructor(private val context: Context) {
// 🔥 Отмечаем что я отправлял сообщения в этот диалог (перемещает из requests в chats)
val updatedRows = dialogDao.markIHaveSent(account, toPublicKey)
android.util.Log.d("MessageRepo", "📤 MARKED i_have_sent=1 for opponent=${toPublicKey.take(16)}..., updatedRows=$updatedRows")
// Отправляем пакет
val packet = PacketMessage().apply {
@@ -316,17 +313,14 @@ class MessageRepository private constructor(private val context: Context) {
// ✅ Проверяем существование перед вставкой (защита от дубликатов)
val stillExists = messageDao.messageExists(account, messageId)
android.util.Log.d("MessageRepo", "📥 INCOMING: messageId=${messageId.take(16)}..., stillExists=$stillExists")
if (!stillExists) {
// Сохраняем в БД только если сообщения нет
messageDao.insertMessage(entity)
android.util.Log.d("MessageRepo", "📥 INSERTED message with read=0, fromMe=0")
}
// Обновляем диалог ПОСЛЕ вставки сообщения
updateDialog(packet.fromPublicKey, plainText, packet.timestamp, incrementUnread = true)
android.util.Log.d("MessageRepo", "📥 Dialog updated")
// 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа
requestUserInfo(packet.fromPublicKey)
@@ -479,25 +473,20 @@ class MessageRepository private constructor(private val context: Context) {
val account = currentAccount ?: return
val privateKey = currentPrivateKey ?: return
android.util.Log.d("MessageRepo", "📊 updateDialog: opponent=${opponentKey.take(16)}..., incrementUnread=$incrementUnread")
try {
// 🔥 КРИТИЧНО: Сначала считаем реальное количество непрочитанных из messages
val unreadCount = messageDao.getUnreadCountForDialog(account, opponentKey)
android.util.Log.d("MessageRepo", "📊 unreadCount from DB: $unreadCount")
// 🔒 Шифруем lastMessage
val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey)
// Проверяем существует ли диалог
val existing = dialogDao.getDialog(account, opponentKey)
android.util.Log.d("MessageRepo", "📊 existing dialog: ${existing != null}, currentUnread=${existing?.unreadCount}")
if (existing != null) {
// Обновляем существующий диалог
dialogDao.updateLastMessage(account, opponentKey, encryptedLastMessage, timestamp)
dialogDao.updateUnreadCount(account, opponentKey, unreadCount)
android.util.Log.d("MessageRepo", "📊 UPDATED dialog unread to: $unreadCount")
} else {
// Создаем новый диалог
dialogDao.insertDialog(DialogEntity(
@@ -507,11 +496,9 @@ class MessageRepository private constructor(private val context: Context) {
lastMessageTimestamp = timestamp,
unreadCount = unreadCount
))
android.util.Log.d("MessageRepo", "📊 CREATED new dialog with unread: $unreadCount")
}
} catch (e: Exception) {
android.util.Log.e("MessageRepo", "📊 ERROR in updateDialog: ${e.message}")
e.printStackTrace()
}
}
@@ -539,15 +526,10 @@ class MessageRepository private constructor(private val context: Context) {
suspend fun updateDialogUserInfo(publicKey: String, title: String, username: String, verified: Int) {
val account = currentAccount ?: return
android.util.Log.d("MessageRepo", "📋 updateDialogUserInfo: publicKey=${publicKey.take(16)}..., title=$title, username=$username")
// Проверяем существует ли диалог с этим пользователем
val existing = dialogDao.getDialog(account, publicKey)
if (existing != null) {
android.util.Log.d("MessageRepo", "📋 Updating existing dialog info for ${publicKey.take(16)}...")
dialogDao.updateOpponentInfo(account, publicKey, title, username, verified)
} else {
android.util.Log.d("MessageRepo", "📋 Dialog not found for ${publicKey.take(16)}..., skipping")
}
}
@@ -558,14 +540,12 @@ class MessageRepository private constructor(private val context: Context) {
*/
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

@@ -194,6 +194,18 @@ class Protocol(
val currentState = _state.value
log("🔌 CONNECT CALLED: currentState=$currentState, reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting")
// КРИТИЧНО: Если уже подключены и аутентифицированы - не переподключаемся!
if (currentState == ProtocolState.AUTHENTICATED || currentState == ProtocolState.HANDSHAKING) {
log("✅ Already authenticated or handshaking, skipping connect()")
return
}
// КРИТИЧНО: Если уже CONNECTED и есть credentials - тоже пропускаем
if (currentState == ProtocolState.CONNECTED && webSocket != null) {
log("✅ Already connected with valid socket, skipping connect()")
return
}
// КРИТИЧНО: проверяем флаг isConnecting, а не только состояние
if (isConnecting || currentState == ProtocolState.CONNECTING) {
log("⚠️ Already connecting, skipping... (preventing duplicate connect)")

View File

@@ -45,14 +45,11 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
*/
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d(TAG, "🔔 New FCM token (short): ${token.take(20)}...")
Log.d(TAG, "🔔 New FCM token (FULL): $token")
// Сохраняем токен локально
saveFcmToken(token)
// 📤 Токен будет отправлен на сервер после успешного логина в MainActivity
Log.d(TAG, "💾 FCM token saved. Will be sent to server after login.")
}
/**
@@ -60,11 +57,9 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
*/
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
Log.d(TAG, "📬 Push notification received from: ${remoteMessage.from}")
// Обрабатываем data payload
remoteMessage.data.isNotEmpty().let {
Log.d(TAG, "📦 Message data payload: ${remoteMessage.data}")
val type = remoteMessage.data["type"]
val senderPublicKey = remoteMessage.data["sender_public_key"]
@@ -78,17 +73,14 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
}
"message_read" -> {
// Сообщение прочитано - можно обновить UI если приложение открыто
Log.d(TAG, "📖 Message read by $senderPublicKey")
}
else -> {
Log.d(TAG, "⚠️ Unknown notification type: $type")
}
}
}
// Обрабатываем notification payload (если есть)
remoteMessage.notification?.let {
Log.d(TAG, "📨 Message Notification Body: ${it.body}")
showSimpleNotification(it.title ?: "Rosetta", it.body ?: "New message")
}
}
@@ -181,6 +173,5 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
private fun saveFcmToken(token: String) {
val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("fcm_token", token).apply()
Log.d(TAG, "💾 FCM token saved locally")
}
}

View File

@@ -813,7 +813,11 @@ fun ChatDetailScreen(
// Закрываем клавиатуру перед открытием меню
keyboardController?.hide()
focusManager.clearFocus()
showMenu = true
// Даём клавиатуре время закрыться перед показом bottom sheet
scope.launch {
delay(150) // Задержка для плавного закрытия клавиатуры
showMenu = true
}
},
modifier = Modifier
.size(48.dp)

View File

@@ -43,7 +43,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* Вызывается при удалении диалога
*/
fun clearDialogCache(dialogKey: String) {
android.util.Log.d(TAG, "🗑️ Clearing ChatViewModel cache for: $dialogKey")
dialogMessagesCache.remove(dialogKey)
}
@@ -52,10 +51,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* Удаляет все ключи содержащие этот 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)
}
}
@@ -212,7 +209,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
dialogDao.updateDialogFromMessages(account, opponentKey ?: return@launch)
} catch (e: Exception) {
android.util.Log.e(TAG, "Error adding latest message", e)
}
}
}
@@ -342,7 +338,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
lastReadMessageTimestamp = 0L
readReceiptSentForCurrentDialog = false
isDialogActive = true // 🔥 Диалог активен!
android.util.Log.d("ChatViewModel", "✅ Dialog active flag set to TRUE in openDialog")
// 📨 Применяем Forward сообщения СРАЗУ после сброса
if (hasForward) {
@@ -378,9 +373,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* Как setCurrentDialogPublicKeyView("") в архиве
*/
fun closeDialog() {
android.util.Log.d("ChatViewModel", "🔒 CLOSE DIALOG")
isDialogActive = false
android.util.Log.d("ChatViewModel", "❌ Dialog active flag set to FALSE")
}
/**
@@ -474,10 +467,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
launch(Dispatchers.IO) {
// 👁️ Отмечаем как прочитанные ТОЛЬКО если диалог активен
if (isDialogActive) {
android.util.Log.d("ChatViewModel", "📖 Marking dialog as read (dialog is active)")
messageDao.markDialogAsRead(account, dialogKey)
} else {
android.util.Log.d("ChatViewModel", "⏸️ NOT marking as read (dialog not active)")
}
// 🔥 Пересчитываем счетчики из messages
dialogDao.updateDialogFromMessages(account, opponent)
@@ -545,10 +535,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 👁️ Фоновые операции - НЕ помечаем как прочитанные если диалог неактивен!
if (isDialogActive) {
android.util.Log.d("ChatViewModel", "📖 Marking dialog as read in refresh (dialog is active)")
messageDao.markDialogAsRead(account, dialogKey)
} else {
android.util.Log.d("ChatViewModel", "⏸️ NOT marking as read in refresh (dialog not active)")
}
// 🔥 Пересчитываем счетчики из messages
dialogDao.updateDialogFromMessages(account, opponent)

View File

@@ -202,6 +202,7 @@ fun ChatsListScreen(
// Status dialog state
var showStatusDialog by remember { mutableStateOf(false) }
val debugLogs by ProtocolManager.debugLogs.collectAsState()
// <20> FCM токен диалог
var showFcmDialog by remember { mutableStateOf(false) }
@@ -239,14 +240,33 @@ fun ChatsListScreen(
}
*/
// Status dialog
// Status dialog with logs
if (showStatusDialog) {
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
val scrollState = rememberScrollState()
AlertDialog(
onDismissRequest = { showStatusDialog = false },
title = { Text("Connection Status", fontWeight = FontWeight.Bold) },
title = {
Text(
"Connection Status & Logs",
fontWeight = FontWeight.Bold,
color = textColor
)
},
text = {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 500.dp)
) {
// Status indicator
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
) {
Box(
modifier = Modifier
.size(12.dp)
@@ -268,14 +288,118 @@ fun ChatsListScreen(
ProtocolState.HANDSHAKING -> "Authenticating..."
ProtocolState.AUTHENTICATED -> "Authenticated"
},
fontSize = 16.sp
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = textColor
)
}
Divider(
color = if (isDarkTheme) Color(0xFF424242) else Color(0xFFE0E0E0),
modifier = Modifier.padding(vertical = 8.dp)
)
// Logs header with copy button
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Debug Logs:",
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = secondaryTextColor
)
TextButton(
onClick = {
val logsText = debugLogs.joinToString("\n")
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(logsText))
android.widget.Toast.makeText(
context,
"Logs copied to clipboard!",
android.widget.Toast.LENGTH_SHORT
).show()
},
enabled = debugLogs.isNotEmpty()
) {
Text(
"Copy All",
fontSize = 12.sp,
color = if (debugLogs.isNotEmpty()) PrimaryBlue else Color.Gray
)
}
}
// Logs content
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f, fill = false)
.clip(RoundedCornerShape(8.dp))
.background(if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF5F5F5))
.padding(8.dp)
) {
if (debugLogs.isEmpty()) {
Text(
"No logs available.\nLogs are disabled by default for performance.\n\nEnable with:\nProtocolManager.enableUILogs(true)",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(8.dp)
)
} else {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollState)
) {
debugLogs.forEach { log ->
Text(
log,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = textColor,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp)
)
}
}
}
}
// Enable/Disable logs button
TextButton(
onClick = {
ProtocolManager.enableUILogs(!debugLogs.isNotEmpty())
android.widget.Toast.makeText(
context,
if (debugLogs.isEmpty()) "Logs enabled" else "Logs disabled",
android.widget.Toast.LENGTH_SHORT
).show()
},
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Text(
if (debugLogs.isEmpty()) "⚠️ Enable Logs" else "Disable Logs",
fontSize = 12.sp,
color = if (debugLogs.isEmpty()) Color(0xFFFFC107) else Color.Gray
)
}
}
},
confirmButton = {
Button(onClick = { showStatusDialog = false }) {
Text("OK")
Button(
onClick = { showStatusDialog = false },
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue
)
) {
Text("Close", color = Color.White)
}
},
containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White

View File

@@ -141,11 +141,9 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
// 📬 Подписываемся на requests (запросы от новых пользователей)
viewModelScope.launch {
android.util.Log.d("ChatsVM", "📬 Subscribing to requests for publicKey=${publicKey.take(16)}...")
dialogDao.getRequestsFlow(publicKey)
.flowOn(Dispatchers.IO)
.map { requestsList ->
android.util.Log.d("ChatsVM", "📬 Received ${requestsList.size} requests from DB")
requestsList.map { dialog ->
// 🔥 Загружаем информацию о пользователе если её нет
if (dialog.opponentTitle.isEmpty() || dialog.opponentTitle == dialog.opponentKey) {
@@ -267,10 +265,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
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!
@@ -278,15 +272,12 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
// 🔥 Обновляем счетчик 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) {
"$currentAccount:$opponentKey"
} else {
"$opponentKey:$currentAccount"
}
android.util.Log.d("ChatsVM", "🗑️ dialogKey=$dialogKey")
// 🗑️ 1. Очищаем ВСЕ кэши сообщений
MessageRepository.getInstance(getApplication()).clearDialogCache(opponentKey)
@@ -295,14 +286,12 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
// 🗑️ 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
)
android.util.Log.d("ChatsVM", "🗑️ Deleted by dialogKey: $deletedByDialogKey")
// 🗑️ 4. Также удаляем по from/to ключам (на всякий случай - старые сообщения)
val deletedBetweenUsers = database.messageDao().deleteMessagesBetweenUsers(
@@ -310,11 +299,9 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
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(
@@ -324,12 +311,8 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
// 🗑️ 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 обновится автоматически из БД
}
@@ -378,8 +361,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
// 🔥 ВАЖНО: Используем хеш ключа, как в MessageRepository.requestUserInfo
val privateKeyHash = CryptoManager.generatePrivateKeyHash(currentUserPrivateKey)
android.util.Log.d("ChatsVM", "📬 Requesting user info for: ${publicKey.take(16)}...")
// Запрашиваем информацию о пользователе с сервера
val packet = PacketSearch().apply {
this.privateKey = privateKeyHash
@@ -387,7 +368,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
}
ProtocolManager.send(packet)
} catch (e: Exception) {
android.util.Log.e("ChatsVM", "📬 Error loading user info: ${e.message}")
}
}
}