mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Improve reply preview
This commit is contained in:
@@ -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,9 +108,11 @@ 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` = {
|
||||||
|
it
|
||||||
|
.shadow(
|
||||||
elevation = 8.dp,
|
elevation = 8.dp,
|
||||||
shape = RoundedCornerShape(
|
shape = RoundedCornerShape(
|
||||||
topStart = 20.dp,
|
topStart = 20.dp,
|
||||||
@@ -124,23 +122,13 @@ fun MessageItem(
|
|||||||
),
|
),
|
||||||
spotColor = if (isDark) Color(0x66000000) else Color(0x33000000)
|
spotColor = if (isDark) Color(0x66000000) else Color(0x33000000)
|
||||||
)
|
)
|
||||||
} else {
|
.background(getMessageGradient(isDark))
|
||||||
Modifier
|
},
|
||||||
|
`else` = {
|
||||||
|
background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.background(
|
.padding(top = 6.dp)
|
||||||
brush = if (isAuthor) {
|
|
||||||
getMessageGradient(isDark)
|
|
||||||
} else {
|
|
||||||
Brush.linearGradient(
|
|
||||||
listOf(
|
|
||||||
MaterialTheme.colorScheme.surfaceContainerHighest,
|
|
||||||
MaterialTheme.colorScheme.surfaceContainerHighest
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 8.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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user