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

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

View File

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

View File

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

View File

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