feat: Enhance send and mic button animations in MessageInputBar for improved user experience
This commit is contained in:
@@ -26,6 +26,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -565,23 +566,6 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Кнопка логов (для отладки)
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
keyboardController?.hide()
|
|
||||||
focusManager.clearFocus()
|
|
||||||
showLogs = true
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.BugReport,
|
|
||||||
contentDescription = "Logs",
|
|
||||||
tint =
|
|
||||||
if (debugLogs.isNotEmpty()) PrimaryBlue
|
|
||||||
else textColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Кнопка меню с выпадающим списком
|
// Кнопка меню с выпадающим списком
|
||||||
Box {
|
Box {
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -1627,79 +1611,16 @@ private fun MessageInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 🔥 REACT NATIVE STYLE: Attach | Glass Input | Mic
|
// 🔥 TELEGRAM STYLE: простой фон, все кнопки внутри
|
||||||
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxWidth()
|
||||||
.heightIn(min = 48.dp, max = if (hasReply) 200.dp else 140.dp)
|
.padding(horizontal = 0.dp, vertical = 0.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(
|
.background(
|
||||||
brush = Brush.verticalGradient(
|
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
|
||||||
colors = if (isDarkTheme) {
|
|
||||||
listOf(
|
|
||||||
Color(0xFF3C3C3C).copy(alpha = 0.9f),
|
|
||||||
Color(0xFF2C2C2C).copy(alpha = 0.95f)
|
|
||||||
)
|
|
||||||
} 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)
|
// REPLY PANEL
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = hasReply,
|
visible = hasReply,
|
||||||
enter = fadeIn(tween(150)) + expandVertically(),
|
enter = fadeIn(tween(150)) + expandVertically(),
|
||||||
@@ -1708,19 +1629,17 @@ private fun MessageInputBar(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 12.dp, end = 6.dp, top = 10.dp, bottom = 4.dp),
|
.background(if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFFFFFFF))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Вертикальная синяя линия (как в React Native)
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(3.dp)
|
.width(3.dp)
|
||||||
.height(36.dp)
|
.height(32.dp)
|
||||||
.background(PrimaryBlue, RoundedCornerShape(1.5.dp))
|
.background(PrimaryBlue, RoundedCornerShape(1.5.dp))
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
|
||||||
// Контент reply
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = if (isForwardMode) "Forward message${if (replyMessages.size > 1) "s" else ""}"
|
text = if (isForwardMode) "Forward message${if (replyMessages.size > 1) "s" else ""}"
|
||||||
@@ -1744,8 +1663,6 @@ private fun MessageInputBar(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Кнопка X
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onCloseReply,
|
onClick = onCloseReply,
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
@@ -1761,18 +1678,38 @@ private fun MessageInputBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input Row внутри Glass
|
// INPUT ROW - как в Telegram
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 14.dp, end = 6.dp, top = 4.dp, bottom = 4.dp),
|
.heightIn(min = 48.dp)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
verticalAlignment = Alignment.Bottom
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
|
// EMOJI BUTTON (слева)
|
||||||
|
IconButton(
|
||||||
|
onClick = { toggleEmojiPicker() },
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
if (showEmojiPicker) Icons.Default.Keyboard
|
||||||
|
else Icons.Default.SentimentSatisfiedAlt,
|
||||||
|
contentDescription = "Emoji",
|
||||||
|
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
|
||||||
|
modifier = Modifier.size(26.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TEXT INPUT
|
// TEXT INPUT
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.align(Alignment.CenterVertically),
|
.heightIn(min = 36.dp)
|
||||||
|
.background(
|
||||||
|
color = if (isDarkTheme) Color(0xFF3A3A3C) else Color.White,
|
||||||
|
shape = RoundedCornerShape(18.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
AppleEmojiTextField(
|
AppleEmojiTextField(
|
||||||
@@ -1781,68 +1718,25 @@ private fun MessageInputBar(
|
|||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
textSize = 16f,
|
textSize = 16f,
|
||||||
hint = "Message",
|
hint = "Message",
|
||||||
hintColor = if (isDarkTheme) Color.White.copy(alpha = 0.35f)
|
hintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
|
||||||
else Color.Black.copy(alpha = 0.35f),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right Zone: Emoji + Send (как в React Native)
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
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
|
|
||||||
.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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SEND BUTTON - только когда canSend
|
// ATTACH / MIC / SEND BUTTON (справа)
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
if (canSend) {
|
||||||
visible = canSend,
|
// SEND BUTTON
|
||||||
enter = fadeIn(tween(200)) + scaleIn(
|
IconButton(
|
||||||
initialScale = 0.5f,
|
onClick = { handleSend() },
|
||||||
animationSpec = tween(220, easing = FastOutSlowInEasing)
|
modifier = Modifier.size(48.dp)
|
||||||
),
|
|
||||||
exit = fadeOut(tween(200)) + scaleOut(
|
|
||||||
targetScale = 0.5f,
|
|
||||||
animationSpec = tween(220)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(52.dp)
|
.size(34.dp)
|
||||||
.height(34.dp)
|
.clip(CircleShape)
|
||||||
.clip(RoundedCornerShape(17.dp))
|
.background(PrimaryBlue),
|
||||||
.background(PrimaryBlue)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null,
|
|
||||||
onClick = { handleSend() }
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -1853,52 +1747,22 @@ private fun MessageInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
// MIC BUTTON
|
||||||
} // End of Input Row
|
IconButton(
|
||||||
} // End of Glass Column
|
onClick = { /* TODO: Voice recording */ },
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
// 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(
|
Icon(
|
||||||
Icons.Default.Mic,
|
Icons.Default.Mic,
|
||||||
contentDescription = "Voice",
|
contentDescription = "Voice",
|
||||||
tint = if (isDarkTheme) Color.White else Color(0xFF333333),
|
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(26.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // End of outer Row
|
}
|
||||||
|
}
|
||||||
} // End of else (not blocked)
|
} // End of else (not blocked)
|
||||||
|
|
||||||
// Apple Emoji Picker - только показываем если не заблокирован
|
// Apple Emoji Picker - только показываем если не заблокирован
|
||||||
|
|||||||
@@ -614,11 +614,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val opponent = opponentKey ?: return
|
val opponent = opponentKey ?: return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
ProtocolManager.addLog("💾 Saving dialog: ${lastMessage.take(20)}...")
|
||||||
val existingDialog = dialogDao.getDialog(account, opponent)
|
val existingDialog = dialogDao.getDialog(account, opponent)
|
||||||
|
|
||||||
if (existingDialog != null) {
|
if (existingDialog != null) {
|
||||||
// Обновляем последнее сообщение
|
// Обновляем последнее сообщение
|
||||||
dialogDao.updateLastMessage(account, opponent, lastMessage, timestamp)
|
dialogDao.updateLastMessage(account, opponent, lastMessage, timestamp)
|
||||||
|
ProtocolManager.addLog("✅ Dialog updated (existing)")
|
||||||
} else {
|
} else {
|
||||||
// Создаём новый диалог
|
// Создаём новый диалог
|
||||||
dialogDao.insertDialog(DialogEntity(
|
dialogDao.insertDialog(DialogEntity(
|
||||||
@@ -629,8 +631,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
lastMessage = lastMessage,
|
lastMessage = lastMessage,
|
||||||
lastMessageTimestamp = timestamp
|
lastMessageTimestamp = timestamp
|
||||||
))
|
))
|
||||||
|
ProtocolManager.addLog("✅ Dialog created (new)")
|
||||||
}
|
}
|
||||||
ProtocolManager.addLog("💾 Dialog saved")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ProtocolManager.addLog("❌ Dialog save error: ${e.message}")
|
ProtocolManager.addLog("❌ Dialog save error: ${e.message}")
|
||||||
Log.e(TAG, "Dialog save error", e)
|
Log.e(TAG, "Dialog save error", e)
|
||||||
|
|||||||
@@ -186,11 +186,6 @@ fun ChatsListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dev console state
|
|
||||||
var showDevConsole by remember { mutableStateOf(false) }
|
|
||||||
var titleClickCount by remember { mutableStateOf(0) }
|
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
|
||||||
|
|
||||||
// Status dialog state
|
// Status dialog state
|
||||||
var showStatusDialog by remember { mutableStateOf(false) }
|
var showStatusDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -418,19 +413,6 @@ fun ChatsListScreen(
|
|||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.clickable {
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
if (currentTime - lastClickTime < 500) {
|
|
||||||
titleClickCount++
|
|
||||||
if (titleClickCount >= 3) {
|
|
||||||
showDevConsole = true
|
|
||||||
titleClickCount = 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
titleClickCount = 1
|
|
||||||
}
|
|
||||||
lastClickTime = currentTime
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -528,38 +510,7 @@ fun ChatsListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Console button - always visible at bottom left
|
// Console button removed
|
||||||
AnimatedVisibility(
|
|
||||||
visible = visible,
|
|
||||||
enter =
|
|
||||||
fadeIn(tween(500, delayMillis = 400)) +
|
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = { -it },
|
|
||||||
animationSpec = tween(500, delayMillis = 400)
|
|
||||||
),
|
|
||||||
modifier = Modifier.align(Alignment.BottomStart).padding(16.dp)
|
|
||||||
) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = { showDevConsole = true },
|
|
||||||
containerColor =
|
|
||||||
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5),
|
|
||||||
contentColor =
|
|
||||||
when (protocolState) {
|
|
||||||
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50)
|
|
||||||
ProtocolState.CONNECTING, ProtocolState.HANDSHAKING ->
|
|
||||||
Color(0xFFFFA726)
|
|
||||||
else -> Color(0xFFFF5722)
|
|
||||||
},
|
|
||||||
shape = CircleShape,
|
|
||||||
modifier = Modifier.size(48.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Terminal,
|
|
||||||
contentDescription = "Dev Console",
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // Close ModalNavigationDrawer
|
} // Close ModalNavigationDrawer
|
||||||
|
|||||||
Reference in New Issue
Block a user