From a9e426506bc2b19ab1b9773626ae247a79c66659 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sat, 17 Jan 2026 06:21:26 +0500 Subject: [PATCH] feat: Enhance connection handling and add debug logs feature; improve user experience and troubleshooting --- .../keyboard/KeyboardTransitionCoordinator.kt | 51 ------- .../com/rosetta/messenger/MainActivity.kt | 24 --- .../messenger/data/MessageRepository.kt | 20 --- .../com/rosetta/messenger/network/Protocol.kt | 12 ++ .../push/RosettaFirebaseMessagingService.kt | 9 -- .../messenger/ui/chats/ChatDetailScreen.kt | 6 +- .../messenger/ui/chats/ChatViewModel.kt | 13 -- .../messenger/ui/chats/ChatsListScreen.kt | 138 +++++++++++++++++- .../messenger/ui/chats/ChatsListViewModel.kt | 20 --- 9 files changed, 148 insertions(+), 145 deletions(-) diff --git a/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt b/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt index b1f910e..ef0595b 100644 --- a/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt +++ b/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt @@ -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()) } } diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 88d3ebf..0f972ca 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -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) } } } diff --git a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt index 7807195..bb8ba7d 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -151,10 +151,8 @@ class MessageRepository private constructor(private val context: Context) { attachments: List = 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") } /** diff --git a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt index 4b06a46..37a249f 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -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)") diff --git a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt index 1730a05..a2fe8ad 100644 --- a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt +++ b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt @@ -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") } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index cc3e2c0..e639dbd 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -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) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index ab3ab8d..9596148 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -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) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index 1fb2e39..a4320b1 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -202,6 +202,7 @@ fun ChatsListScreen( // Status dialog state var showStatusDialog by remember { mutableStateOf(false) } + val debugLogs by ProtocolManager.debugLogs.collectAsState() // � 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 diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt index 5f46ee9..f1a58f9 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt @@ -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}") } } }