diff --git a/app/src/main/java/app/closer/ui/messages/components/ChatComponents.kt b/app/src/main/java/app/closer/ui/messages/components/ChatComponents.kt index 95220f43..80392649 100644 --- a/app/src/main/java/app/closer/ui/messages/components/ChatComponents.kt +++ b/app/src/main/java/app/closer/ui/messages/components/ChatComponents.kt @@ -39,6 +39,7 @@ import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -284,6 +285,8 @@ private fun EncryptedVoiceMessage( var tempFile by remember { mutableStateOf(null) } var playing by remember { mutableStateOf(false) } var loading by remember { mutableStateOf(false) } + var positionMs by remember { mutableLongStateOf(0L) } + val totalMs = durationMs.coerceAtLeast(1L) DisposableEffect(Unit) { onDispose { @@ -292,6 +295,14 @@ private fun EncryptedVoiceMessage( } } + // Poll the player position so the progress bar advances during playback. + LaunchedEffect(playing) { + while (playing) { + positionMs = player?.currentPosition?.toLong() ?: positionMs + delay(100) + } + } + fun toggle() { val p = player if (p != null) { @@ -310,7 +321,7 @@ private fun EncryptedVoiceMessage( val mp = MediaPlayer() runCatching { mp.setDataSource(f.absolutePath) - mp.setOnCompletionListener { playing = false; runCatching { mp.seekTo(0) } } + mp.setOnCompletionListener { playing = false; positionMs = 0L; runCatching { mp.seekTo(0) } } mp.prepare() mp.start() }.onSuccess { player = mp; playing = true }.onFailure { mp.release() } @@ -340,8 +351,17 @@ private fun EncryptedVoiceMessage( ) } } - Icon(Icons.Filled.Mic, contentDescription = null, tint = tint.copy(alpha = 0.7f), modifier = Modifier.size(16.dp)) - Text(text = formatDuration(durationMs), style = MaterialTheme.typography.bodyMedium, color = tint) + LinearProgressIndicator( + progress = { if (totalMs > 0) (positionMs.toFloat() / totalMs).coerceIn(0f, 1f) else 0f }, + modifier = Modifier.width(92.dp), + color = tint, + trackColor = tint.copy(alpha = 0.25f) + ) + Text( + text = formatDuration(if (playing || positionMs > 0) positionMs else durationMs), + style = MaterialTheme.typography.bodyMedium, + color = tint + ) } } } @@ -464,7 +484,11 @@ fun ChatComposer( ) { granted: Boolean -> if (granted) startRecording() } LaunchedEffect(isRecording) { - while (isRecording) { elapsedMs = System.currentTimeMillis() - recordStart; delay(200) } + while (isRecording) { + elapsedMs = System.currentTimeMillis() - recordStart + if (elapsedMs >= MAX_RECORDING_MS) { finishRecording(send = true); break } // auto-send at the cap + delay(200) + } } DisposableEffect(Unit) { onDispose { runCatching { recorder.value?.release() }; recordFile.value?.delete() } @@ -549,6 +573,9 @@ fun ChatComposer( } } +/** Cap voice notes at 2 minutes; recording auto-stops and sends at this length. */ +private const val MAX_RECORDING_MS = 120_000L + private fun formatDuration(ms: Long): String { val totalSec = (ms / 1000).toInt() val m = totalSec / 60