Improve reply preview

This commit is contained in:
2025-12-25 20:27:33 +03:00
Unverified
parent 3f4cddf781
commit 169cfc815e
3 changed files with 121 additions and 85 deletions
@@ -26,6 +26,27 @@ fun getMessageGradient(isDark: Boolean) = Brush.linearGradient(
end = Offset(1000f, 1000f) end = Offset(1000f, 1000f)
) )
/**
* Get gradient brush for own messages
*/
fun getReplyMessageGradient(isDark: Boolean) = Brush.linearGradient(
colors = if (isDark) {
listOf(
Color(0xFF5f11a6),
Color(0xFF1418da),
Color(0xFF1b3d73)
)
} else {
listOf(
Color(0xFF7836ee),
Color(0xFF2034f3),
Color(0xFF076eed)
)
},
start = Offset(0f, 0f),
end = Offset(1000f, 1000f)
)
/** /**
* Generate a consistent gradient from a name for avatar fallback * Generate a consistent gradient from a name for avatar fallback
*/ */
@@ -12,9 +12,12 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
@@ -27,12 +30,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.pr0gramm3r101.utils.conditional
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import ru.fromchat.api.Message import ru.fromchat.api.Message
@@ -92,18 +95,11 @@ fun MessageItem(
.widthIn(max = 280.dp), .widthIn(max = 280.dp),
horizontalAlignment = if (isAuthor) Alignment.End else Alignment.Start horizontalAlignment = if (isAuthor) Alignment.End else Alignment.Start
) { ) {
// Reply preview
message.reply_to?.let { replyTo ->
ReplyPreview(
replyTo = replyTo,
modifier = Modifier.padding(bottom = 4.dp)
)
}
// Message bubble // Message bubble
val isDark = isSystemInDarkTheme() val isDark = isSystemInDarkTheme()
Box( Box(
modifier = Modifier modifier = Modifier
.width(IntrinsicSize.Min)
.clip( .clip(
RoundedCornerShape( RoundedCornerShape(
topStart = 20.dp, topStart = 20.dp,
@@ -112,35 +108,27 @@ fun MessageItem(
bottomEnd = if (isAuthor) 8.dp else 20.dp bottomEnd = if (isAuthor) 8.dp else 20.dp
) )
) )
.then( .conditional(
if (isAuthor) { isAuthor,
Modifier.shadow( `if` = {
elevation = 8.dp, it
shape = RoundedCornerShape( .shadow(
topStart = 20.dp, elevation = 8.dp,
topEnd = 20.dp, shape = RoundedCornerShape(
bottomStart = 20.dp, topStart = 20.dp,
bottomEnd = 8.dp topEnd = 20.dp,
), bottomStart = 20.dp,
spotColor = if (isDark) Color(0x66000000) else Color(0x33000000) bottomEnd = 8.dp
) ),
} else { spotColor = if (isDark) Color(0x66000000) else Color(0x33000000)
Modifier
}
)
.background(
brush = if (isAuthor) {
getMessageGradient(isDark)
} else {
Brush.linearGradient(
listOf(
MaterialTheme.colorScheme.surfaceContainerHighest,
MaterialTheme.colorScheme.surfaceContainerHighest
) )
) .background(getMessageGradient(isDark))
},
`else` = {
background(MaterialTheme.colorScheme.surfaceContainerHighest)
} }
) )
.padding(horizontal = 12.dp, vertical = 8.dp) .padding(top = 6.dp)
) { ) {
Column { Column {
// Username inside bubble (for received messages) // Username inside bubble (for received messages)
@@ -150,10 +138,59 @@ fun MessageItem(
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 4.dp) modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 4.dp)
) )
} }
// Reply preview
message.reply_to?.let { replyTo ->
Box(
Modifier.padding(bottom = 4.dp, start = 6.dp, end = 6.dp)
) {
Row(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.fillMaxWidth()
.height(IntrinsicSize.Min)
.conditional(
isAuthor,
`if` = {
background(getReplyMessageGradient(isDark))
},
`else` = {
background(MaterialTheme.colorScheme.surfaceVariant)
}
)
) {
Box(
Modifier
.background(MaterialTheme.colorScheme.primary)
.width(3.dp)
.fillMaxHeight()
)
Column(
Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
) {
Text(
text = replyTo.username,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary,
fontSize = 11.sp
)
Text(
text = replyTo.content.take(50) + if (replyTo.content.length > 50) "..." else "",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
maxLines = 1
)
}
}
}
}
// Message content // Message content
Text( Text(
text = message.content, text = message.content,
@@ -162,12 +199,14 @@ fun MessageItem(
Color.White Color.White
} else { } else {
MaterialTheme.colorScheme.onSurface MaterialTheme.colorScheme.onSurface
} },
modifier = Modifier.padding(horizontal = 12.dp)
) )
// Timestamp and edited indicator // Timestamp and edited indicator
Row( Row(
modifier = Modifier.padding(top = 4.dp), modifier = Modifier
.padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 8.dp),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -202,45 +241,17 @@ fun MessageItem(
} }
} }
@Composable
private fun ReplyPreview(
replyTo: Message,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 8.dp, vertical = 6.dp)
) {
Column {
Text(
text = replyTo.username,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary,
fontSize = 11.sp
)
Text(
text = replyTo.content.take(50) + if (replyTo.content.length > 50) "..." else "",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
maxLines = 1
)
}
}
}
@ExperimentalTime @ExperimentalTime
private fun formatTime(timestamp: String): String { private fun formatTime(timestamp: String): String {
return try { return try {
val instant = Instant.parse(timestamp) Instant.parse(timestamp).toLocalDateTime(TimeZone.currentSystemDefault()).let {
val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) "${
val hour = localDateTime.hour.toString().padStart(2, '0') it.hour.toString().padStart(2, '0')
val minute = localDateTime.minute.toString().padStart(2, '0') }:${
"$hour:$minute" it.minute.toString().padStart(2, '0')
} catch (e: Exception) { }"
}
} catch (_: Exception) {
// Fallback: try parsing without timezone if it fails // Fallback: try parsing without timezone if it fails
try { try {
val parts = timestamp.split("T") val parts = timestamp.split("T")
@@ -254,9 +265,8 @@ private fun formatTime(timestamp: String): String {
} else { } else {
"" ""
} }
} catch (e2: Exception) { } catch (_: Exception) {
"" ""
} }
} }
} }
@@ -1,5 +1,6 @@
package com.pr0gramm3r101.utils package com.pr0gramm3r101.utils
import androidx.compose.ui.Modifier
import androidx.navigation.NavController import androidx.navigation.NavController
/** /**
@@ -21,18 +22,22 @@ fun NavController.navigateAndWipeBackStack(route: String, launchSingleTop: Boole
val currentRoute = currentBackStackEntry?.destination?.route val currentRoute = currentBackStackEntry?.destination?.route
// Navigate to the new route, removing the current screen as well // Navigate to the new route, removing the current screen as well
if (currentRoute != null && currentRoute != route) { navigate(route) {
navigate(route) { if (currentRoute != null && currentRoute != route) {
popUpTo(currentRoute) { popUpTo(currentRoute) {
inclusive = true inclusive = true
} }
this.launchSingleTop = launchSingleTop
}
} else {
// Fallback: just navigate if current route is same or null
navigate(route) {
this.launchSingleTop = launchSingleTop
} }
this.launchSingleTop = launchSingleTop
} }
} }
inline fun Modifier.conditional(
condition: Boolean,
`else`: Modifier.(Modifier) -> Modifier = { Modifier },
`if`: Modifier.(Modifier) -> Modifier
) = if (condition) {
this + `if`(this)
} else {
this + `else`(this)
}