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)
)
/**
* 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
*/
@@ -12,9 +12,12 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.pr0gramm3r101.utils.conditional
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import ru.fromchat.api.Message
@@ -92,18 +95,11 @@ fun MessageItem(
.widthIn(max = 280.dp),
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
val isDark = isSystemInDarkTheme()
Box(
modifier = Modifier
.width(IntrinsicSize.Min)
.clip(
RoundedCornerShape(
topStart = 20.dp,
@@ -112,35 +108,27 @@ fun MessageItem(
bottomEnd = if (isAuthor) 8.dp else 20.dp
)
)
.then(
if (isAuthor) {
Modifier.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(
topStart = 20.dp,
topEnd = 20.dp,
bottomStart = 20.dp,
bottomEnd = 8.dp
),
spotColor = if (isDark) Color(0x66000000) else Color(0x33000000)
)
} else {
Modifier
}
)
.background(
brush = if (isAuthor) {
getMessageGradient(isDark)
} else {
Brush.linearGradient(
listOf(
MaterialTheme.colorScheme.surfaceContainerHighest,
MaterialTheme.colorScheme.surfaceContainerHighest
.conditional(
isAuthor,
`if` = {
it
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(
topStart = 20.dp,
topEnd = 20.dp,
bottomStart = 20.dp,
bottomEnd = 8.dp
),
spotColor = if (isDark) Color(0x66000000) else Color(0x33000000)
)
)
.background(getMessageGradient(isDark))
},
`else` = {
background(MaterialTheme.colorScheme.surfaceContainerHighest)
}
)
.padding(horizontal = 12.dp, vertical = 8.dp)
.padding(top = 6.dp)
) {
Column {
// Username inside bubble (for received messages)
@@ -150,10 +138,59 @@ fun MessageItem(
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold,
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
Text(
text = message.content,
@@ -162,12 +199,14 @@ fun MessageItem(
Color.White
} else {
MaterialTheme.colorScheme.onSurface
}
},
modifier = Modifier.padding(horizontal = 12.dp)
)
// Timestamp and edited indicator
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,
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
private fun formatTime(timestamp: String): String {
return try {
val instant = Instant.parse(timestamp)
val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault())
val hour = localDateTime.hour.toString().padStart(2, '0')
val minute = localDateTime.minute.toString().padStart(2, '0')
"$hour:$minute"
} catch (e: Exception) {
Instant.parse(timestamp).toLocalDateTime(TimeZone.currentSystemDefault()).let {
"${
it.hour.toString().padStart(2, '0')
}:${
it.minute.toString().padStart(2, '0')
}"
}
} catch (_: Exception) {
// Fallback: try parsing without timezone if it fails
try {
val parts = timestamp.split("T")
@@ -254,9 +265,8 @@ private fun formatTime(timestamp: String): String {
} else {
""
}
} catch (e2: Exception) {
} catch (_: Exception) {
""
}
}
}
}
@@ -1,5 +1,6 @@
package com.pr0gramm3r101.utils
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
/**
@@ -21,18 +22,22 @@ fun NavController.navigateAndWipeBackStack(route: String, launchSingleTop: Boole
val currentRoute = currentBackStackEntry?.destination?.route
// 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) {
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)
}