From 9addd41571b6f02c8ef0e7854439328dc0d3123e Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 12 Jan 2026 17:54:13 +0500 Subject: [PATCH] feat: Implement block/unblock user functionality with confirmation dialogs in ChatDetailScreen --- app/src/main/AndroidManifest.xml | 2 +- .../messenger/database/MessageEntities.kt | 12 + .../messenger/ui/chats/ChatDetailScreen.kt | 490 +++++++++++------- 3 files changed, 324 insertions(+), 180 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 37d390e..609ae69 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,7 +21,7 @@ android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.RosettaAndroid" - android:windowSoftInputMode="adjustNothing"> + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt index 0c3bb8d..cc1e129 100644 --- a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt @@ -195,6 +195,18 @@ interface MessageDao { @Query("DELETE FROM messages WHERE account = :account AND dialog_key = :dialogKey") suspend fun deleteDialog(account: String, dialogKey: String) + /** + * Удалить все сообщения между двумя пользователями + */ + @Query(""" + DELETE FROM messages + WHERE account = :account AND ( + (from_public_key = :user1 AND to_public_key = :user2) OR + (from_public_key = :user2 AND to_public_key = :user1) + ) + """) + suspend fun deleteMessagesBetweenUsers(account: String, user1: String, user2: String) + /** * Количество непрочитанных сообщений в диалоге */ 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 c5499b7..76f4f3f 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 @@ -243,6 +243,13 @@ fun ChatDetailScreen( var showMenu by remember { mutableStateOf(false) } var showDeleteConfirm by remember { mutableStateOf(false) } var showBlockConfirm by remember { mutableStateOf(false) } + var showUnblockConfirm by remember { mutableStateOf(false) } + + // Проверяем, заблокирован ли пользователь + var isBlocked by remember { mutableStateOf(false) } + LaunchedEffect(user.publicKey, currentUserPublicKey) { + isBlocked = database.blacklistDao().isUserBlocked(user.publicKey, currentUserPublicKey) + } // Подключаем к ViewModel val messages by viewModel.messages.collectAsState() @@ -625,28 +632,32 @@ fun ChatDetailScreen( } ) - // Block User (не показываем для Saved Messages) + // Block/Unblock User (не показываем для Saved Messages) if (!isSavedMessages) { DropdownMenuItem( text = { Row(verticalAlignment = Alignment.CenterVertically) { Icon( - Icons.Default.Block, + if (isBlocked) Icons.Default.Check else Icons.Default.Block, contentDescription = null, - tint = Color(0xFFFF3B30), + tint = if (isBlocked) PrimaryBlue else Color(0xFFFF3B30), modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.width(12.dp)) Text( - "Block User", - color = Color(0xFFFF3B30), + if (isBlocked) "Unblock User" else "Block User", + color = if (isBlocked) PrimaryBlue else Color(0xFFFF3B30), fontSize = 16.sp ) } }, onClick = { showMenu = false - showBlockConfirm = true + if (isBlocked) { + showUnblockConfirm = true + } else { + showBlockConfirm = true + } } ) } @@ -675,6 +686,7 @@ fun ChatDetailScreen( modifier = Modifier .fillMaxSize() .padding(paddingValues) + .imePadding() // ⌨️ Поднимаем контент при появлении клавиатуры ) { // Список сообщений - занимает весь экран Box(modifier = Modifier.fillMaxSize()) { @@ -901,11 +913,13 @@ fun ChatDetailScreen( backgroundColor = inputBackgroundColor, textColor = textColor, placeholderColor = secondaryTextColor, + secondaryTextColor = secondaryTextColor, // Reply state replyMessages = replyMessages, isForwardMode = isForwardMode, onCloseReply = { viewModel.clearReplyMessages() }, - chatTitle = chatTitle + chatTitle = chatTitle, + isBlocked = isBlocked ) } @@ -1107,18 +1121,21 @@ fun ChatDetailScreen( showDeleteConfirm = false scope.launch { try { - // Delete all messages in this dialog - database.messageDao().deleteDialog( + // Удаляем все сообщения из диалога + // DELETE FROM messages WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? + database.messageDao().deleteMessagesBetweenUsers( account = currentUserPublicKey, - dialogKey = user.publicKey + user1 = user.publicKey, + user2 = currentUserPublicKey ) - // Delete dialog cache + // Очищаем кеш диалога database.dialogDao().deleteDialog( account = currentUserPublicKey, opponentKey = user.publicKey ) + android.util.Log.d("ChatDetail", "✅ Chat deleted with: ${user.publicKey.take(10)}") } catch (e: Exception) { - android.util.Log.e("ChatDetail", "Error deleting chat", e) + android.util.Log.e("ChatDetail", "❌ Error deleting chat", e) } } onBack() @@ -1159,19 +1176,20 @@ fun ChatDetailScreen( showBlockConfirm = false scope.launch { try { - // Add user to blacklist + android.util.Log.d("ChatDetail", "🚫 Blocking user: ${user.publicKey.take(10)}") + // Добавляем пользователя в blacklist database.blacklistDao().blockUser( com.rosetta.messenger.database.BlacklistEntity( publicKey = user.publicKey, account = currentUserPublicKey ) ) - android.util.Log.d("ChatDetail", "User blocked: ${user.publicKey.take(10)}") + isBlocked = true + android.util.Log.d("ChatDetail", "✅ User blocked: ${user.publicKey.take(10)}") } catch (e: Exception) { - android.util.Log.e("ChatDetail", "Error blocking user", e) + android.util.Log.e("ChatDetail", "❌ Error blocking user", e) } } - onBack() } ) { Text("Block", color = Color(0xFFFF3B30)) @@ -1184,6 +1202,55 @@ fun ChatDetailScreen( } ) } + + // Диалог подтверждения разблокировки + if (showUnblockConfirm) { + AlertDialog( + onDismissRequest = { showUnblockConfirm = false }, + containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, + title = { + Text( + "Unblock ${user.title.ifEmpty { "User" }}", + fontWeight = FontWeight.Bold, + color = textColor + ) + }, + text = { + Text( + "Are you sure you want to unblock this user? They will be able to send you messages again.", + color = secondaryTextColor + ) + }, + confirmButton = { + TextButton( + onClick = { + showUnblockConfirm = false + scope.launch { + try { + android.util.Log.d("ChatDetail", "✅ Unblocking user: ${user.publicKey.take(10)}") + // Удаляем пользователя из blacklist + database.blacklistDao().unblockUser( + publicKey = user.publicKey, + account = currentUserPublicKey + ) + isBlocked = false + android.util.Log.d("ChatDetail", "✅ User unblocked: ${user.publicKey.take(10)}") + } catch (e: Exception) { + android.util.Log.e("ChatDetail", "❌ Error unblocking user", e) + } + } + } + ) { + Text("Unblock", color = PrimaryBlue) + } + }, + dismissButton = { + TextButton(onClick = { showUnblockConfirm = false }) { + Text("Cancel", color = Color(0xFF8E8E93)) + } + } + ) + } } /** 🚀 Анимация появления сообщения Telegram-style */ @@ -1454,11 +1521,13 @@ private fun MessageInputBar( backgroundColor: Color, textColor: Color, placeholderColor: Color, + secondaryTextColor: Color, // Reply state (как в React Native) replyMessages: List = emptyList(), isForwardMode: Boolean = false, onCloseReply: () -> Unit = {}, - chatTitle: String = "" + chatTitle: String = "", + isBlocked: Boolean = false ) { val hasReply = replyMessages.isNotEmpty() var showEmojiPicker by remember { mutableStateOf(false) } @@ -1523,62 +1592,112 @@ private fun MessageInputBar( Column( modifier = Modifier.fillMaxWidth() ) { - // 🔥 TELEGRAM-STYLE FLOATING LIQUID GLASS INPUT - // Используем Column вместо Row для reply panel внутри - Column( - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 16.dp) - // Инпут растёт вверх до 6 строк (~140dp) + reply - .heightIn(min = 44.dp, max = if (hasReply) 200.dp else 140.dp) - .shadow( - elevation = 4.dp, - shape = RoundedCornerShape(22.dp), - clip = false, - ambientColor = Color.Black.copy(alpha = 0.2f), - spotColor = Color.Black.copy(alpha = 0.2f) + // Если пользователь заблокирован - показываем BlockedChatFooter + if (isBlocked) { + // BLOCKED CHAT FOOTER + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp) + .background( + color = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F3F5), + shape = RoundedCornerShape(12.dp) + ) + .border( + width = 1.dp, + color = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE5E5EA), + shape = RoundedCornerShape(12.dp) + ) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + Icons.Default.Block, + contentDescription = null, + tint = Color(0xFFFF6B6B), + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "You need to unblock user to send messages.", + fontSize = 14.sp, + color = secondaryTextColor, + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + } + } else { + // 🔥 REACT NATIVE STYLE: Attach | Glass Input | Mic + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 16.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // ATTACH BUTTON - круглая кнопка слева + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background( + if (isDarkTheme) Color(0xFF3C3C3C).copy(alpha = 0.8f) + else Color(0xFFF0F0F0).copy(alpha = 0.85f) + ) + .border( + width = 1.dp, + color = if (isDarkTheme) Color.White.copy(alpha = 0.25f) + else Color.Black.copy(alpha = 0.1f), + shape = CircleShape + ) + .clickable( + interactionSource = interactionSource, + indication = null + ) { /* TODO: Attach */ }, + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Attachment, + contentDescription = "Attach", + tint = if (isDarkTheme) Color.White else Color(0xFF333333), + modifier = Modifier.size(22.dp).graphicsLayer { rotationZ = -45f } + ) + } + + // GLASS INPUT - расширяется, содержит Emoji + TextField + Send + Column( + modifier = Modifier + .weight(1f) + .heightIn(min = 48.dp, max = if (hasReply) 200.dp else 140.dp) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(if (hasReply) 16.dp else 22.dp), + clip = false, + ambientColor = Color.Black.copy(alpha = 0.2f), + spotColor = Color.Black.copy(alpha = 0.2f) + ) + .clip(RoundedCornerShape(if (hasReply) 16.dp else 22.dp)) + .background( + brush = Brush.verticalGradient( + colors = if (isDarkTheme) { + listOf( + Color(0xFF3C3C3C).copy(alpha = 0.9f), + Color(0xFF2C2C2C).copy(alpha = 0.95f) ) - .clip(RoundedCornerShape(22.dp)) - .background( - brush = - Brush.verticalGradient( - colors = - if (isDarkTheme) { - listOf( - Color(0xFF2D2D2F) - .copy(alpha = 0.92f), - Color(0xFF1C1C1E) - .copy(alpha = 0.96f) - ) - } else { - listOf( - Color(0xFFF2F2F7) - .copy(alpha = 0.94f), - Color(0xFFE5E5EA) - .copy(alpha = 0.97f) - ) - } - ) - ) - .border( - width = 1.dp, - brush = - Brush.verticalGradient( - colors = - if (isDarkTheme) { - listOf( - Color.White.copy(alpha = 0.18f), - Color.White.copy(alpha = 0.06f) - ) - } else { - listOf( - Color.White.copy(alpha = 0.9f), - Color.Black.copy(alpha = 0.05f) - ) - } - ), - shape = RoundedCornerShape(22.dp) + } else { + listOf( + Color(0xFFF0F0F0).copy(alpha = 0.92f), + Color(0xFFE8E8E8).copy(alpha = 0.96f) ) + } + ) + ) + .border( + width = 1.dp, + color = if (isDarkTheme) Color.White.copy(alpha = 0.25f) + else Color.Black.copy(alpha = 0.1f), + shape = RoundedCornerShape(if (hasReply) 16.dp else 22.dp) + ) ) { // 🔥 REPLY PANEL внутри glass (как в React Native) AnimatedVisibility( @@ -1642,136 +1761,148 @@ private fun MessageInputBar( } } - // Input Row + // Input Row внутри Glass Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 6.dp, vertical = 4.dp), + .padding(start = 14.dp, end = 6.dp, top = 4.dp, bottom = 4.dp), verticalAlignment = Alignment.Bottom ) { - // EMOJI BUTTON - Box( - modifier = - Modifier.align(Alignment.Bottom) - .size(36.dp) - .clip(CircleShape) - .clickable( - interactionSource = interactionSource, - indication = null, - onClick = { toggleEmojiPicker() } - ), - contentAlignment = Alignment.Center - ) { - Icon( - if (showEmojiPicker) Icons.Default.Keyboard - else Icons.Default.SentimentSatisfiedAlt, - contentDescription = "Emoji", - tint = - if (showEmojiPicker) PrimaryBlue - else { - if (isDarkTheme) Color.White.copy(alpha = 0.65f) - else Color.Black.copy(alpha = 0.55f) - }, - modifier = Modifier.size(26.dp) - ) - } - // TEXT INPUT Box( - modifier = - Modifier.weight(1f) - .align(Alignment.CenterVertically) - .padding(horizontal = 4.dp), - contentAlignment = Alignment.CenterStart + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically), + contentAlignment = Alignment.CenterStart ) { AppleEmojiTextField( - value = value, - onValueChange = { newValue -> onValueChange(newValue) }, - textColor = textColor, - textSize = 17f, - hint = "Message", - hintColor = - if (isDarkTheme) Color.White.copy(alpha = 0.35f) - else Color.Black.copy(alpha = 0.35f), - modifier = Modifier.fillMaxWidth() + value = value, + onValueChange = { newValue -> onValueChange(newValue) }, + textColor = textColor, + textSize = 16f, + hint = "Message", + hintColor = if (isDarkTheme) Color.White.copy(alpha = 0.35f) + else Color.Black.copy(alpha = 0.35f), + modifier = Modifier.fillMaxWidth() ) } - // ATTACH BUTTON + // Right Zone: Emoji + Send (как в React Native) Box( - modifier = - Modifier.align(Alignment.Bottom) - .size(36.dp) - .clip(CircleShape) - .clickable( - interactionSource = interactionSource, - indication = null - ) { /* TODO: Attach */}, - contentAlignment = Alignment.Center + modifier = Modifier + .align(Alignment.Bottom) + .padding(bottom = 8.dp), + contentAlignment = Alignment.BottomEnd ) { - Icon( - Icons.Default.Attachment, - contentDescription = "Attach", - tint = - if (isDarkTheme) Color.White.copy(alpha = 0.65f) - else Color.Black.copy(alpha = 0.55f), - modifier = Modifier.size(24.dp).graphicsLayer { rotationZ = -45f } - ) - } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.Bottom + ) { + // EMOJI BUTTON + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = { toggleEmojiPicker() } + ), + contentAlignment = Alignment.Center + ) { + Icon( + if (showEmojiPicker) Icons.Default.Keyboard + else Icons.Default.SentimentSatisfiedAlt, + contentDescription = "Emoji", + tint = if (isDarkTheme) Color.White.copy(alpha = 0.62f) + else Color.Black.copy(alpha = 0.5f), + modifier = Modifier.size(24.dp) + ) + } - Spacer(modifier = Modifier.width(2.dp)) - - // MIC / SEND BUTTON - Box( - modifier = - Modifier.align(Alignment.Bottom) - .size(36.dp) - .clip(CircleShape) - .then( - if (canSend) { - Modifier.background(PrimaryBlue) - } else { - Modifier - } - ) + // SEND BUTTON - только когда canSend + androidx.compose.animation.AnimatedVisibility( + visible = canSend, + enter = fadeIn(tween(200)) + scaleIn( + initialScale = 0.5f, + animationSpec = tween(220, easing = FastOutSlowInEasing) + ), + exit = fadeOut(tween(200)) + scaleOut( + targetScale = 0.5f, + animationSpec = tween(220) + ) + ) { + Box( + modifier = Modifier + .width(52.dp) + .height(34.dp) + .clip(RoundedCornerShape(17.dp)) + .background(PrimaryBlue) .clickable( - interactionSource = interactionSource, - indication = null, - onClick = { - if (canSend) handleSend() - else { /* TODO: Voice recording */ } - } + interactionSource = interactionSource, + indication = null, + onClick = { handleSend() } ), - contentAlignment = Alignment.Center - ) { - androidx.compose.animation.Crossfade( - targetState = canSend, - animationSpec = tween(150), - label = "iconCrossfade" - ) { showSend -> - if (showSend) { - Icon( - imageVector = TelegramSendIcon, - contentDescription = "Send", - tint = Color.White, - modifier = Modifier.size(20.dp) - ) - } else { - Icon( - Icons.Default.Mic, - contentDescription = "Voice", - tint = - if (isDarkTheme) Color.White.copy(alpha = 0.65f) - else Color.Black.copy(alpha = 0.55f), - modifier = Modifier.size(22.dp) - ) + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = TelegramSendIcon, + contentDescription = "Send", + tint = Color.White, + modifier = Modifier.size(18.dp) + ) + } + } } } - } } // End of Input Row } // End of Glass Column - // Apple Emoji Picker + // MIC BUTTON - справа снаружи (как в React Native) + androidx.compose.animation.AnimatedVisibility( + visible = !canSend, + enter = fadeIn(tween(200)) + slideInHorizontally( + initialOffsetX = { it / 2 }, + animationSpec = tween(250) + ), + exit = fadeOut(tween(200)) + slideOutHorizontally( + targetOffsetX = { it / 2 }, + animationSpec = tween(250) + ) + ) { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background( + if (isDarkTheme) Color(0xFF3C3C3C).copy(alpha = 0.8f) + else Color(0xFFF0F0F0).copy(alpha = 0.85f) + ) + .border( + width = 1.dp, + color = if (isDarkTheme) Color.White.copy(alpha = 0.25f) + else Color.Black.copy(alpha = 0.1f), + shape = CircleShape + ) + .clickable( + interactionSource = interactionSource, + indication = null + ) { /* TODO: Voice recording */ }, + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Mic, + contentDescription = "Voice", + tint = if (isDarkTheme) Color.White else Color(0xFF333333), + modifier = Modifier.size(22.dp) + ) + } + } + } // End of outer Row + } // End of else (not blocked) + + // Apple Emoji Picker - только показываем если не заблокирован + if (!isBlocked) { AnimatedVisibility( visible = showEmojiPicker, enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(), @@ -1783,6 +1914,7 @@ private fun MessageInputBar( onClose = { showEmojiPicker = false } ) } + } // End of if (!isBlocked) for emoji picker } }