mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Change timer icon and fix more animation bugs
Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
@@ -1006,12 +1006,19 @@ fun ChatScreen(
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isPendingOwnTextSend =
|
||||||
|
message.id < 0 &&
|
||||||
|
message.files.isNullOrEmpty() &&
|
||||||
|
message.user_id == currentUserId
|
||||||
MessageItem(
|
MessageItem(
|
||||||
message = message,
|
message = message,
|
||||||
isAuthor = message.user_id == currentUserId,
|
isAuthor = message.user_id == currentUserId,
|
||||||
group = item.group,
|
group = item.group,
|
||||||
showTimestamp = showTimestamp,
|
showTimestamp = showTimestamp,
|
||||||
onBubbleTap = {
|
onBubbleTap = if (isPendingOwnTextSend) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
{
|
||||||
if (item.group.isLastInGroup &&
|
if (item.group.isLastInGroup &&
|
||||||
messageKey !in revealedTimestampKeys
|
messageKey !in revealedTimestampKeys
|
||||||
) {
|
) {
|
||||||
@@ -1032,6 +1039,7 @@ fun ChatScreen(
|
|||||||
hiddenDefaultTimestampKeys =
|
hiddenDefaultTimestampKeys =
|
||||||
hiddenDefaultTimestampKeys - messageKey
|
hiddenDefaultTimestampKeys - messageKey
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
enterAnimationRole = enterRole,
|
enterAnimationRole = enterRole,
|
||||||
modifier = Modifier.padding(top = item.spacingAbove),
|
modifier = Modifier.padding(top = item.spacingAbove),
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ import androidx.compose.foundation.layout.widthIn
|
|||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Timer
|
||||||
import androidx.compose.material.icons.rounded.ErrorOutline
|
import androidx.compose.material.icons.rounded.ErrorOutline
|
||||||
import androidx.compose.material.icons.rounded.Schedule
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -62,6 +62,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
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 kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import ru.fromchat.Logger
|
import ru.fromchat.Logger
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
@@ -117,7 +118,7 @@ private fun Modifier.enterLayoutHeight(
|
|||||||
if (!active) return this
|
if (!active) return this
|
||||||
return layout { measurable, constraints ->
|
return layout { measurable, constraints ->
|
||||||
val placeable = measurable.measure(constraints)
|
val placeable = measurable.measure(constraints)
|
||||||
val layoutScale = scale.coerceIn(0f, 1f)
|
val layoutScale = scale.coerceAtLeast(0f)
|
||||||
val h = (placeable.height * layoutScale).roundToInt().coerceAtLeast(
|
val h = (placeable.height * layoutScale).roundToInt().coerceAtLeast(
|
||||||
if (layoutScale > 0f) minHeightPx else 0,
|
if (layoutScale > 0f) minHeightPx else 0,
|
||||||
)
|
)
|
||||||
@@ -241,6 +242,12 @@ fun MessageItem(
|
|||||||
Animatable(if (startsAsNew) 0f else 1f)
|
Animatable(if (startsAsNew) 0f else 1f)
|
||||||
}
|
}
|
||||||
val timestampForceAlpha = remember(enterIdentity) { Animatable(1f) }
|
val timestampForceAlpha = remember(enterIdentity) { Animatable(1f) }
|
||||||
|
val isPendingOutbound = message.id < 0 && message.files.isNullOrEmpty()
|
||||||
|
val sendFailed = isPendingOutbound && !message.uploadError.isNullOrBlank()
|
||||||
|
val showSendingIndicator =
|
||||||
|
isAuthor &&
|
||||||
|
isPendingOutbound &&
|
||||||
|
!sendFailed
|
||||||
val isNewEnterRole = enterAnimationRole == EnterAnimationRole.NewMessage
|
val isNewEnterRole = enterAnimationRole == EnterAnimationRole.NewMessage
|
||||||
LaunchedEffect(enterIdentity, enterAnimationRole, showTimestamp) {
|
LaunchedEffect(enterIdentity, enterAnimationRole, showTimestamp) {
|
||||||
Logger.d(
|
Logger.d(
|
||||||
@@ -271,7 +278,7 @@ fun MessageItem(
|
|||||||
enterScale.animateTo(
|
enterScale.animateTo(
|
||||||
1f,
|
1f,
|
||||||
spring(
|
spring(
|
||||||
dampingRatio = Spring.DampingRatioNoBouncy,
|
dampingRatio = 0.76f,
|
||||||
stiffness = Spring.StiffnessMediumLow,
|
stiffness = Spring.StiffnessMediumLow,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -281,11 +288,11 @@ fun MessageItem(
|
|||||||
"spring_end identity=${enterIdentity.take(12)} role=${enterAnimationRole.name}",
|
"spring_end identity=${enterIdentity.take(12)} role=${enterAnimationRole.name}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LaunchedEffect(enterIdentity, enterAnimationRole) {
|
LaunchedEffect(enterIdentity, enterAnimationRole, showSendingIndicator, sendFailed) {
|
||||||
when (enterAnimationRole) {
|
when (enterAnimationRole) {
|
||||||
EnterAnimationRole.PreviousLast -> {
|
EnterAnimationRole.PreviousLast -> {
|
||||||
// Fade in parallel with the new bubble spring (same frame as enqueue).
|
// Still-sending rows keep the timer visible; only fade delivered timestamps.
|
||||||
if (timestampForceAlpha.value > 0.01f) {
|
if (!showSendingIndicator && !sendFailed && timestampForceAlpha.value > 0.01f) {
|
||||||
timestampForceAlpha.animateTo(0f, tween(80))
|
timestampForceAlpha.animateTo(0f, tween(80))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,27 +316,48 @@ fun MessageItem(
|
|||||||
val showAvatar = showAvatarSlot && group.isLastInGroup
|
val showAvatar = showAvatarSlot && group.isLastInGroup
|
||||||
val showUsernameInBubble = showUsername && !isAuthor && group.isFirstInGroup
|
val showUsernameInBubble = showUsername && !isAuthor && group.isFirstInGroup
|
||||||
|
|
||||||
val isPendingOutbound = message.id < 0 && message.files.isNullOrEmpty()
|
val sendingMetaAlpha = remember(enterIdentity) {
|
||||||
val sendFailed = isPendingOutbound && !message.uploadError.isNullOrBlank()
|
Animatable(if (showSendingIndicator) 1f else 0f)
|
||||||
val showScheduleIcon =
|
}
|
||||||
runEnterAnimation &&
|
val deliveredTimeAlpha = remember(enterIdentity) {
|
||||||
isAuthor &&
|
Animatable(if (showSendingIndicator) 0f else 1f)
|
||||||
isPendingOutbound &&
|
}
|
||||||
!sendFailed
|
var wasSendingIndicator by remember(enterIdentity) { mutableStateOf(showSendingIndicator) }
|
||||||
|
LaunchedEffect(showSendingIndicator, sendFailed, group.isLastInGroup) {
|
||||||
|
when {
|
||||||
|
showSendingIndicator -> {
|
||||||
|
sendingMetaAlpha.snapTo(1f)
|
||||||
|
deliveredTimeAlpha.snapTo(0f)
|
||||||
|
wasSendingIndicator = true
|
||||||
|
}
|
||||||
|
wasSendingIndicator && !sendFailed -> {
|
||||||
|
wasSendingIndicator = false
|
||||||
|
if (group.isLastInGroup) {
|
||||||
|
launch { sendingMetaAlpha.animateTo(0f, tween(220)) }
|
||||||
|
deliveredTimeAlpha.animateTo(1f, tween(220))
|
||||||
|
} else {
|
||||||
|
sendingMetaAlpha.snapTo(0f)
|
||||||
|
deliveredTimeAlpha.snapTo(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Group membership drives timestamp space; PreviousLast fades alpha while
|
// Group membership drives timestamp space; PreviousLast fades alpha while
|
||||||
// showTimestamp is still held true by ChatScreen until this role arrives.
|
// showTimestamp is still held true by ChatScreen until this role arrives.
|
||||||
val timestampVisible = when {
|
val timestampVisible = when {
|
||||||
|
showSendingIndicator || sendFailed -> true
|
||||||
enterAnimationRole == EnterAnimationRole.PreviousLast -> false
|
enterAnimationRole == EnterAnimationRole.PreviousLast -> false
|
||||||
showScheduleIcon || sendFailed -> true
|
|
||||||
else -> showTimestamp
|
else -> showTimestamp
|
||||||
}
|
}
|
||||||
val timestampAlpha =
|
val timestampAlpha = when {
|
||||||
if (enterAnimationRole == EnterAnimationRole.PreviousLast) {
|
showSendingIndicator || sendFailed -> 1f
|
||||||
timestampForceAlpha.value
|
enterAnimationRole == EnterAnimationRole.PreviousLast -> timestampForceAlpha.value
|
||||||
} else {
|
else -> 1f
|
||||||
1f
|
|
||||||
}
|
}
|
||||||
val timestampTakesSpace = timestampVisible
|
val timestampTakesSpace =
|
||||||
|
timestampVisible ||
|
||||||
|
sendingMetaAlpha.value > 0.01f ||
|
||||||
|
(group.isLastInGroup && deliveredTimeAlpha.value > 0.01f && !showSendingIndicator)
|
||||||
val metaColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
val metaColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||||
val enterTransformOrigin =
|
val enterTransformOrigin =
|
||||||
if (isAuthor) TransformOrigin(1f, 1f) else TransformOrigin(0f, 1f)
|
if (isAuthor) TransformOrigin(1f, 1f) else TransformOrigin(0f, 1f)
|
||||||
@@ -853,8 +881,8 @@ fun MessageItem(
|
|||||||
.graphicsLayer { alpha = timestampAlpha }
|
.graphicsLayer { alpha = timestampAlpha }
|
||||||
.padding(
|
.padding(
|
||||||
top = 2.dp,
|
top = 2.dp,
|
||||||
start = if (isAuthor) 0.dp else 4.dp,
|
start = if (isAuthor) 0.dp else 8.dp,
|
||||||
end = if (isAuthor) 4.dp else 0.dp,
|
end = if (isAuthor) 8.dp else 0.dp,
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -866,22 +894,15 @@ fun MessageItem(
|
|||||||
exit = fadeOut(tween(180)) +
|
exit = fadeOut(tween(180)) +
|
||||||
shrinkVertically(shrinkTowards = Alignment.Top),
|
shrinkVertically(shrinkTowards = Alignment.Top),
|
||||||
) {
|
) {
|
||||||
// Fixed-height meta row so schedule ↔ time never shifts layout.
|
// Fixed-height meta row so timer ↔ time never shifts layout.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.height(16.dp),
|
modifier = Modifier.height(16.dp),
|
||||||
contentAlignment = Alignment.CenterEnd,
|
contentAlignment = Alignment.CenterEnd,
|
||||||
) {
|
) {
|
||||||
|
if (sendFailed) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (showScheduleIcon) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Schedule,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(14.dp),
|
|
||||||
tint = metaColor,
|
|
||||||
)
|
|
||||||
} else if (sendFailed) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.ErrorOutline,
|
imageVector = Icons.Rounded.ErrorOutline,
|
||||||
contentDescription = sendFailedLabel,
|
contentDescription = sendFailedLabel,
|
||||||
@@ -899,15 +920,31 @@ fun MessageItem(
|
|||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = metaColor,
|
color = metaColor,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Timer,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(14.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = sendingMetaAlpha.value
|
||||||
|
},
|
||||||
|
tint = metaColor,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.graphicsLayer {
|
||||||
|
alpha = deliveredTimeAlpha.value
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = formattedTime,
|
text = formattedTime,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = metaColor,
|
color = metaColor,
|
||||||
)
|
)
|
||||||
}
|
if (message.is_edited) {
|
||||||
if (message.is_edited && !showScheduleIcon) {
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = editedSuffix,
|
text = editedSuffix,
|
||||||
@@ -927,6 +964,7 @@ fun MessageItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageReplyQuote(
|
private fun MessageReplyQuote(
|
||||||
|
|||||||
Reference in New Issue
Block a user