feat: Enhance message display with inline timestamps and status for outgoing messages in chat

This commit is contained in:
k1ngsterr1
2026-01-13 19:19:44 +05:00
parent b60738ce55
commit a7576865ef
2 changed files with 90 additions and 69 deletions

View File

@@ -1591,10 +1591,38 @@ private fun MessageBubble(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
} }
AppleEmojiText(text = message.text, color = textColor, fontSize = 16.sp) // 🔥 Telegram-style: текст + время inline
Spacer(modifier = Modifier.height(2.dp)) 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,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(top = 2.dp)
)
}
} else {
// Исходящие сообщения - время + статус справа внизу
Box {
AppleEmojiText(
text = message.text + " ", // Пробелы для места под время и статус
color = textColor,
fontSize = 16.sp
)
// Время и статус в правом нижнем углу
Row( Row(
modifier = Modifier.align(Alignment.End), modifier = Modifier
.align(Alignment.BottomEnd)
.padding(top = 2.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
@@ -1602,7 +1630,6 @@ private fun MessageBubble(
color = timeColor, color = timeColor,
fontSize = 11.sp fontSize = 11.sp
) )
if (message.isOutgoing) {
Spacer(modifier = Modifier.width(3.dp)) Spacer(modifier = Modifier.width(3.dp))
AnimatedMessageStatus( AnimatedMessageStatus(
status = message.status, status = message.status,
@@ -1616,6 +1643,7 @@ private fun MessageBubble(
} }
} }
} }
}
} // End of swipe Box wrapper } // End of swipe Box wrapper
} }
@@ -2273,30 +2301,20 @@ fun MessageSkeletonList(
isDarkTheme: Boolean, isDarkTheme: Boolean,
modifier: Modifier = Modifier 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 infiniteTransition = rememberInfiniteTransition(label = "shimmer")
val shimmerProgress by infiniteTransition.animateFloat( val shimmerAlpha by infiniteTransition.animateFloat(
initialValue = 0f, initialValue = 0.4f,
targetValue = 1f, targetValue = 0.8f,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(1200, easing = LinearEasing), animation = tween(800, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart repeatMode = RepeatMode.Reverse
), ),
label = "shimmer" label = "shimmerAlpha"
)
// Градиент для shimmer эффекта
val shimmerBrush = Brush.horizontalGradient(
colors = listOf(
shimmerColor,
shimmerHighlight,
shimmerColor
),
startX = shimmerProgress * 1000f - 500f,
endX = shimmerProgress * 1000f + 500f
) )
// 🔥 Box с выравниванием внизу - как настоящий чат // 🔥 Box с выравниванием внизу - как настоящий чат
@@ -2306,28 +2324,28 @@ fun MessageSkeletonList(
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp), .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 = true, widthFraction = 0.45f, bubbleColor = outgoingBubbleColor, alpha = shimmerAlpha)
SkeletonBubble(isOutgoing = false, widthFraction = 0.35f, brush = shimmerBrush, isDarkTheme = isDarkTheme) SkeletonBubble(isOutgoing = false, widthFraction = 0.55f, bubbleColor = incomingBubbleColor, alpha = shimmerAlpha)
SkeletonBubble(isOutgoing = true, widthFraction = 0.30f, brush = shimmerBrush, isDarkTheme = isDarkTheme) SkeletonBubble(isOutgoing = true, widthFraction = 0.35f, bubbleColor = outgoingBubbleColor, alpha = shimmerAlpha)
SkeletonBubble(isOutgoing = false, widthFraction = 0.28f, brush = shimmerBrush, isDarkTheme = isDarkTheme) SkeletonBubble(isOutgoing = false, widthFraction = 0.50f, bubbleColor = incomingBubbleColor, alpha = shimmerAlpha)
SkeletonBubble(isOutgoing = true, widthFraction = 0.40f, brush = shimmerBrush, isDarkTheme = isDarkTheme) SkeletonBubble(isOutgoing = true, widthFraction = 0.60f, bubbleColor = outgoingBubbleColor, alpha = shimmerAlpha)
SkeletonBubble(isOutgoing = false, widthFraction = 0.32f, brush = shimmerBrush, isDarkTheme = isDarkTheme) SkeletonBubble(isOutgoing = false, widthFraction = 0.40f, bubbleColor = incomingBubbleColor, alpha = shimmerAlpha)
} }
} }
} }
/** /**
* Пузырёк-скелетон сообщения (как настоящий bubble) * Пузырёк-скелетон сообщения (толстый как настоящий с текстом)
*/ */
@Composable @Composable
private fun SkeletonBubble( private fun SkeletonBubble(
isOutgoing: Boolean, isOutgoing: Boolean,
widthFraction: Float, widthFraction: Float,
brush: Brush, bubbleColor: Color,
isDarkTheme: Boolean alpha: Float
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -2336,14 +2354,15 @@ private fun SkeletonBubble(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth(widthFraction) .fillMaxWidth(widthFraction)
.height(34.dp) // Фиксированная высота как у реального пузырька .defaultMinSize(minHeight = 44.dp) // Минимум как пузырёк с текстом
.clip(RoundedCornerShape( .clip(RoundedCornerShape(
topStart = 18.dp, topStart = 18.dp,
topEnd = 18.dp, topEnd = 18.dp,
bottomStart = if (isOutgoing) 18.dp else 4.dp, bottomStart = if (isOutgoing) 18.dp else 6.dp,
bottomEnd = if (isOutgoing) 4.dp else 18.dp bottomEnd = if (isOutgoing) 6.dp else 18.dp
)) ))
.background(brush) .background(bubbleColor.copy(alpha = alpha))
.padding(horizontal = 14.dp, vertical = 10.dp)
) )
} }
} }

View File

@@ -815,10 +815,12 @@ fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit)
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Avatar container with online indicator
Box(modifier = Modifier.size(56.dp)) {
// Avatar // Avatar
Box( Box(
modifier = modifier =
Modifier.size(56.dp) Modifier.fillMaxSize()
.clip(CircleShape) .clip(CircleShape)
.background(avatarColors.backgroundColor), .background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -829,14 +831,14 @@ fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit)
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
fontSize = 18.sp fontSize = 18.sp
) )
}
// Online indicator // Online indicator
if (dialog.isOnline == 1) { if (dialog.isOnline == 1) {
Box( Box(
modifier = modifier =
Modifier.size(14.dp) Modifier.size(16.dp)
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.offset(x = (-2).dp, y = (-2).dp)
.clip(CircleShape) .clip(CircleShape)
.background( .background(
if (isDarkTheme) Color(0xFF1A1A1A) if (isDarkTheme) Color(0xFF1A1A1A)