feat: Implement block/unblock user functionality with confirmation dialogs in ChatDetailScreen

This commit is contained in:
k1ngsterr1
2026-01-12 17:54:13 +05:00
parent fb339642fa
commit 9addd41571
3 changed files with 324 additions and 180 deletions

View File

@@ -21,7 +21,7 @@
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.RosettaAndroid"
android:windowSoftInputMode="adjustNothing">
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -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)
/**
* Количество непрочитанных сообщений в диалоге
*/

View File

@@ -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,29 +632,33 @@ 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
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<ChatViewModel.ReplyMessage> = emptyList(),
isForwardMode: Boolean = false,
onCloseReply: () -> Unit = {},
chatTitle: String = ""
chatTitle: String = "",
isBlocked: Boolean = false
) {
val hasReply = replyMessages.isNotEmpty()
var showEmojiPicker by remember { mutableStateOf(false) }
@@ -1523,61 +1592,111 @@ private fun MessageInputBar(
Column(
modifier = Modifier.fillMaxWidth()
) {
// 🔥 TELEGRAM-STYLE FLOATING LIQUID GLASS INPUT
// Используем Column вместо Row для reply panel внутри
// Если пользователь заблокирован - показываем 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.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 16.dp)
// Инпут растёт вверх до 6 строк (~140dp) + reply
.heightIn(min = 44.dp, max = if (hasReply) 200.dp else 140.dp)
modifier = Modifier
.weight(1f)
.heightIn(min = 48.dp, max = if (hasReply) 200.dp else 140.dp)
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(22.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(22.dp))
.clip(RoundedCornerShape(if (hasReply) 16.dp else 22.dp))
.background(
brush =
Brush.verticalGradient(
colors =
if (isDarkTheme) {
brush = Brush.verticalGradient(
colors = if (isDarkTheme) {
listOf(
Color(0xFF2D2D2F)
.copy(alpha = 0.92f),
Color(0xFF1C1C1E)
.copy(alpha = 0.96f)
Color(0xFF3C3C3C).copy(alpha = 0.9f),
Color(0xFF2C2C2C).copy(alpha = 0.95f)
)
} else {
listOf(
Color(0xFFF2F2F7)
.copy(alpha = 0.94f),
Color(0xFFE5E5EA)
.copy(alpha = 0.97f)
Color(0xFFF0F0F0).copy(alpha = 0.92f),
Color(0xFFE8E8E8).copy(alpha = 0.96f)
)
}
)
)
.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)
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)
@@ -1642,17 +1761,46 @@ 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
) {
// TEXT INPUT
Box(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
contentAlignment = Alignment.CenterStart
) {
AppleEmojiTextField(
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()
)
}
// Right Zone: Emoji + Send (как в React Native)
Box(
modifier = Modifier
.align(Alignment.Bottom)
.padding(bottom = 8.dp),
contentAlignment = Alignment.BottomEnd
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.Bottom
) {
// EMOJI BUTTON
Box(
modifier =
Modifier.align(Alignment.Bottom)
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.clickable(
@@ -1666,112 +1814,95 @@ private fun MessageInputBar(
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)
tint = if (isDarkTheme) Color.White.copy(alpha = 0.62f)
else Color.Black.copy(alpha = 0.5f),
modifier = Modifier.size(24.dp)
)
}
// TEXT INPUT
Box(
modifier =
Modifier.weight(1f)
.align(Alignment.CenterVertically)
.padding(horizontal = 4.dp),
contentAlignment = Alignment.CenterStart
// 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)
)
) {
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()
)
}
// ATTACH BUTTON
Box(
modifier =
Modifier.align(Alignment.Bottom)
.size(36.dp)
.clip(CircleShape)
.clickable(
interactionSource = interactionSource,
indication = null
) { /* TODO: Attach */},
contentAlignment = Alignment.Center
) {
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 }
)
}
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
}
)
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 */ }
}
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)
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
}
}