mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 22:05:26 +00:00
improve animation
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user