diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatGradients.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatGradients.kt index 496dbe0..8a8d5c1 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatGradients.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatGradients.kt @@ -1,5 +1,6 @@ package ru.fromchat.ui.chat +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import kotlin.math.abs @@ -7,90 +8,23 @@ import kotlin.math.abs /** * Get gradient brush for own messages */ -fun getMessageGradient(isDark: Boolean): Brush { - return if (isDark) { - Brush.linearGradient( - colors = listOf( - Color(0xFF9333EA), - Color(0xFF6366F1), - Color(0xFF2F68C5) - ), - start = androidx.compose.ui.geometry.Offset(0f, 0f), - end = androidx.compose.ui.geometry.Offset(1000f, 1000f) - ) - } else { - Brush.linearGradient( - colors = listOf( - Color(0xFFB794F6), - Color(0xFF818CF8), - Color(0xFF60A5FA) - ), - start = androidx.compose.ui.geometry.Offset(0f, 0f), - end = androidx.compose.ui.geometry.Offset(1000f, 1000f) - ) - } -} - -/** - * Get background gradient brushes for chat background - */ -fun getBackgroundGradients(isDark: Boolean): List { - return if (isDark) { +fun getMessageGradient(isDark: Boolean) = Brush.linearGradient( + colors = if (isDark) { listOf( - Brush.radialGradient( - colors = listOf( - Color(0x26DBA1F9), - Color.Transparent - ), - center = androidx.compose.ui.geometry.Offset(0.2f, 0.8f), - radius = 500f - ), - Brush.radialGradient( - colors = listOf( - Color(0x26F3B7BE), - Color.Transparent - ), - center = androidx.compose.ui.geometry.Offset(0.8f, 0.2f), - radius = 500f - ), - Brush.radialGradient( - colors = listOf( - Color(0x1AD0C1DA), - Color.Transparent - ), - center = androidx.compose.ui.geometry.Offset(0.4f, 0.4f), - radius = 500f - ) + Color(0xFF9333EA), + Color(0xFF6366F1), + Color(0xFF2F68C5) ) } else { listOf( - Brush.radialGradient( - colors = listOf( - Color(0x15B794F6), - Color.Transparent - ), - center = androidx.compose.ui.geometry.Offset(0.2f, 0.8f), - radius = 500f - ), - Brush.radialGradient( - colors = listOf( - Color(0x15F3B7BE), - Color.Transparent - ), - center = androidx.compose.ui.geometry.Offset(0.8f, 0.2f), - radius = 500f - ), - Brush.radialGradient( - colors = listOf( - Color(0x0DD0C1DA), - Color.Transparent - ), - center = androidx.compose.ui.geometry.Offset(0.4f, 0.4f), - radius = 500f - ) + Color(0xFFB794F6), + Color(0xFF818CF8), + Color(0xFF60A5FA) ) - } -} + }, + start = Offset(0f, 0f), + end = Offset(1000f, 1000f) +) /** * Generate a consistent gradient from a name for avatar fallback @@ -102,21 +36,21 @@ fun generateGradientFromName(name: String): Brush { val b = abs((hash / 65536) % 256) // Create two colors based on hash for gradient - val color1 = Color( - red = (r + 100).coerceIn(0, 255) / 255f, - green = (g + 100).coerceIn(0, 255) / 255f, - blue = (b + 100).coerceIn(0, 255) / 255f - ) - val color2 = Color( - red = (r + 50).coerceIn(0, 255) / 255f, - green = (g + 50).coerceIn(0, 255) / 255f, - blue = (b + 50).coerceIn(0, 255) / 255f - ) - return Brush.linearGradient( - colors = listOf(color1, color2), - start = androidx.compose.ui.geometry.Offset(0f, 0f), - end = androidx.compose.ui.geometry.Offset(100f, 100f) + colors = listOf( + Color( + red = (r + 100).coerceIn(0, 255) / 255f, + green = (g + 100).coerceIn(0, 255) / 255f, + blue = (b + 100).coerceIn(0, 255) / 255f + ), + Color( + red = (r + 50).coerceIn(0, 255) / 255f, + green = (g + 50).coerceIn(0, 255) / 255f, + blue = (b + 50).coerceIn(0, 255) / 255f + ) + ), + start = Offset(0f, 0f), + end = Offset(100f, 100f) ) } diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatInput.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatInput.kt index ea70312..ff5c16b 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatInput.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatInput.kt @@ -1,7 +1,12 @@ package ru.fromchat.ui.chat import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.background @@ -20,15 +25,16 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Reply import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -41,6 +47,7 @@ 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.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -56,6 +63,78 @@ import ru.fromchat.Res import ru.fromchat.api.Message import ru.fromchat.message_placeholder +@Composable +private fun AnimatedPreviewBar( + state: T?, + content: @Composable (T) -> Unit +) { + AnimatedVisibility( + visible = state != null, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + var lastState by remember { mutableStateOf(state) } + + LaunchedEffect(state) { + if (state != null) { + lastState = state + } + } + + if (state != null || lastState != null) { + content(state ?: lastState!!) + } + } +} + +@Composable +private fun PreviewBar( + icon: ImageVector, + title: String, + subtitle: String, + onClose: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1 + ) + } + + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + @OptIn(ExperimentalHazeMaterialsApi::class) @Composable fun ChatInput( @@ -77,6 +156,7 @@ fun ChatInput( if (text.isNotBlank()) { typingJob?.cancel() typingHandler.sendTyping() + @Suppress("AssignedValueIsNeverRead") typingJob = scope.launch { delay(3000) // 3 seconds typingHandler.stopTyping() @@ -90,30 +170,6 @@ fun ChatInput( Column( Modifier.windowInsetsPadding(WindowInsets.navigationBars) ) { - // Reply preview - replyTo?.let { reply -> - ReplyPreviewBar( - replyTo = reply, - onClose = onClearReply, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - .hazeEffect(hazeState, style = HazeMaterials.thick()) - ) - } - - // Edit preview - editingMessage?.let { edit -> - EditPreviewBar( - message = edit, - onClose = onClearEdit, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - .hazeEffect(hazeState, style = HazeMaterials.thick()) - ) - } - // Input field with blur Box( modifier = Modifier @@ -121,28 +177,46 @@ fun ChatInput( .background(Color.Transparent) .padding(horizontal = 8.dp, vertical = 8.dp) ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically + val shape = RoundedCornerShape(24.dp) + + Column( + modifier = Modifier + .fillMaxWidth() + .border( + Dp.Hairline, + MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), + shape + ) + .clip(shape) + .hazeEffect( + state = hazeState, + style = HazeMaterials.thin() + ) ) { - val shape = RoundedCornerShape(24.dp) + AnimatedPreviewBar(replyTo) { replyTo -> + PreviewBar( + icon = Icons.AutoMirrored.Filled.Reply, + title = "Replying to ${replyTo.username}", + subtitle = replyTo.content.take(50) + if (replyTo.content.length > 50) "..." else "", + onClose = { onClearReply() } + ) + } + + AnimatedPreviewBar(editingMessage) { message -> + PreviewBar( + icon = Icons.Filled.Edit, + title = "Editing message", + subtitle = message.content.take(50) + if (message.content.length > 50) "..." else "", + onClose = { onClearEdit() } + ) + } OutlinedTextField( value = text, onValueChange = onTextChange, modifier = Modifier - .weight(1f) - .border( - Dp.Hairline, - MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), - shape - ) - .clip(shape) - .hazeEffect( - state = hazeState, - style = HazeMaterials.thin() - ), + .fillMaxWidth() + .animateContentSize(), placeholder = { Text( text = stringResource(Res.string.message_placeholder), @@ -199,92 +273,4 @@ fun ChatInput( } } } -} - -@Composable -private fun ReplyPreviewBar( - replyTo: Message, - onClose: () -> Unit, - modifier: Modifier = Modifier -) { - Surface( - modifier = modifier, // hazeEffect moved to ChatInput where it's called - shape = RoundedCornerShape(8.dp), - color = Color.Transparent - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Replying to ${replyTo.username}", - style = MaterialTheme.typography.labelSmall, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = replyTo.content.take(50) + if (replyTo.content.length > 50) "..." else "", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1 - ) - } - IconButton(onClick = onClose) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Close", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } -} - -@Composable -private fun EditPreviewBar( - message: Message, - onClose: () -> Unit, - modifier: Modifier = Modifier -) { - Surface( - modifier = modifier, // hazeEffect moved to ChatInput where it's called - shape = RoundedCornerShape(8.dp), - color = Color.Transparent - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Editing message", - style = MaterialTheme.typography.labelSmall, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = message.content.take(50) + if (message.content.length > 50) "..." else "", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1 - ) - } - IconButton(onClick = onClose) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Close", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } -} +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt index 953fdc0..65e921d 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt @@ -279,7 +279,10 @@ fun ChatScreen( replyTo = replyTo, editingMessage = editingMessage, onClearReply = { replyTo = null }, - onClearEdit = { editingMessage = null }, + onClearEdit = { + editingMessage = null + inputText = "" + }, hazeState = hazeState ) } @@ -355,13 +358,17 @@ fun ChatScreen( } // Context menu + @Suppress("AssignedValueIsNeverRead") MessageContextMenu( state = contextMenuState, isAuthor = contextMenuState.message?.user_id == currentUserId, onDismiss = { contextMenuState = contextMenuState.copy(isOpen = false) }, onReply = { message -> replyTo = message - editingMessage = null + if (editingMessage != null) { + editingMessage = null + inputText = "" + } }, onEdit = { message -> editingMessage = message