feat: Implement block/unblock user functionality with confirmation dialogs in ChatDetailScreen
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.RosettaAndroid"
|
android:theme="@style/Theme.RosettaAndroid"
|
||||||
android:windowSoftInputMode="adjustNothing">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,18 @@ interface MessageDao {
|
|||||||
@Query("DELETE FROM messages WHERE account = :account AND dialog_key = :dialogKey")
|
@Query("DELETE FROM messages WHERE account = :account AND dialog_key = :dialogKey")
|
||||||
suspend fun deleteDialog(account: String, dialogKey: String)
|
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)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Количество непрочитанных сообщений в диалоге
|
* Количество непрочитанных сообщений в диалоге
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -243,6 +243,13 @@ fun ChatDetailScreen(
|
|||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
var showBlockConfirm 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
|
// Подключаем к ViewModel
|
||||||
val messages by viewModel.messages.collectAsState()
|
val messages by viewModel.messages.collectAsState()
|
||||||
@@ -625,28 +632,32 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Block User (не показываем для Saved Messages)
|
// Block/Unblock User (не показываем для Saved Messages)
|
||||||
if (!isSavedMessages) {
|
if (!isSavedMessages) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Block,
|
if (isBlocked) Icons.Default.Check else Icons.Default.Block,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color(0xFFFF3B30),
|
tint = if (isBlocked) PrimaryBlue else Color(0xFFFF3B30),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
"Block User",
|
if (isBlocked) "Unblock User" else "Block User",
|
||||||
color = Color(0xFFFF3B30),
|
color = if (isBlocked) PrimaryBlue else Color(0xFFFF3B30),
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
showMenu = false
|
showMenu = false
|
||||||
showBlockConfirm = true
|
if (isBlocked) {
|
||||||
|
showUnblockConfirm = true
|
||||||
|
} else {
|
||||||
|
showBlockConfirm = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -675,6 +686,7 @@ fun ChatDetailScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
|
.imePadding() // ⌨️ Поднимаем контент при появлении клавиатуры
|
||||||
) {
|
) {
|
||||||
// Список сообщений - занимает весь экран
|
// Список сообщений - занимает весь экран
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
@@ -901,11 +913,13 @@ fun ChatDetailScreen(
|
|||||||
backgroundColor = inputBackgroundColor,
|
backgroundColor = inputBackgroundColor,
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
placeholderColor = secondaryTextColor,
|
placeholderColor = secondaryTextColor,
|
||||||
|
secondaryTextColor = secondaryTextColor,
|
||||||
// Reply state
|
// Reply state
|
||||||
replyMessages = replyMessages,
|
replyMessages = replyMessages,
|
||||||
isForwardMode = isForwardMode,
|
isForwardMode = isForwardMode,
|
||||||
onCloseReply = { viewModel.clearReplyMessages() },
|
onCloseReply = { viewModel.clearReplyMessages() },
|
||||||
chatTitle = chatTitle
|
chatTitle = chatTitle,
|
||||||
|
isBlocked = isBlocked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,18 +1121,21 @@ fun ChatDetailScreen(
|
|||||||
showDeleteConfirm = false
|
showDeleteConfirm = false
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
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,
|
account = currentUserPublicKey,
|
||||||
dialogKey = user.publicKey
|
user1 = user.publicKey,
|
||||||
|
user2 = currentUserPublicKey
|
||||||
)
|
)
|
||||||
// Delete dialog cache
|
// Очищаем кеш диалога
|
||||||
database.dialogDao().deleteDialog(
|
database.dialogDao().deleteDialog(
|
||||||
account = currentUserPublicKey,
|
account = currentUserPublicKey,
|
||||||
opponentKey = user.publicKey
|
opponentKey = user.publicKey
|
||||||
)
|
)
|
||||||
|
android.util.Log.d("ChatDetail", "✅ Chat deleted with: ${user.publicKey.take(10)}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("ChatDetail", "Error deleting chat", e)
|
android.util.Log.e("ChatDetail", "❌ Error deleting chat", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onBack()
|
onBack()
|
||||||
@@ -1159,19 +1176,20 @@ fun ChatDetailScreen(
|
|||||||
showBlockConfirm = false
|
showBlockConfirm = false
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
// Add user to blacklist
|
android.util.Log.d("ChatDetail", "🚫 Blocking user: ${user.publicKey.take(10)}")
|
||||||
|
// Добавляем пользователя в blacklist
|
||||||
database.blacklistDao().blockUser(
|
database.blacklistDao().blockUser(
|
||||||
com.rosetta.messenger.database.BlacklistEntity(
|
com.rosetta.messenger.database.BlacklistEntity(
|
||||||
publicKey = user.publicKey,
|
publicKey = user.publicKey,
|
||||||
account = currentUserPublicKey
|
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) {
|
} 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))
|
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 */
|
/** 🚀 Анимация появления сообщения Telegram-style */
|
||||||
@@ -1454,11 +1521,13 @@ private fun MessageInputBar(
|
|||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
textColor: Color,
|
textColor: Color,
|
||||||
placeholderColor: Color,
|
placeholderColor: Color,
|
||||||
|
secondaryTextColor: Color,
|
||||||
// Reply state (как в React Native)
|
// Reply state (как в React Native)
|
||||||
replyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
|
replyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
|
||||||
isForwardMode: Boolean = false,
|
isForwardMode: Boolean = false,
|
||||||
onCloseReply: () -> Unit = {},
|
onCloseReply: () -> Unit = {},
|
||||||
chatTitle: String = ""
|
chatTitle: String = "",
|
||||||
|
isBlocked: Boolean = false
|
||||||
) {
|
) {
|
||||||
val hasReply = replyMessages.isNotEmpty()
|
val hasReply = replyMessages.isNotEmpty()
|
||||||
var showEmojiPicker by remember { mutableStateOf(false) }
|
var showEmojiPicker by remember { mutableStateOf(false) }
|
||||||
@@ -1523,62 +1592,112 @@ private fun MessageInputBar(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
// 🔥 TELEGRAM-STYLE FLOATING LIQUID GLASS INPUT
|
// Если пользователь заблокирован - показываем BlockedChatFooter
|
||||||
// Используем Column вместо Row для reply panel внутри
|
if (isBlocked) {
|
||||||
Column(
|
// BLOCKED CHAT FOOTER
|
||||||
modifier =
|
Row(
|
||||||
Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp, vertical = 16.dp)
|
.fillMaxWidth()
|
||||||
// Инпут растёт вверх до 6 строк (~140dp) + reply
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
.heightIn(min = 44.dp, max = if (hasReply) 200.dp else 140.dp)
|
.background(
|
||||||
.shadow(
|
color = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F3F5),
|
||||||
elevation = 4.dp,
|
shape = RoundedCornerShape(12.dp)
|
||||||
shape = RoundedCornerShape(22.dp),
|
)
|
||||||
clip = false,
|
.border(
|
||||||
ambientColor = Color.Black.copy(alpha = 0.2f),
|
width = 1.dp,
|
||||||
spotColor = Color.Black.copy(alpha = 0.2f)
|
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))
|
} else {
|
||||||
.background(
|
listOf(
|
||||||
brush =
|
Color(0xFFF0F0F0).copy(alpha = 0.92f),
|
||||||
Brush.verticalGradient(
|
Color(0xFFE8E8E8).copy(alpha = 0.96f)
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.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)
|
// 🔥 REPLY PANEL внутри glass (как в React Native)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
@@ -1642,136 +1761,148 @@ private fun MessageInputBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input Row
|
// Input Row внутри Glass
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
.padding(start = 14.dp, end = 6.dp, top = 4.dp, bottom = 4.dp),
|
||||||
verticalAlignment = Alignment.Bottom
|
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
|
// TEXT INPUT
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier.weight(1f)
|
.weight(1f)
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically),
|
||||||
.padding(horizontal = 4.dp),
|
contentAlignment = Alignment.CenterStart
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
) {
|
||||||
AppleEmojiTextField(
|
AppleEmojiTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = { newValue -> onValueChange(newValue) },
|
onValueChange = { newValue -> onValueChange(newValue) },
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
textSize = 17f,
|
textSize = 16f,
|
||||||
hint = "Message",
|
hint = "Message",
|
||||||
hintColor =
|
hintColor = if (isDarkTheme) Color.White.copy(alpha = 0.35f)
|
||||||
if (isDarkTheme) Color.White.copy(alpha = 0.35f)
|
else Color.Black.copy(alpha = 0.35f),
|
||||||
else Color.Black.copy(alpha = 0.35f),
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ATTACH BUTTON
|
// Right Zone: Emoji + Send (как в React Native)
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier.align(Alignment.Bottom)
|
.align(Alignment.Bottom)
|
||||||
.size(36.dp)
|
.padding(bottom = 8.dp),
|
||||||
.clip(CircleShape)
|
contentAlignment = Alignment.BottomEnd
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null
|
|
||||||
) { /* TODO: Attach */},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Row(
|
||||||
Icons.Default.Attachment,
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
contentDescription = "Attach",
|
verticalAlignment = Alignment.Bottom
|
||||||
tint =
|
) {
|
||||||
if (isDarkTheme) Color.White.copy(alpha = 0.65f)
|
// EMOJI BUTTON
|
||||||
else Color.Black.copy(alpha = 0.55f),
|
Box(
|
||||||
modifier = Modifier.size(24.dp).graphicsLayer { rotationZ = -45f }
|
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))
|
// SEND BUTTON - только когда canSend
|
||||||
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
// MIC / SEND BUTTON
|
visible = canSend,
|
||||||
Box(
|
enter = fadeIn(tween(200)) + scaleIn(
|
||||||
modifier =
|
initialScale = 0.5f,
|
||||||
Modifier.align(Alignment.Bottom)
|
animationSpec = tween(220, easing = FastOutSlowInEasing)
|
||||||
.size(36.dp)
|
),
|
||||||
.clip(CircleShape)
|
exit = fadeOut(tween(200)) + scaleOut(
|
||||||
.then(
|
targetScale = 0.5f,
|
||||||
if (canSend) {
|
animationSpec = tween(220)
|
||||||
Modifier.background(PrimaryBlue)
|
)
|
||||||
} else {
|
) {
|
||||||
Modifier
|
Box(
|
||||||
}
|
modifier = Modifier
|
||||||
)
|
.width(52.dp)
|
||||||
|
.height(34.dp)
|
||||||
|
.clip(RoundedCornerShape(17.dp))
|
||||||
|
.background(PrimaryBlue)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = null,
|
indication = null,
|
||||||
onClick = {
|
onClick = { handleSend() }
|
||||||
if (canSend) handleSend()
|
|
||||||
else { /* TODO: Voice recording */ }
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
androidx.compose.animation.Crossfade(
|
Icon(
|
||||||
targetState = canSend,
|
imageVector = TelegramSendIcon,
|
||||||
animationSpec = tween(150),
|
contentDescription = "Send",
|
||||||
label = "iconCrossfade"
|
tint = Color.White,
|
||||||
) { showSend ->
|
modifier = Modifier.size(18.dp)
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} // End of Input Row
|
} // End of Input Row
|
||||||
} // End of Glass Column
|
} // 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(
|
AnimatedVisibility(
|
||||||
visible = showEmojiPicker,
|
visible = showEmojiPicker,
|
||||||
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
|
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
|
||||||
@@ -1783,6 +1914,7 @@ private fun MessageInputBar(
|
|||||||
onClose = { showEmojiPicker = false }
|
onClose = { showEmojiPicker = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} // End of if (!isBlocked) for emoji picker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user