From a7576865ef39e37efd098d0a426368fc3f07da6c Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 13 Jan 2026 19:19:44 +0500 Subject: [PATCH] feat: Enhance message display with inline timestamps and status for outgoing messages in chat --- .../messenger/ui/chats/ChatDetailScreen.kt | 123 ++++++++++-------- .../messenger/ui/chats/ChatsListScreen.kt | 36 ++--- 2 files changed, 90 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 161c8d1..fd9981d 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -1591,27 +1591,55 @@ private fun MessageBubble( Spacer(modifier = Modifier.height(6.dp)) } - AppleEmojiText(text = message.text, color = textColor, fontSize = 16.sp) - Spacer(modifier = Modifier.height(2.dp)) - Row( - modifier = Modifier.align(Alignment.End), - verticalAlignment = Alignment.CenterVertically - ) { - Text( + // 🔥 Telegram-style: текст + время inline + if (!message.isOutgoing) { + // Входящие сообщения - время справа inline с текстом + Box { + AppleEmojiText( + text = message.text + " ", // Пробелы для места под время + color = textColor, + fontSize = 16.sp + ) + // Время в правом нижнем углу + Text( text = timeFormat.format(message.timestamp), color = timeColor, - fontSize = 11.sp - ) - if (message.isOutgoing) { - Spacer(modifier = Modifier.width(3.dp)) - AnimatedMessageStatus( - status = message.status, - timeColor = timeColor, - timestamp = message.timestamp.time, - onRetry = onRetry, - onDelete = onDelete + fontSize = 11.sp, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(top = 2.dp) ) } + } else { + // Исходящие сообщения - время + статус справа внизу + Box { + AppleEmojiText( + text = message.text + " ", // Пробелы для места под время и статус + color = textColor, + fontSize = 16.sp + ) + // Время и статус в правом нижнем углу + Row( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(top = 2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = timeFormat.format(message.timestamp), + color = timeColor, + fontSize = 11.sp + ) + Spacer(modifier = Modifier.width(3.dp)) + AnimatedMessageStatus( + status = message.status, + timeColor = timeColor, + timestamp = message.timestamp.time, + onRetry = onRetry, + onDelete = onDelete + ) + } + } } } } @@ -2273,30 +2301,20 @@ fun MessageSkeletonList( isDarkTheme: Boolean, modifier: Modifier = Modifier ) { - val shimmerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8) - val shimmerHighlight = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFF5F5F5) + // Цвета пузырьков как у настоящих сообщений + val outgoingBubbleColor = if (isDarkTheme) Color(0xFF3B82F6).copy(alpha = 0.3f) else Color(0xFF3B82F6).copy(alpha = 0.2f) + val incomingBubbleColor = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE5E5EA) - // Анимация shimmer + // Shimmer анимация val infiniteTransition = rememberInfiniteTransition(label = "shimmer") - val shimmerProgress by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 1f, + val shimmerAlpha by infiniteTransition.animateFloat( + initialValue = 0.4f, + targetValue = 0.8f, animationSpec = infiniteRepeatable( - animation = tween(1200, easing = LinearEasing), - repeatMode = RepeatMode.Restart + animation = tween(800, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse ), - label = "shimmer" - ) - - // Градиент для shimmer эффекта - val shimmerBrush = Brush.horizontalGradient( - colors = listOf( - shimmerColor, - shimmerHighlight, - shimmerColor - ), - startX = shimmerProgress * 1000f - 500f, - endX = shimmerProgress * 1000f + 500f + label = "shimmerAlpha" ) // 🔥 Box с выравниванием внизу - как настоящий чат @@ -2306,28 +2324,28 @@ fun MessageSkeletonList( .align(Alignment.BottomCenter) .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(6.dp) ) { - // Паттерн сообщений снизу вверх (как в реальном чате) - короткие пузырьки - SkeletonBubble(isOutgoing = true, widthFraction = 0.25f, brush = shimmerBrush, isDarkTheme = isDarkTheme) - SkeletonBubble(isOutgoing = false, widthFraction = 0.35f, brush = shimmerBrush, isDarkTheme = isDarkTheme) - SkeletonBubble(isOutgoing = true, widthFraction = 0.30f, brush = shimmerBrush, isDarkTheme = isDarkTheme) - SkeletonBubble(isOutgoing = false, widthFraction = 0.28f, brush = shimmerBrush, isDarkTheme = isDarkTheme) - SkeletonBubble(isOutgoing = true, widthFraction = 0.40f, brush = shimmerBrush, isDarkTheme = isDarkTheme) - SkeletonBubble(isOutgoing = false, widthFraction = 0.32f, brush = shimmerBrush, isDarkTheme = isDarkTheme) + // Паттерн сообщений снизу вверх (как в реальном чате) + SkeletonBubble(isOutgoing = true, widthFraction = 0.45f, bubbleColor = outgoingBubbleColor, alpha = shimmerAlpha) + SkeletonBubble(isOutgoing = false, widthFraction = 0.55f, bubbleColor = incomingBubbleColor, alpha = shimmerAlpha) + SkeletonBubble(isOutgoing = true, widthFraction = 0.35f, bubbleColor = outgoingBubbleColor, alpha = shimmerAlpha) + SkeletonBubble(isOutgoing = false, widthFraction = 0.50f, bubbleColor = incomingBubbleColor, alpha = shimmerAlpha) + SkeletonBubble(isOutgoing = true, widthFraction = 0.60f, bubbleColor = outgoingBubbleColor, alpha = shimmerAlpha) + SkeletonBubble(isOutgoing = false, widthFraction = 0.40f, bubbleColor = incomingBubbleColor, alpha = shimmerAlpha) } } } /** - * Пузырёк-скелетон сообщения (как настоящий bubble) + * Пузырёк-скелетон сообщения (толстый как настоящий с текстом) */ @Composable private fun SkeletonBubble( isOutgoing: Boolean, widthFraction: Float, - brush: Brush, - isDarkTheme: Boolean + bubbleColor: Color, + alpha: Float ) { Row( modifier = Modifier.fillMaxWidth(), @@ -2336,14 +2354,15 @@ private fun SkeletonBubble( Box( modifier = Modifier .fillMaxWidth(widthFraction) - .height(34.dp) // Фиксированная высота как у реального пузырька + .defaultMinSize(minHeight = 44.dp) // Минимум как пузырёк с текстом .clip(RoundedCornerShape( topStart = 18.dp, topEnd = 18.dp, - bottomStart = if (isOutgoing) 18.dp else 4.dp, - bottomEnd = if (isOutgoing) 4.dp else 18.dp + bottomStart = if (isOutgoing) 18.dp else 6.dp, + bottomEnd = if (isOutgoing) 6.dp else 18.dp )) - .background(brush) + .background(bubbleColor.copy(alpha = alpha)) + .padding(horizontal = 14.dp, vertical = 10.dp) ) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index 90db50d..25cc7a3 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -815,28 +815,30 @@ fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit) .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { - // Avatar - Box( - modifier = - Modifier.size(56.dp) - .clip(CircleShape) - .background(avatarColors.backgroundColor), - contentAlignment = Alignment.Center - ) { - Text( - text = initials, - color = avatarColors.textColor, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp - ) - + // Avatar container with online indicator + Box(modifier = Modifier.size(56.dp)) { + // Avatar + Box( + modifier = + Modifier.fillMaxSize() + .clip(CircleShape) + .background(avatarColors.backgroundColor), + contentAlignment = Alignment.Center + ) { + Text( + text = initials, + color = avatarColors.textColor, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp + ) + } + // Online indicator if (dialog.isOnline == 1) { Box( modifier = - Modifier.size(14.dp) + Modifier.size(16.dp) .align(Alignment.BottomEnd) - .offset(x = (-2).dp, y = (-2).dp) .clip(CircleShape) .background( if (isDarkTheme) Color(0xFF1A1A1A)