From 0d923b5caac0ecbbf941515d6a786c52892dcfd3 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 4 Jan 2026 12:19:00 +0100 Subject: [PATCH 1/3] optimise imports --- .../main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt | 1 - .../vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt | 2 -- .../kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt | 1 - .../com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt | 1 - 4 files changed, 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index 0473c743b..5e7ed2ff6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl typealias SincePerRelayMap = MutableMap diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 45e797cb4..d4ab144f6 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -30,8 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.core.isAddressable import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where import com.vitorpamplona.quartz.utils.EventFactory -import kotlin.collections.component1 -import kotlin.collections.component2 class QueryBuilder( val fts: FullTextSearchModule, diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt index 26f0ed41d..cd27fd9ad 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt @@ -24,7 +24,6 @@ import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.refTo import platform.Security.SecRandomCopyBytes import platform.Security.kSecRandomDefault -import kotlin.random.Random.Default.nextBytes actual class SecureRandom { actual fun nextInt(): Int { diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt index a6ca2615b..77c56012a 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt @@ -59,7 +59,6 @@ import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorSerializer -import kotlinx.serialization.json.JsonNull.content import java.io.InputStream class JacksonMapper { From a4925de4a19208641c24114a80d7322b9d45ca39 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 4 Jan 2026 12:27:07 +0100 Subject: [PATCH 2/3] cleaner code --- .../java/com/vitorpamplona/amethyst/LocalPreferences.kt | 6 ++---- .../ui/screen/loggedIn/home/ShortNotePostViewModel.kt | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index d2678ca68..18916ef15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -248,10 +248,8 @@ object LocalPreferences { withContext(Dispatchers.IO) { val prefsDir = File(prefsDirPath) prefsDir.list()?.forEach { - if (it.contains(npub)) { - if (!File(prefsDir, it).delete()) { - Log.w("LocalPreferences", "Failed to delete preference file: $it") - } + if (it.contains(npub) && !File(prefsDir, it).delete()) { + Log.w("LocalPreferences", "Failed to delete preference file: $it") } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 652cc425f..cd479b134 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -972,7 +972,6 @@ open class ShortNotePostViewModel : val uploadErrorTitle = stringRes(appContext, R.string.upload_error_title) val uploadVoiceNip95NotSupported = stringRes(appContext, R.string.upload_error_voice_message_nip95_not_supported) val uploadVoiceFailed = stringRes(appContext, R.string.upload_error_voice_message_failed) - val uploadVoiceUnexpected = stringRes(appContext, R.string.upload_error_voice_message_unexpected_state) val uploadVoiceExceptionMessage: (String) -> String = { detail -> stringRes(appContext, R.string.upload_error_voice_message_exception, detail) } From 59fb17563715b8b295a9f6100336d5fd3b898797 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 4 Jan 2026 12:32:08 +0100 Subject: [PATCH 3/3] refactor VoiceMessagePreview.kt --- .../ui/actions/uploads/VoiceMessagePreview.kt | 229 +++++++++++------- 1 file changed, 135 insertions(+), 94 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt index a601837be..7633a47d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt @@ -74,64 +74,23 @@ fun VoiceMessagePreview( var progress by remember { mutableFloatStateOf(0f) } var mediaPlayer by remember { mutableStateOf(null) } - // Initialize MediaPlayer - DisposableEffect(voiceMetadata.url, localFile) { - val player = createMediaPlayer(voiceMetadata.url, localFile) - player?.setOnCompletionListener { + ManageMediaPlayer( + voiceMetadata = voiceMetadata, + localFile = localFile, + onCompletion = { isPlaying = false progress = 0f - } - mediaPlayer = player + }, + onPlayerChanged = { mediaPlayer = it }, + onRelease = { isPlaying = false }, + ) - onDispose { - // Stop playback and clean up - try { - player?.stop() - } catch (e: IllegalStateException) { - // Player might already be stopped - Log.d("VoiceMessagePreview", "MediaPlayer stop failed (already stopped)", e) - } - player?.release() - mediaPlayer = null - isPlaying = false - } - } - - // Update progress while playing - LaunchedEffect(mediaPlayer, isPlaying) { - // Capture player reference to avoid reading volatile state repeatedly - val player = mediaPlayer - if (player != null && isPlaying) { - while (isActive) { - try { - if (player.isPlaying) { - val current = player.currentPosition.toFloat() - val duration = player.duration.toFloat() - // Validate values before calculating progress - val newProgress = - if (duration > 0 && current >= 0) { - (current / duration).coerceIn(0f, 1f) - } else { - 0f - } - // Only update if value is valid (not NaN or Infinity) - if (newProgress.isFinite()) { - progress = newProgress - } - } else { - // Player stopped, exit loop and let LaunchedEffect restart - break - } - } catch (e: IllegalStateException) { - // Player in invalid state, stop tracking - Log.w("VoiceMessagePreview", "MediaPlayer in invalid state during progress tracking", e) - isPlaying = false - break - } - delay(100) - } - } - } + TrackPlaybackProgress( + mediaPlayer = mediaPlayer, + isPlaying = isPlaying, + onProgressUpdate = { progress = it }, + onInvalidState = { isPlaying = false }, + ) Box( modifier = @@ -150,27 +109,13 @@ fun VoiceMessagePreview( // Play/Pause Button IconButton( onClick = { - val player = mediaPlayer - if (player != null) { - try { - if (isPlaying) { - player.pause() - isPlaying = false - } else { - // Validate progress before comparison - if (progress.isFinite() && progress >= 1f) { - player.seekTo(0) - progress = 0f - } - player.start() - isPlaying = true - } - } catch (e: IllegalStateException) { - // MediaPlayer in invalid state, ignore - Log.w("VoiceMessagePreview", "MediaPlayer operation failed in onClick handler", e) - isPlaying = false - } - } + handlePlayPauseClick( + mediaPlayer = mediaPlayer, + isPlaying = isPlaying, + progress = progress, + onProgressReset = { progress = 0f }, + onPlayingChanged = { isPlaying = it }, + ) }, modifier = Modifier.size(48.dp), ) { @@ -194,24 +139,11 @@ fun VoiceMessagePreview( waveformBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant)), progressBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.primary)), onProgressChange = { newProgress -> - // Validate incoming progress value - if (newProgress.isFinite() && newProgress >= 0f && newProgress <= 1f) { - val player = mediaPlayer - if (player != null) { - try { - val duration = player.duration - // Only seek if duration is valid - if (duration > 0) { - val newPosition = (newProgress * duration).toInt() - player.seekTo(newPosition) - progress = newProgress - } - } catch (e: IllegalStateException) { - // MediaPlayer in invalid state, ignore - Log.w("VoiceMessagePreview", "MediaPlayer seek failed in onProgressChange", e) - } - } - } + handleWaveformScrub( + newProgress = newProgress, + mediaPlayer = mediaPlayer, + onProgressChanged = { progress = it }, + ) }, ) @@ -240,6 +172,115 @@ fun VoiceMessagePreview( } } +@Composable +private fun ManageMediaPlayer( + voiceMetadata: AudioMeta, + localFile: File?, + onCompletion: () -> Unit, + onPlayerChanged: (MediaPlayer?) -> Unit, + onRelease: () -> Unit, +) { + DisposableEffect(voiceMetadata.url, localFile) { + val player = createMediaPlayer(voiceMetadata.url, localFile) + player?.setOnCompletionListener { onCompletion() } + onPlayerChanged(player) + + onDispose { + try { + player?.stop() + } catch (e: IllegalStateException) { + Log.d("VoiceMessagePreview", "MediaPlayer stop failed (already stopped)", e) + } + player?.release() + onPlayerChanged(null) + onRelease() + } + } +} + +@Composable +private fun TrackPlaybackProgress( + mediaPlayer: MediaPlayer?, + isPlaying: Boolean, + onProgressUpdate: (Float) -> Unit, + onInvalidState: () -> Unit, +) { + LaunchedEffect(mediaPlayer, isPlaying) { + val player = mediaPlayer ?: return@LaunchedEffect + if (!isPlaying) return@LaunchedEffect + + while (isActive) { + try { + if (!player.isPlaying) break + calculateProgress(player.currentPosition.toFloat(), player.duration.toFloat())?.let { onProgressUpdate(it) } + } catch (e: IllegalStateException) { + Log.w("VoiceMessagePreview", "MediaPlayer in invalid state during progress tracking", e) + onInvalidState() + break + } + delay(100) + } + } +} + +private fun calculateProgress( + current: Float, + duration: Float, +): Float? { + val progress = + if (duration > 0 && current >= 0) { + (current / duration).coerceIn(0f, 1f) + } else { + 0f + } + return progress.takeIf { it.isFinite() } +} + +private fun handlePlayPauseClick( + mediaPlayer: MediaPlayer?, + isPlaying: Boolean, + progress: Float, + onProgressReset: () -> Unit, + onPlayingChanged: (Boolean) -> Unit, +) { + val player = mediaPlayer ?: return + try { + if (isPlaying) { + player.pause() + onPlayingChanged(false) + return + } + if (progress.isFinite() && progress >= 1f) { + player.seekTo(0) + onProgressReset() + } + player.start() + onPlayingChanged(true) + } catch (e: IllegalStateException) { + Log.w("VoiceMessagePreview", "MediaPlayer operation failed in onClick handler", e) + onPlayingChanged(false) + } +} + +private fun handleWaveformScrub( + newProgress: Float, + mediaPlayer: MediaPlayer?, + onProgressChanged: (Float) -> Unit, +) { + if (!newProgress.isFinite() || newProgress < 0f || newProgress > 1f) return + val player = mediaPlayer ?: return + try { + val duration = player.duration + if (duration > 0) { + val newPosition = (newProgress * duration).toInt() + player.seekTo(newPosition) + onProgressChanged(newProgress) + } + } catch (e: IllegalStateException) { + Log.w("VoiceMessagePreview", "MediaPlayer seek failed in onProgressChange", e) + } +} + private fun createMediaPlayer( url: String, localFile: File?,