feat: Add ChatDetailScreen and implement chat navigation with animated transitions
This commit is contained in:
@@ -26,6 +26,8 @@ import com.rosetta.messenger.data.PreferencesManager
|
|||||||
import com.rosetta.messenger.ui.auth.AccountInfo
|
import com.rosetta.messenger.ui.auth.AccountInfo
|
||||||
import com.rosetta.messenger.ui.auth.AuthFlow
|
import com.rosetta.messenger.ui.auth.AuthFlow
|
||||||
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
||||||
|
import com.rosetta.messenger.ui.chats.ChatDetailScreen
|
||||||
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
||||||
import com.rosetta.messenger.ui.splash.SplashScreen
|
import com.rosetta.messenger.ui.splash.SplashScreen
|
||||||
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
||||||
@@ -214,6 +216,45 @@ fun MainScreen(
|
|||||||
val accountPublicKey = account?.publicKey ?: "04c266b98ae5"
|
val accountPublicKey = account?.publicKey ?: "04c266b98ae5"
|
||||||
val privateKeyHash = account?.privateKeyHash ?: ""
|
val privateKeyHash = account?.privateKeyHash ?: ""
|
||||||
|
|
||||||
|
// Навигация между экранами
|
||||||
|
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
|
||||||
|
|
||||||
|
// Анимированный переход между чатами
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = selectedUser,
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState != null) {
|
||||||
|
// Открытие чата - слайд слева
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { it },
|
||||||
|
animationSpec = tween(300)
|
||||||
|
) togetherWith slideOutHorizontally(
|
||||||
|
targetOffsetX = { -it / 3 },
|
||||||
|
animationSpec = tween(300)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Закрытие чата - слайд справа
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { -it / 3 },
|
||||||
|
animationSpec = tween(300)
|
||||||
|
) togetherWith slideOutHorizontally(
|
||||||
|
targetOffsetX = { it },
|
||||||
|
animationSpec = tween(300)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = "chatNavigation"
|
||||||
|
) { user ->
|
||||||
|
if (user != null) {
|
||||||
|
// Экран чата
|
||||||
|
ChatDetailScreen(
|
||||||
|
user = user,
|
||||||
|
currentUserPublicKey = accountPublicKey,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
onBack = { selectedUser = null }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Список чатов
|
||||||
ChatsListScreen(
|
ChatsListScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
accountName = accountName,
|
accountName = accountName,
|
||||||
@@ -249,9 +290,10 @@ fun MainScreen(
|
|||||||
// TODO: Show new chat screen
|
// TODO: Show new chat screen
|
||||||
},
|
},
|
||||||
onUserSelect = { user ->
|
onUserSelect = { user ->
|
||||||
// TODO: Navigate to chat with selected user
|
selectedUser = user
|
||||||
android.util.Log.d("MainScreen", "User selected: ${user.publicKey}")
|
|
||||||
},
|
},
|
||||||
onLogout = onLogout
|
onLogout = onLogout
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,571 @@
|
|||||||
|
package com.rosetta.messenger.ui.chats
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
|
import com.rosetta.messenger.network.SearchUser
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Модель сообщения
|
||||||
|
*/
|
||||||
|
data class ChatMessage(
|
||||||
|
val id: String,
|
||||||
|
val text: String,
|
||||||
|
val isOutgoing: Boolean,
|
||||||
|
val timestamp: Date,
|
||||||
|
val status: MessageStatus = MessageStatus.SENT
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class MessageStatus {
|
||||||
|
SENDING, SENT, DELIVERED, READ
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Экран детального чата с пользователем
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ChatDetailScreen(
|
||||||
|
user: SearchUser,
|
||||||
|
currentUserPublicKey: String,
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onUserProfileClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
||||||
|
val surfaceColor = if (isDarkTheme) Color(0xFF212121) else Color(0xFFF5F5F5)
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
|
val inputBackgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF0F0F0)
|
||||||
|
|
||||||
|
// Определяем это Saved Messages или обычный чат
|
||||||
|
val isSavedMessages = user.publicKey == currentUserPublicKey
|
||||||
|
val chatTitle = if (isSavedMessages) "Saved Messages" else user.title.ifEmpty { user.publicKey.take(10) }
|
||||||
|
val chatSubtitle = if (isSavedMessages) "Notes" else if (user.online == 1) "online" else "last seen recently"
|
||||||
|
|
||||||
|
// Состояние сообщений
|
||||||
|
var messages by remember { mutableStateOf<List<ChatMessage>>(emptyList()) }
|
||||||
|
var inputText by remember { mutableStateOf("") }
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// Аватар
|
||||||
|
val avatarColors = getAvatarColor(
|
||||||
|
if (isSavedMessages) "SavedMessages" else user.title.ifEmpty { user.publicKey },
|
||||||
|
isDarkTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
// Анимация появления
|
||||||
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(Unit) { visible = true }
|
||||||
|
|
||||||
|
// Логируем открытие чата
|
||||||
|
LaunchedEffect(user.publicKey) {
|
||||||
|
ProtocolManager.addLog("💬 Chat opened with: ${user.title.ifEmpty { user.publicKey.take(10) }}")
|
||||||
|
ProtocolManager.addLog(" PublicKey: ${user.publicKey.take(20)}...")
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
// Кастомный TopAppBar для чата
|
||||||
|
Surface(
|
||||||
|
color = backgroundColor,
|
||||||
|
shadowElevation = 4.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.height(56.dp)
|
||||||
|
.padding(horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Кнопка назад
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Аватар
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(if (isSavedMessages) PrimaryBlue else avatarColors.backgroundColor)
|
||||||
|
.clickable { onUserProfileClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (isSavedMessages) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Bookmark,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = if (user.title.isNotEmpty()) getInitials(user.title) else user.publicKey.take(2).uppercase(),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = avatarColors.textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// Информация о пользователе
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clickable { onUserProfileClick() }
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = chatTitle,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = textColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (!isSavedMessages && user.verified > 0) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
VerifiedBadge(verified = user.verified, size = 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = chatSubtitle,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = if (!isSavedMessages && user.online == 1) PrimaryBlue else secondaryTextColor,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопки действий
|
||||||
|
if (!isSavedMessages) {
|
||||||
|
IconButton(onClick = { /* TODO: Voice call */ }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Call,
|
||||||
|
contentDescription = "Call",
|
||||||
|
tint = textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = { /* TODO: More options */ }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.MoreVert,
|
||||||
|
contentDescription = "More",
|
||||||
|
tint = textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = backgroundColor
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.imePadding() // Весь контент поднимается с клавиатурой
|
||||||
|
) {
|
||||||
|
// Список сообщений
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (messages.isEmpty()) {
|
||||||
|
// Пустое состояние
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
if (isSavedMessages) Icons.Default.Bookmark else Icons.Default.Chat,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = secondaryTextColor.copy(alpha = 0.5f),
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = if (isSavedMessages)
|
||||||
|
"Save messages here for quick access"
|
||||||
|
else
|
||||||
|
"No messages yet",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = secondaryTextColor,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = if (isSavedMessages)
|
||||||
|
"Forward messages here or send notes to yourself"
|
||||||
|
else
|
||||||
|
"Send a message to start the conversation",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = secondaryTextColor.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
|
||||||
|
reverseLayout = true
|
||||||
|
) {
|
||||||
|
items(messages.reversed(), key = { it.id }) { message ->
|
||||||
|
MessageBubble(
|
||||||
|
message = message,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поле ввода сообщения
|
||||||
|
MessageInputBar(
|
||||||
|
value = inputText,
|
||||||
|
onValueChange = { inputText = it },
|
||||||
|
onSend = {
|
||||||
|
if (inputText.isNotBlank()) {
|
||||||
|
val newMessage = ChatMessage(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
text = inputText.trim(),
|
||||||
|
isOutgoing = true,
|
||||||
|
timestamp = Date(),
|
||||||
|
status = MessageStatus.SENDING
|
||||||
|
)
|
||||||
|
messages = messages + newMessage
|
||||||
|
|
||||||
|
// Логируем отправку
|
||||||
|
ProtocolManager.addLog("📤 Message sent: \"${inputText.take(30)}...\"")
|
||||||
|
|
||||||
|
inputText = ""
|
||||||
|
|
||||||
|
// Прокрутка вниз
|
||||||
|
scope.launch {
|
||||||
|
listState.animateScrollToItem(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Отправить через протокол
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
backgroundColor = inputBackgroundColor,
|
||||||
|
textColor = textColor,
|
||||||
|
placeholderColor = secondaryTextColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пузырек сообщения
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun MessageBubble(
|
||||||
|
message: ChatMessage,
|
||||||
|
isDarkTheme: Boolean
|
||||||
|
) {
|
||||||
|
val bubbleColor = if (message.isOutgoing) {
|
||||||
|
PrimaryBlue
|
||||||
|
} else {
|
||||||
|
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8)
|
||||||
|
}
|
||||||
|
val textColor = if (message.isOutgoing) Color.White else {
|
||||||
|
if (isDarkTheme) Color.White else Color.Black
|
||||||
|
}
|
||||||
|
val timeColor = if (message.isOutgoing) Color.White.copy(alpha = 0.7f) else {
|
||||||
|
if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
horizontalArrangement = if (message.isOutgoing) Arrangement.End else Arrangement.Start
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 280.dp)
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = 16.dp,
|
||||||
|
topEnd = 16.dp,
|
||||||
|
bottomStart = if (message.isOutgoing) 16.dp else 4.dp,
|
||||||
|
bottomEnd = if (message.isOutgoing) 4.dp else 16.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.background(bubbleColor)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = message.text,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = timeFormat.format(message.timestamp),
|
||||||
|
color = timeColor,
|
||||||
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
if (message.isOutgoing) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
when (message.status) {
|
||||||
|
MessageStatus.SENDING -> Icons.Default.Schedule
|
||||||
|
MessageStatus.SENT -> Icons.Default.Done
|
||||||
|
MessageStatus.DELIVERED -> Icons.Default.DoneAll
|
||||||
|
MessageStatus.READ -> Icons.Default.DoneAll
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (message.status == MessageStatus.READ)
|
||||||
|
Color(0xFF4CAF50)
|
||||||
|
else
|
||||||
|
timeColor,
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Панель ввода сообщения со стеклянным эффектом (Glass Morphism)
|
||||||
|
* Все иконки внутри одного стеклянного инпута
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun MessageInputBar(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
onSend: () -> Unit,
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
backgroundColor: Color,
|
||||||
|
textColor: Color,
|
||||||
|
placeholderColor: Color
|
||||||
|
) {
|
||||||
|
// Цвета для glass morphism эффекта
|
||||||
|
val glassBackground = if (isDarkTheme)
|
||||||
|
Color(0xFF1A1A1A).copy(alpha = 0.85f)
|
||||||
|
else
|
||||||
|
Color(0xFFFFFFFF).copy(alpha = 0.9f)
|
||||||
|
|
||||||
|
val inputGlass = if (isDarkTheme)
|
||||||
|
Color(0xFF2C2C2E).copy(alpha = 0.7f)
|
||||||
|
else
|
||||||
|
Color(0xFFF2F2F7).copy(alpha = 0.85f)
|
||||||
|
|
||||||
|
val inputBorder = if (isDarkTheme)
|
||||||
|
Color(0xFFFFFFFF).copy(alpha = 0.15f)
|
||||||
|
else
|
||||||
|
Color(0xFF000000).copy(alpha = 0.08f)
|
||||||
|
|
||||||
|
val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
|
|
||||||
|
// Основной контейнер
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(glassBackground)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
.padding(bottom = 4.dp)
|
||||||
|
.navigationBarsPadding(),
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
// Единый стеклянный контейнер для всего инпута
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.heightIn(min = 44.dp, max = 120.dp)
|
||||||
|
.clip(RoundedCornerShape(22.dp))
|
||||||
|
.background(inputGlass)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = inputBorder,
|
||||||
|
shape = RoundedCornerShape(22.dp)
|
||||||
|
)
|
||||||
|
// Блик сверху для стеклянного эффекта
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.White.copy(alpha = if (isDarkTheme) 0.06f else 0.4f),
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = if (isDarkTheme) 0.03f else 0.02f)
|
||||||
|
),
|
||||||
|
startY = 0f,
|
||||||
|
endY = 80f
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(22.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Кнопка смайликов (слева внутри инпута)
|
||||||
|
IconButton(
|
||||||
|
onClick = { /* TODO: Emoji picker */ },
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.EmojiEmotions,
|
||||||
|
contentDescription = "Emoji",
|
||||||
|
tint = iconColor,
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Текстовое поле
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(PrimaryBlue),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxLines = 5,
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Message",
|
||||||
|
color = placeholderColor.copy(alpha = 0.6f),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка прикрепления (справа внутри инпута)
|
||||||
|
IconButton(
|
||||||
|
onClick = { /* TODO: Attachment picker */ },
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.AttachFile,
|
||||||
|
contentDescription = "Attach",
|
||||||
|
tint = iconColor,
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка камеры (справа внутри инпута)
|
||||||
|
IconButton(
|
||||||
|
onClick = { /* TODO: Camera */ },
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CameraAlt,
|
||||||
|
contentDescription = "Camera",
|
||||||
|
tint = iconColor,
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// Кнопка отправки (появляется только когда есть текст)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = value.isNotBlank(),
|
||||||
|
enter = scaleIn(animationSpec = tween(150)) + fadeIn(animationSpec = tween(150)),
|
||||||
|
exit = scaleOut(animationSpec = tween(150)) + fadeOut(animationSpec = tween(150))
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onSend,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(44.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
PrimaryBlue,
|
||||||
|
PrimaryBlue.copy(alpha = 0.85f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Send,
|
||||||
|
contentDescription = "Send",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user