improve animation

This commit is contained in:
callebtc
2025-08-30 15:53:07 +02:00
parent d92a4cd20b
commit 0566ced951
@@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.random.Random import kotlin.random.Random
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
/** /**
* Matrix-style encryption animation for messages during PoW computation * Matrix-style encryption animation for messages during PoW computation
@@ -264,7 +265,7 @@ fun MessageWithMatrixAnimation(
/** /**
* Display message with proper formatting but animated content * Display message with proper formatting but animated content
* Reuses the same styling logic as formatMessageAsAnnotatedString * Uses IDENTICAL layout structure as normal message for pixel-perfect alignment
*/ */
@Composable @Composable
private fun AnimatedMessageDisplay( private fun AnimatedMessageDisplay(
@@ -275,109 +276,115 @@ private fun AnimatedMessageDisplay(
timeFormatter: java.text.SimpleDateFormat, timeFormatter: java.text.SimpleDateFormat,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f // Get the animated content text
var animatedContent by remember(message.content) { mutableStateOf(message.content) }
val isAnimating = shouldAnimateMessage(message.id)
// Determine if this message was sent by self // Update animated content when animation state changes
val isSelf = message.senderPeerID == meshService.myPeerID || LaunchedEffect(isAnimating, message.content) {
message.sender == currentUserNickname || if (isAnimating && message.content.isNotEmpty()) {
message.sender.startsWith("$currentUserNickname#") // Start with encrypted content
val encryptedChars = "!@#$%^&*()_+-=[]{}|;:,.<>?~`".toCharArray()
if (message.sender != "system") { // Animate each character gradually
// Get base color for this peer (same logic as formatMessageAsAnnotatedString) message.content.forEachIndexed { index, targetChar ->
val baseColor = if (isSelf) { launch {
Color(0xFFFF9500) // Orange for self (iOS orange) // Stagger character animations
} else { delay(index * 50L)
getPeerColor(message, isDark)
}
// Split sender into base name and hashtag suffix val animationDuration = Random.nextLong(1000, 3000)
val (baseName, suffix) = splitSuffix(message.sender) val startTime = System.currentTimeMillis()
// Build the message with animated content // Phase 1: Random encrypted characters
Row( while (System.currentTimeMillis() - startTime < animationDuration) {
modifier = modifier, val randomChar = encryptedChars[Random.nextInt(encryptedChars.size)]
verticalAlignment = androidx.compose.ui.Alignment.Top val currentText = animatedContent.toCharArray()
) { if (index < currentText.size) {
// Sender portion: <@nickname> currentText[index] = randomChar
Text( animatedContent = String(currentText)
text = buildAnnotatedString {
withStyle(SpanStyle(
color = baseColor,
fontSize = 15.sp, // Use BASE_FONT_SIZE directly
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
)) {
append("<@")
append(truncateNickname(baseName))
if (suffix.isNotEmpty()) {
withStyle(SpanStyle(color = baseColor.copy(alpha = 0.6f))) {
append(suffix)
}
} }
append("> ") delay(100)
} }
},
fontFamily = FontFamily.Monospace
)
// Animated content portion (no timestamp during animation) // Phase 2: Reveal final character
AnimatedMatrixText( val finalText = animatedContent.toCharArray()
targetText = message.content, if (index < finalText.size) {
isAnimating = true, finalText[index] = targetChar
color = baseColor, animatedContent = String(finalText)
fontSize = 15.sp, // Use BASE_FONT_SIZE directly (15sp) }
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal, }
modifier = Modifier.weight(1f) }
) } else {
} // Not animating, show final content
} else { animatedContent = message.content
// System message with animation (no timestamp during animation)
Row(
modifier = modifier,
verticalAlignment = androidx.compose.ui.Alignment.Top
) {
Text(
text = "* ",
color = Color.Gray,
fontSize = 13.sp, // (BASE_FONT_SIZE - 2).sp = 13sp
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic,
fontFamily = FontFamily.Monospace
)
AnimatedMatrixText(
targetText = message.content,
isAnimating = true,
color = Color.Gray,
fontSize = 13.sp, // (BASE_FONT_SIZE - 2).sp = 13sp
modifier = Modifier.weight(1f)
)
Text(
text = " *",
color = Color.Gray,
fontSize = 13.sp, // (BASE_FONT_SIZE - 2).sp = 13sp
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic,
fontFamily = FontFamily.Monospace
)
} }
} }
// Create a temporary message with animated content for formatting
val animatedMessage = message.copy(content = animatedContent)
// Use the EXACT same formatting function as normal messages, but without timestamp during animation
val annotatedText = if (isAnimating) {
formatMessageAsAnnotatedStringWithoutTimestamp(
message = animatedMessage,
currentUserNickname = currentUserNickname,
meshService = meshService,
colorScheme = colorScheme
)
} else {
formatMessageAsAnnotatedString(
message = animatedMessage,
currentUserNickname = currentUserNickname,
meshService = meshService,
colorScheme = colorScheme,
timeFormatter = timeFormatter
)
}
// Use IDENTICAL Text composable structure as normal message
Text(
text = annotatedText,
modifier = modifier,
fontFamily = FontFamily.Monospace,
softWrap = true
)
} }
/** /**
* Find the start index of the message content (after timestamp and sender) * Format message without timestamp for animation phase
* Identical to formatMessageAsAnnotatedString but excludes timestamp
*/ */
private fun findContentStartIndex(messageText: String): Int { private fun formatMessageAsAnnotatedStringWithoutTimestamp(
// Look for pattern like "] <@sender> " to find where content starts message: com.bitchat.android.model.BitchatMessage,
val pattern = """] <[^>]+> """.toRegex() currentUserNickname: String,
val match = pattern.find(messageText) meshService: com.bitchat.android.mesh.BluetoothMeshService,
return match?.range?.last?.plus(1) ?: -1 colorScheme: androidx.compose.material3.ColorScheme
} ): AnnotatedString {
// Simply call the main formatting function with a dummy formatter,
// then remove the timestamp part
val timeFormatter = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault())
val fullText = formatMessageAsAnnotatedString(
message = message,
currentUserNickname = currentUserNickname,
meshService = meshService,
colorScheme = colorScheme,
timeFormatter = timeFormatter
)
/** // Find and remove the timestamp at the end
* Find the end index of the message content (before final timestamp) val text = fullText.text
*/ val timestampPattern = """ \[\d{2}:\d{2}:\d{2}]$""".toRegex()
private fun findContentEndIndex(messageText: String): Int { val match = timestampPattern.find(text)
// Look for pattern like " [HH:mm:ss]" at the end
val pattern = """ \[\d{2}:\d{2}:\d{2}]$""".toRegex() return if (match != null) {
val match = pattern.find(messageText) // Remove timestamp portion
return match?.range?.first ?: messageText.length val endIndex = match.range.first
AnnotatedString(
text = text.substring(0, endIndex),
spanStyles = fullText.spanStyles.filter { it.end <= endIndex },
paragraphStyles = fullText.paragraphStyles.filter { it.end <= endIndex }
)
} else {
fullText
}
} }