feat: Enhance connection handling and add debug logs feature; improve user experience and troubleshooting
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user