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

This commit is contained in:
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, hideKeyboard: () -> Unit,
showEmoji: () -> 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 currentState = TransitionState.KEYBOARD_TO_EMOJI
isTransitioning = true isTransitioning = true
// 🔥 Гарантируем что emojiHeight = maxKeyboardHeight (не меняется при закрытии клавиатуры) // 🔥 Гарантируем что emojiHeight = maxKeyboardHeight (не меняется при закрытии клавиатуры)
if (maxKeyboardHeight > 0.dp) { if (maxKeyboardHeight > 0.dp) {
emojiHeight = maxKeyboardHeight emojiHeight = maxKeyboardHeight
Log.d(TAG, " 📌 Locked emojiHeight to maxKeyboardHeight: $emojiHeight")
} }
// 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры // 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры
Log.d(TAG, " 🚀 IMMEDIATELY showing emoji (no waiting for keyboard close)")
showEmoji() showEmoji()
isEmojiVisible = true
Log.d(TAG, " ✅ showEmoji() called, isEmojiVisible=true")
// Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji) // Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji)
Log.d(TAG, " ⌨️ Hiding keyboard...") Log.d(TAG, " ⌨️ Hiding keyboard...")
@@ -114,7 +102,6 @@ class KeyboardTransitionCoordinator {
hideKeyboard() hideKeyboard()
Log.d(TAG, " ✅ hideKeyboard() completed") Log.d(TAG, " ✅ hideKeyboard() completed")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ Error hiding keyboard", e)
} }
isKeyboardVisible = false isKeyboardVisible = false
@@ -123,8 +110,6 @@ class KeyboardTransitionCoordinator {
// Очищаем pending callback - больше не нужен // Очищаем pending callback - больше не нужен
pendingShowEmojiCallback = null pendingShowEmojiCallback = null
Log.d(TAG, "✅ requestShowEmoji() completed")
} }
// ============ Главный метод: Emoji → Keyboard ============ // ============ Главный метод: Emoji → Keyboard ============
@@ -137,33 +122,18 @@ class KeyboardTransitionCoordinator {
showKeyboard: () -> Unit, showKeyboard: () -> Unit,
hideEmoji: () -> 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 если он есть (предотвращаем конфликт) // 🔥 Отменяем pending emoji callback если он есть (предотвращаем конфликт)
if (pendingShowEmojiCallback != null) { if (pendingShowEmojiCallback != null) {
Log.d(TAG, "⚠️ Cancelling pending emoji callback (switching to keyboard)")
pendingShowEmojiCallback = null pendingShowEmojiCallback = null
} }
Log.d(TAG, " 📞 Setting currentState = EMOJI_TO_KEYBOARD")
currentState = TransitionState.EMOJI_TO_KEYBOARD currentState = TransitionState.EMOJI_TO_KEYBOARD
isTransitioning = true isTransitioning = true
// Шаг 1: Показать системную клавиатуру // Шаг 1: Показать системную клавиатуру
Log.d(TAG, " 📞 Calling showKeyboard()...")
try { try {
showKeyboard() showKeyboard()
Log.d(TAG, " ✅ showKeyboard() completed")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ Error showing keyboard", e)
} }
// Шаг 2: Через небольшую задержку скрыть emoji // Шаг 2: Через небольшую задержку скрыть emoji
@@ -177,10 +147,8 @@ class KeyboardTransitionCoordinator {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
currentState = TransitionState.IDLE currentState = TransitionState.IDLE
isTransitioning = false isTransitioning = false
Log.d(TAG, "✅ Keyboard visible, emoji hidden")
}, TRANSITION_DURATION) }, TRANSITION_DURATION)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ Error in requestShowKeyboard transition", e)
currentState = TransitionState.IDLE currentState = TransitionState.IDLE
isTransitioning = false isTransitioning = false
} }
@@ -193,8 +161,6 @@ class KeyboardTransitionCoordinator {
* Открыть только emoji панель (без клавиатуры). * Открыть только emoji панель (без клавиатуры).
*/ */
fun openEmojiOnly(showEmoji: () -> Unit) { fun openEmojiOnly(showEmoji: () -> Unit) {
Log.d(TAG, "😊 Opening emoji panel only")
currentState = TransitionState.EMOJI_OPENING currentState = TransitionState.EMOJI_OPENING
isTransitioning = true isTransitioning = true
@@ -209,7 +175,6 @@ class KeyboardTransitionCoordinator {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
currentState = TransitionState.IDLE currentState = TransitionState.IDLE
isTransitioning = false isTransitioning = false
Log.d(TAG, "✅ Emoji panel opened")
}, TRANSITION_DURATION) }, TRANSITION_DURATION)
} }
@@ -217,8 +182,6 @@ class KeyboardTransitionCoordinator {
* Закрыть emoji панель. * Закрыть emoji панель.
*/ */
fun closeEmoji(hideEmoji: () -> Unit) { fun closeEmoji(hideEmoji: () -> Unit) {
Log.d(TAG, "😊 Closing emoji panel")
currentState = TransitionState.EMOJI_CLOSING currentState = TransitionState.EMOJI_CLOSING
isTransitioning = true isTransitioning = true
@@ -228,7 +191,6 @@ class KeyboardTransitionCoordinator {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
currentState = TransitionState.IDLE currentState = TransitionState.IDLE
isTransitioning = false isTransitioning = false
Log.d(TAG, "✅ Emoji panel closed")
}, TRANSITION_DURATION) }, TRANSITION_DURATION)
} }
@@ -236,8 +198,6 @@ class KeyboardTransitionCoordinator {
* Закрыть системную клавиатуру. * Закрыть системную клавиатуру.
*/ */
fun closeKeyboard(hideKeyboard: () -> Unit) { fun closeKeyboard(hideKeyboard: () -> Unit) {
Log.d(TAG, "⌨️ Closing keyboard")
currentState = TransitionState.KEYBOARD_CLOSING currentState = TransitionState.KEYBOARD_CLOSING
isTransitioning = true isTransitioning = true
@@ -247,7 +207,6 @@ class KeyboardTransitionCoordinator {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
currentState = TransitionState.IDLE currentState = TransitionState.IDLE
isTransitioning = false isTransitioning = false
Log.d(TAG, "✅ Keyboard closed")
}, TRANSITION_DURATION) }, TRANSITION_DURATION)
} }
@@ -262,7 +221,6 @@ class KeyboardTransitionCoordinator {
// Логируем раз в 50ms ИЛИ при значительном изменении высоты (>5dp) // Логируем раз в 50ms ИЛИ при значительном изменении высоты (>5dp)
if (heightChanged && (now - lastLogTime > 50 || lastLoggedHeight < 0)) { if (heightChanged && (now - lastLogTime > 50 || lastLoggedHeight < 0)) {
Log.d(TAG, "⌨️ KB: ${height.value.toInt()}dp, emoji: ${emojiHeight.value.toInt()}dp, visible=$isKeyboardVisible")
lastLogTime = now lastLogTime = now
lastLoggedHeight = height.value lastLoggedHeight = height.value
} }
@@ -351,7 +309,6 @@ class KeyboardTransitionCoordinator {
* Сброс состояния (для отладки). * Сброс состояния (для отладки).
*/ */
fun reset() { fun reset() {
Log.d(TAG, "🔄 Reset coordinator state")
currentState = TransitionState.IDLE currentState = TransitionState.IDLE
isTransitioning = false isTransitioning = false
isKeyboardVisible = false isKeyboardVisible = false
@@ -363,14 +320,6 @@ class KeyboardTransitionCoordinator {
* Логирование текущего состояния. * Логирование текущего состояния.
*/ */
fun logState() { 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( val notificationPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(), contract = ActivityResultContracts.RequestPermission(),
onResult = { isGranted -> 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 ) == PackageManager.PERMISSION_GRANTED
if (!hasPermission) { if (!hasPermission) {
Log.d(TAG, "📬 Requesting notification permission...")
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
Log.d(TAG, "✅ Notification permission already granted")
} }
} }
} }
@@ -289,13 +282,10 @@ class MainActivity : ComponentActivity() {
// Получаем FCM токен // Получаем FCM токен
FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (!task.isSuccessful) { if (!task.isSuccessful) {
Log.e(TAG, "❌ Failed to get FCM token", task.exception)
return@addOnCompleteListener return@addOnCompleteListener
} }
val token = task.result val token = task.result
Log.d(TAG, "🔔 FCM token (short): ${token?.take(20)}...")
Log.d(TAG, "🔔 FCM token (FULL): $token")
// Сохраняем токен локально // Сохраняем токен локально
token?.let { saveFcmToken(it) } token?.let { saveFcmToken(it) }
@@ -304,9 +294,7 @@ class MainActivity : ComponentActivity() {
// (см. вызов sendFcmTokenToServer в onAccountLogin) // (см. вызов sendFcmTokenToServer в onAccountLogin)
} }
Log.d(TAG, "✅ Firebase initialized successfully")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ Error initializing Firebase", e)
} }
} }
@@ -316,7 +304,6 @@ class MainActivity : ComponentActivity() {
private fun saveFcmToken(token: String) { private fun saveFcmToken(token: String) {
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE) val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
prefs.edit().putString("fcm_token", token).apply() 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) val token = prefs.getString("fcm_token", null)
if (token == null) { if (token == null) {
Log.d(TAG, "⚠️ Cannot send FCM token: Token not found")
return@launch return@launch
} }
// 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED // 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED
var waitAttempts = 0 var waitAttempts = 0
while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED && waitAttempts < 50) { while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED && waitAttempts < 50) {
Log.d(TAG, "⏳ Waiting for protocol to be authenticated... (attempt ${waitAttempts + 1}/50)")
delay(100) // Ждем 100ms delay(100) // Ждем 100ms
waitAttempts++ waitAttempts++
} }
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) { if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
Log.e(TAG, "❌ Cannot send FCM token: Protocol not authenticated after 5 seconds")
return@launch 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 { val packet = PacketPushNotification().apply {
this.notificationsToken = token this.notificationsToken = token
this.action = PushNotificationAction.SUBSCRIBE this.action = PushNotificationAction.SUBSCRIBE
} }
ProtocolManager.send(packet) ProtocolManager.send(packet)
Log.d(TAG, "✅ FCM token sent to server (packet ID: 0x10)")
} catch (e: Exception) { } 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(), attachments: List<MessageAttachment> = emptyList(),
replyToMessageId: String? = null replyToMessageId: String? = null
): Message { ): Message {
android.util.Log.d("MessageRepo", "📤 sendMessage START: to=${toPublicKey.take(16)}...")
val account = currentAccount ?: throw IllegalStateException("Not initialized") val account = currentAccount ?: throw IllegalStateException("Not initialized")
val privateKey = currentPrivateKey ?: 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 messageId = UUID.randomUUID().toString().replace("-", "").take(32)
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
@@ -220,7 +218,6 @@ class MessageRepository private constructor(private val context: Context) {
// 🔥 Отмечаем что я отправлял сообщения в этот диалог (перемещает из requests в chats) // 🔥 Отмечаем что я отправлял сообщения в этот диалог (перемещает из requests в chats)
val updatedRows = dialogDao.markIHaveSent(account, toPublicKey) 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 { val packet = PacketMessage().apply {
@@ -316,17 +313,14 @@ class MessageRepository private constructor(private val context: Context) {
// ✅ Проверяем существование перед вставкой (защита от дубликатов) // ✅ Проверяем существование перед вставкой (защита от дубликатов)
val stillExists = messageDao.messageExists(account, messageId) val stillExists = messageDao.messageExists(account, messageId)
android.util.Log.d("MessageRepo", "📥 INCOMING: messageId=${messageId.take(16)}..., stillExists=$stillExists")
if (!stillExists) { if (!stillExists) {
// Сохраняем в БД только если сообщения нет // Сохраняем в БД только если сообщения нет
messageDao.insertMessage(entity) messageDao.insertMessage(entity)
android.util.Log.d("MessageRepo", "📥 INSERTED message with read=0, fromMe=0")
} }
// Обновляем диалог ПОСЛЕ вставки сообщения // Обновляем диалог ПОСЛЕ вставки сообщения
updateDialog(packet.fromPublicKey, plainText, packet.timestamp, incrementUnread = true) updateDialog(packet.fromPublicKey, plainText, packet.timestamp, incrementUnread = true)
android.util.Log.d("MessageRepo", "📥 Dialog updated")
// 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа // 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа
requestUserInfo(packet.fromPublicKey) requestUserInfo(packet.fromPublicKey)
@@ -479,25 +473,20 @@ class MessageRepository private constructor(private val context: Context) {
val account = currentAccount ?: return val account = currentAccount ?: return
val privateKey = currentPrivateKey ?: return val privateKey = currentPrivateKey ?: return
android.util.Log.d("MessageRepo", "📊 updateDialog: opponent=${opponentKey.take(16)}..., incrementUnread=$incrementUnread")
try { try {
// 🔥 КРИТИЧНО: Сначала считаем реальное количество непрочитанных из messages // 🔥 КРИТИЧНО: Сначала считаем реальное количество непрочитанных из messages
val unreadCount = messageDao.getUnreadCountForDialog(account, opponentKey) val unreadCount = messageDao.getUnreadCountForDialog(account, opponentKey)
android.util.Log.d("MessageRepo", "📊 unreadCount from DB: $unreadCount")
// 🔒 Шифруем lastMessage // 🔒 Шифруем lastMessage
val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey) val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey)
// Проверяем существует ли диалог // Проверяем существует ли диалог
val existing = dialogDao.getDialog(account, opponentKey) val existing = dialogDao.getDialog(account, opponentKey)
android.util.Log.d("MessageRepo", "📊 existing dialog: ${existing != null}, currentUnread=${existing?.unreadCount}")
if (existing != null) { if (existing != null) {
// Обновляем существующий диалог // Обновляем существующий диалог
dialogDao.updateLastMessage(account, opponentKey, encryptedLastMessage, timestamp) dialogDao.updateLastMessage(account, opponentKey, encryptedLastMessage, timestamp)
dialogDao.updateUnreadCount(account, opponentKey, unreadCount) dialogDao.updateUnreadCount(account, opponentKey, unreadCount)
android.util.Log.d("MessageRepo", "📊 UPDATED dialog unread to: $unreadCount")
} else { } else {
// Создаем новый диалог // Создаем новый диалог
dialogDao.insertDialog(DialogEntity( dialogDao.insertDialog(DialogEntity(
@@ -507,11 +496,9 @@ class MessageRepository private constructor(private val context: Context) {
lastMessageTimestamp = timestamp, lastMessageTimestamp = timestamp,
unreadCount = unreadCount unreadCount = unreadCount
)) ))
android.util.Log.d("MessageRepo", "📊 CREATED new dialog with unread: $unreadCount")
} }
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("MessageRepo", "📊 ERROR in updateDialog: ${e.message}")
e.printStackTrace() 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) { suspend fun updateDialogUserInfo(publicKey: String, title: String, username: String, verified: Int) {
val account = currentAccount ?: return val account = currentAccount ?: return
android.util.Log.d("MessageRepo", "📋 updateDialogUserInfo: publicKey=${publicKey.take(16)}..., title=$title, username=$username")
// Проверяем существует ли диалог с этим пользователем // Проверяем существует ли диалог с этим пользователем
val existing = dialogDao.getDialog(account, publicKey) val existing = dialogDao.getDialog(account, publicKey)
if (existing != null) { if (existing != null) {
android.util.Log.d("MessageRepo", "📋 Updating existing dialog info for ${publicKey.take(16)}...")
dialogDao.updateOpponentInfo(account, publicKey, title, username, verified) 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) { fun clearDialogCache(opponentKey: String) {
val dialogKey = getDialogKey(opponentKey) val dialogKey = getDialogKey(opponentKey)
android.util.Log.d("MessageRepo", "🗑️ clearDialogCache: dialogKey=$dialogKey")
// Сначала устанавливаем пустой список чтобы все подписчики увидели // Сначала устанавливаем пустой список чтобы все подписчики увидели
messageCache[dialogKey]?.value = emptyList() messageCache[dialogKey]?.value = emptyList()
// Затем удаляем из кэша // Затем удаляем из кэша
messageCache.remove(dialogKey) 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 val currentState = _state.value
log("🔌 CONNECT CALLED: currentState=$currentState, reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting") 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, а не только состояние // КРИТИЧНО: проверяем флаг isConnecting, а не только состояние
if (isConnecting || currentState == ProtocolState.CONNECTING) { if (isConnecting || currentState == ProtocolState.CONNECTING) {
log("⚠️ Already connecting, skipping... (preventing duplicate connect)") log("⚠️ Already connecting, skipping... (preventing duplicate connect)")

View File

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

View File

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

View File

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

View File

@@ -202,6 +202,7 @@ fun ChatsListScreen(
// Status dialog state // Status dialog state
var showStatusDialog by remember { mutableStateOf(false) } var showStatusDialog by remember { mutableStateOf(false) }
val debugLogs by ProtocolManager.debugLogs.collectAsState()
// <20> FCM токен диалог // <20> FCM токен диалог
var showFcmDialog by remember { mutableStateOf(false) } var showFcmDialog by remember { mutableStateOf(false) }
@@ -239,14 +240,33 @@ fun ChatsListScreen(
} }
*/ */
// Status dialog // Status dialog with logs
if (showStatusDialog) { if (showStatusDialog) {
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
val scrollState = rememberScrollState()
AlertDialog( AlertDialog(
onDismissRequest = { showStatusDialog = false }, onDismissRequest = { showStatusDialog = false },
title = { Text("Connection Status", fontWeight = FontWeight.Bold) }, title = {
Text(
"Connection Status & Logs",
fontWeight = FontWeight.Bold,
color = textColor
)
},
text = { text = {
Column { Column(
Row(verticalAlignment = Alignment.CenterVertically) { modifier = Modifier
.fillMaxWidth()
.heightIn(max = 500.dp)
) {
// Status indicator
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(12.dp) .size(12.dp)
@@ -268,14 +288,118 @@ fun ChatsListScreen(
ProtocolState.HANDSHAKING -> "Authenticating..." ProtocolState.HANDSHAKING -> "Authenticating..."
ProtocolState.AUTHENTICATED -> "Authenticated" 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 = { confirmButton = {
Button(onClick = { showStatusDialog = false }) { Button(
Text("OK") onClick = { showStatusDialog = false },
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue
)
) {
Text("Close", color = Color.White)
} }
}, },
containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White

View File

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