From 5ce4391db4505a1e224bee3934469e1727e3c194 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 10:29:06 +0100 Subject: [PATCH 01/14] guard encoder / decoder .stop --- .../amethyst/ui/actions/uploads/VoiceAnonymizer.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 391b79349..c8742fdb8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -196,7 +196,11 @@ class VoiceAnonymizer { return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration) } finally { - decoder?.stop() + try { + decoder?.stop() + } catch (_: IllegalStateException) { + // Decoder was never started + } decoder?.release() extractor.release() } @@ -400,7 +404,11 @@ class VoiceAnonymizer { } } } finally { - encoder.stop() + try { + encoder.stop() + } catch (_: IllegalStateException) { + // Encoder was never started + } encoder.release() if (muxerStarted) { muxer.stop() From c6d437b98e367ae5f65c4744a6b41b255415a45e Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 10:47:03 +0100 Subject: [PATCH 02/14] confirmed: <1 results in Higher pitch. Adjusted NEUTRAL to slightly lower pitch --- .../amethyst/ui/actions/uploads/VoiceAnonymizationController.kt | 1 + .../vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt index b720e20d8..33adff8b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -61,6 +61,7 @@ class VoiceAnonymizationController( preset: VoicePreset, originalFile: File?, ) { + Log.d(logTag, "selectPreset called with: ${preset.name}, pitchFactor: ${preset.pitchFactor}") if (processingPreset != null || preset == selectedPreset) return if (preset == VoicePreset.NONE) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt index 49544e51a..b6c67d244 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt @@ -29,5 +29,5 @@ enum class VoicePreset( NONE(1.0, R.string.voice_preset_none), DEEP(1.4, R.string.voice_preset_deep), HIGH(0.75, R.string.voice_preset_high), - NEUTRAL(0.9, R.string.voice_preset_neutral), + NEUTRAL(1.1, R.string.voice_preset_neutral), } From 708c6da9fe85afabe1c2aac6822d38aa3646f0ab Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 10:52:58 +0100 Subject: [PATCH 03/14] add docs --- .../ui/actions/uploads/VoiceAnonymizer.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index c8742fdb8..7240c87ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -41,12 +41,26 @@ import java.io.File import java.nio.ByteOrder import kotlin.math.abs +/** + * Result of voice anonymization processing. + * + * @property file The output audio file (AAC in MP4 container) + * @property waveform Amplitude data for waveform visualization (one value per second) + * @property duration Audio duration in seconds + */ data class AnonymizedResult( val file: File, val waveform: List, val duration: Int, ) +/** + * Processes audio files to alter voice characteristics for privacy. + * + * Uses TarsosDSP's WSOLA (Waveform Similarity Overlap-Add) algorithm combined with + * rate transposition to shift pitch while preserving duration. Note that in TarsosDSP, + * pitch factors work inversely: factor < 1 raises pitch, factor > 1 lowers pitch. + */ class VoiceAnonymizer { companion object { private const val TAG = "VoiceAnonymizer" @@ -54,6 +68,20 @@ class VoiceAnonymizer { private const val BIT_RATE = 128000 } + /** + * Applies voice anonymization to an audio file. + * + * The process involves three stages: + * 1. Decode input audio to PCM (0-30% progress) + * 2. Apply pitch shifting with TarsosDSP (30-70% progress) + * 3. Encode processed audio to AAC (70-100% progress) + * + * @param inputFile Source audio file (supports formats decodable by MediaCodec) + * @param preset Voice transformation preset (NONE is not allowed) + * @param onProgress Callback invoked with progress value from 0.0 to 1.0 + * @return [Result.success] with [AnonymizedResult] containing the output file, + * waveform data, and duration; or [Result.failure] with the exception + */ suspend fun anonymize( inputFile: File, preset: VoicePreset, From 001dce8fef74644d8fbacbb46cb024743565817b Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 10:56:28 +0100 Subject: [PATCH 04/14] guard for null inputFile.parentFile --- .../amethyst/ui/actions/uploads/VoiceAnonymizer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 7240c87ea..db831a2ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -126,7 +126,8 @@ class VoiceAnonymizer { ): File { val baseName = inputFile.nameWithoutExtension val presetSuffix = preset.name.lowercase() - return File(inputFile.parentFile, "${baseName}_$presetSuffix.mp4") + val parentDir = inputFile.parentFile ?: inputFile.absoluteFile.parentFile + return File(parentDir, "${baseName}_$presetSuffix.mp4") } private data class DecodedAudio( From 0b233f3a3f4a347dcade1183337d374e9b11496d Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 11:00:42 +0100 Subject: [PATCH 05/14] use colorScheme.onSurfaceVariant instead of Gray --- .../amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt index 3b7f02028..ad4cce69d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -63,7 +62,7 @@ fun VoiceAnonymizationSection( Text( text = stringRes(R.string.voice_anonymize_description), style = MaterialTheme.typography.bodySmall, - color = Color.Gray, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 3, overflow = TextOverflow.Ellipsis, ) From 50b0e8bd17e90c21056fadc8859496bf220c8a17 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 11:33:30 +0100 Subject: [PATCH 06/14] Moved processingPreset = preset to execute synchronously before launching the coroutine --- .../amethyst/ui/actions/uploads/VoiceAnonymizationController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt index 33adff8b6..fdb54523e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -77,9 +77,9 @@ class VoiceAnonymizationController( val file = originalFile ?: return processingJob?.cancel() + processingPreset = preset processingJob = scope.launch { - processingPreset = preset try { val anonymizer = VoiceAnonymizer() val result = anonymizer.anonymize(file, preset) From db75a7c910c030fc15bd749ac11c9d8bcc3eeb98 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 11:40:38 +0100 Subject: [PATCH 07/14] preallocate list capacity --- .../amethyst/ui/actions/uploads/VoiceAnonymizer.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index db831a2ae..71838a176 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -172,7 +172,8 @@ class VoiceAnonymizer { decoder.configure(format, null, null, 0) decoder.start() - val pcmSamples = mutableListOf() + val estimatedSamples = (sampleRate.toLong() * durationUs / 1_000_000).toInt() + val pcmSamples = ArrayList(estimatedSamples) val bufferInfo = MediaCodec.BufferInfo() var inputDone = false var outputDone = false @@ -251,8 +252,8 @@ class VoiceAnonymizer { } else -> baseFactor } - val processedSamples = mutableListOf() val totalSamples = pcmData.size + val processedSamples = ArrayList(totalSamples) val wsola = WaveformSimilarityBasedOverlapAdd( From 5be16338438cfdd61a2bda46192006d3d012a3fd Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 12:59:25 +0100 Subject: [PATCH 08/14] limit voice recording to 180s --- .../amethyst/ui/actions/uploads/RecordAudio.kt | 7 +++++++ .../amethyst/ui/actions/uploads/RecordVoiceButton.kt | 6 +++++- .../com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 2 ++ .../ui/screen/loggedIn/home/ShortNotePostScreen.kt | 2 ++ .../amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt | 2 ++ 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt index 32af08e0b..c7fe883aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt @@ -42,11 +42,14 @@ import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.delay import kotlinx.coroutines.isActive +const val MAX_VOICE_RECORD_SECONDS = 180 + @OptIn(ExperimentalPermissionsApi::class) @Composable fun RecordAudioBox( modifier: Modifier, onRecordTaken: (RecordingResult) -> Unit, + maxDurationSeconds: Int? = null, content: @Composable (Boolean, Int) -> Unit, ) { val mediaRecorder = remember { mutableStateOf(null) } @@ -107,6 +110,10 @@ fun RecordAudioBox( while (isActive) { delay(1000) elapsedSeconds++ + if (maxDurationSeconds != null && elapsedSeconds >= maxDurationSeconds) { + stopRecording() + break + } } } else { // Reset elapsed time when not recording diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt index 9cedeeab6..5d9fd487f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt @@ -42,7 +42,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.stringRes @Composable -fun RecordVoiceButton(onVoiceTaken: (RecordingResult) -> Unit) { +fun RecordVoiceButton( + onVoiceTaken: (RecordingResult) -> Unit, + maxDurationSeconds: Int? = null, +) { var isRecording by remember { mutableStateOf(false) } var elapsedSeconds by remember { mutableIntStateOf(0) } @@ -61,6 +64,7 @@ fun RecordVoiceButton(onVoiceTaken: (RecordingResult) -> Unit) { elapsedSeconds = 0 onVoiceTaken(recording) }, + maxDurationSeconds = maxDurationSeconds, ) { recordingState, elapsed -> // Update parent state after composition completes SideEffect { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index a2814a464..ffcb0a2d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -113,6 +113,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.uploads.FloatingRecordingIndicator +import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.ClickableBox @@ -635,6 +636,7 @@ fun ReplyViaVoiceReaction( ) } }, + maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, ) { isRecording, elapsedSeconds -> if (voiceRecordingState != null) { SideEffect { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 4318f13c8..e61651c12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -497,6 +498,7 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { onVoiceTaken = { recording -> postViewModel.selectVoiceRecording(recording) }, + maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, ) if (postViewModel.canUsePoll) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index a2573663b..7eeac6f77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow +import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection @@ -226,6 +227,7 @@ private fun ReRecordButton(viewModel: VoiceReplyViewModel) { onRecordTaken = { recording -> viewModel.selectRecording(recording) }, + maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, ) { isRecording, elapsedSeconds -> val contentColor = if (isRecording) { From 76a05f7d71e3efe42f4861bdc5113a8bc5caddf7 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 13:56:49 +0100 Subject: [PATCH 09/14] moved re-record button inline and added to VoiceMessagePreview.kt --- .../ui/actions/uploads/VoiceMessagePreview.kt | 202 +++++++++++++----- .../loggedIn/home/ShortNotePostScreen.kt | 2 + .../screen/loggedIn/home/VoiceReplyScreen.kt | 98 +-------- 3 files changed, 148 insertions(+), 154 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 7633a47d8..40f60124e 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 @@ -35,8 +35,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -67,6 +69,8 @@ fun VoiceMessagePreview( voiceMetadata: AudioMeta, localFile: File? = null, onRemove: () -> Unit, + onReRecord: ((RecordingResult) -> Unit)? = null, + isUploading: Boolean = false, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -101,77 +105,159 @@ fun VoiceMessagePreview( shape = RoundedCornerShape(8.dp), ).padding(12.dp), ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - // Play/Pause Button - IconButton( - onClick = { - handlePlayPauseClick( - mediaPlayer = mediaPlayer, - isPlaying = isPlaying, - progress = progress, - onProgressReset = { progress = 0f }, - onPlayingChanged = { isPlaying = it }, - ) - }, - modifier = Modifier.size(48.dp), + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = if (isPlaying) stringRes(context, R.string.pause) else stringRes(context, R.string.play), - tint = MaterialTheme.colorScheme.primary, - ) - } - - Spacer(modifier = Modifier.width(8.dp)) - - // Waveform and Duration - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - ) { - AudioWaveformReadOnly( - amplitudes = voiceMetadata.waveform ?: emptyList(), - progress = progress, - waveformBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant)), - progressBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.primary)), - onProgressChange = { newProgress -> - handleWaveformScrub( - newProgress = newProgress, + // Play/Pause Button + IconButton( + onClick = { + handlePlayPauseClick( mediaPlayer = mediaPlayer, - onProgressChanged = { progress = it }, + isPlaying = isPlaying, + progress = progress, + onProgressReset = { progress = 0f }, + onPlayingChanged = { isPlaying = it }, ) }, - ) + modifier = Modifier.size(48.dp), + ) { + Icon( + imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, + contentDescription = if (isPlaying) stringRes(context, R.string.pause) else stringRes(context, R.string.play), + tint = MaterialTheme.colorScheme.primary, + ) + } - Text( - text = formatSecondsToTime(voiceMetadata.duration ?: 0), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 4.dp), - ) + Spacer(modifier = Modifier.width(8.dp)) + + // Waveform and Duration + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + AudioWaveformReadOnly( + amplitudes = voiceMetadata.waveform ?: emptyList(), + progress = progress, + waveformBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant)), + progressBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.primary)), + onProgressChange = { newProgress -> + handleWaveformScrub( + newProgress = newProgress, + mediaPlayer = mediaPlayer, + onProgressChanged = { progress = it }, + ) + }, + ) + + Text( + text = formatSecondsToTime(voiceMetadata.duration ?: 0), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp), + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Remove Button + IconButton( + onClick = onRemove, + modifier = Modifier.size(48.dp), + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringRes(context, R.string.remove), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - Spacer(modifier = Modifier.width(8.dp)) - - // Remove Button - IconButton( - onClick = onRemove, - modifier = Modifier.size(48.dp), - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringRes(context, R.string.remove), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + if (onReRecord != null) { + Spacer(modifier = Modifier.size(8.dp)) + ReRecordButton( + isUploading = isUploading, + isPlaying = isPlaying, + onRecordTaken = onReRecord, ) } } } } +@Composable +private fun ReRecordButton( + isUploading: Boolean, + isPlaying: Boolean, + onRecordTaken: (RecordingResult) -> Unit, +) { + if (isUploading || isPlaying) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Mic, + contentDescription = stringRes(id = R.string.record_a_message), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = stringRes(id = R.string.re_record), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + return + } + + RecordAudioBox( + modifier = Modifier, + onRecordTaken = onRecordTaken, + maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, + ) { isRecording, elapsedSeconds -> + val contentColor = + if (isRecording) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + val icon = + if (isRecording) { + Icons.Default.Stop + } else { + Icons.Default.Mic + } + val label = + if (isRecording) { + formatSecondsToTime(elapsedSeconds) + } else { + stringRes(id = R.string.re_record) + } + val iconDescription = + if (isRecording) { + stringRes(id = R.string.recording_indicator_description) + } else { + stringRes(id = R.string.record_a_message) + } + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = icon, + contentDescription = iconDescription, + tint = contentColor, + ) + Text( + text = label, + color = contentColor, + ) + } + } +} + @Composable private fun ManageMediaPlayer( voiceMetadata: AudioMeta, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index e61651c12..99f6d42e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -375,6 +375,8 @@ private fun NewPostScreenBody( VoiceMessagePreview( voiceMetadata = displayMetadata, localFile = postViewModel.activeFile, + onReRecord = { recording -> postViewModel.selectVoiceRecording(recording) }, + isUploading = postViewModel.isUploadingVoice, onRemove = { postViewModel.removeVoiceMessage() }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index 7eeac6f77..5baff1a94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -21,47 +21,32 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Mic -import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow -import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -104,9 +89,6 @@ fun VoiceReplyScreen( }, ) }, - bottomBar = { - ReRecordButton(viewModel) - }, ) { pad -> Surface( modifier = @@ -162,6 +144,8 @@ private fun VoiceReplyScreenBody( VoiceMessagePreview( voiceMetadata = displayMetadata, localFile = viewModel.activeFile, + onReRecord = { recording -> viewModel.selectRecording(recording) }, + isUploading = viewModel.isUploading, onRemove = { viewModel.cancel() nav.popBack() @@ -193,81 +177,3 @@ private fun VoiceReplyScreenBody( Spacer(modifier = Modifier.height(80.dp)) } } - -@Composable -private fun ReRecordButton(viewModel: VoiceReplyViewModel) { - Column( - modifier = - Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(vertical = 16.dp, horizontal = Size10dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (viewModel.isUploading) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - imageVector = Icons.Default.Mic, - contentDescription = stringRes(id = R.string.record_a_message), - tint = MaterialTheme.colorScheme.onBackground, - ) - Text( - text = stringRes(id = R.string.re_record), - color = MaterialTheme.colorScheme.onBackground, - ) - } - return - } - - RecordAudioBox( - modifier = Modifier, - onRecordTaken = { recording -> - viewModel.selectRecording(recording) - }, - maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, - ) { isRecording, elapsedSeconds -> - val contentColor = - if (isRecording) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onBackground - } - val icon = - if (isRecording) { - Icons.Default.Stop - } else { - Icons.Default.Mic - } - val label = - if (isRecording) { - formatSecondsToTime(elapsedSeconds) - } else { - stringRes(id = R.string.re_record) - } - val iconDescription = - if (isRecording) { - stringRes(id = R.string.recording_indicator_description) - } else { - stringRes(id = R.string.record_a_message) - } - Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - imageVector = icon, - contentDescription = iconDescription, - tint = contentColor, - ) - Text( - text = label, - color = contentColor, - ) - } - } - } -} From 835fbb90b7da16175aa91803c3aeccd373e42097 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 14:00:13 +0100 Subject: [PATCH 10/14] Do something with the "Boolean" value returned by "delete". --- .../ui/actions/uploads/VoiceAnonymizationController.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt index fdb54523e..e154f0a27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -109,8 +109,11 @@ class VoiceAnonymizationController( distortedFiles.values.forEach { result -> try { if (result.file.exists()) { - result.file.delete() - Log.d(logTag, "Deleted distorted file: ${result.file.absolutePath}") + if (result.file.delete()) { + Log.d(logTag, "Deleted distorted file: ${result.file.absolutePath}") + } else { + Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}") + } } } catch (e: Exception) { Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}", e) From 40d694343909b760b77a9a562878c50631525e55 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 14:04:59 +0100 Subject: [PATCH 11/14] sonar fixes --- .../ui/actions/uploads/VoiceAnonymizer.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 71838a176..8c4210bc4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -158,9 +158,7 @@ class VoiceAnonymizer { } } - if (audioTrackIndex == -1 || format == null) { - throw IllegalStateException("No audio track found in file") - } + check(audioTrackIndex != -1 && format != null) { "No audio track found in file" } extractor.selectTrack(audioTrackIndex) val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm" @@ -286,7 +284,9 @@ class VoiceAnonymizer { return true } - override fun processingFinished() {} + override fun processingFinished() { + // No-op: no cleanup needed + } } val dispatcher = @@ -310,7 +310,9 @@ class VoiceAnonymizer { return true } - override fun processingFinished() {} + override fun processingFinished() { + // No-op: no cleanup needed + } } dispatcher.addAudioProcessor(progressProcessor) @@ -484,5 +486,7 @@ private class FloatArrayAudioInputStream( return actualSkip.toLong() * 2 } - override fun close() {} + override fun close() { + // No-op: no cleanup needed + } } From 505a6583ee32dec83a146f6f79c42bedda325952 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 14:20:05 +0100 Subject: [PATCH 12/14] remove unused imports --- .../main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt | 2 -- .../amethyst/commons/model/privateChats/Chatroom.kt | 1 - .../com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt | 1 - .../vitorpamplona/amethyst/commons/richtext/RichTextParser.kt | 1 - .../amethyst/desktop/network/DesktopRelayConnectionManager.kt | 1 - .../kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt | 1 - 6 files changed, 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt index 557e26c46..2b4bb2fe2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.types -import android.R.attr.label -import android.R.attr.maxLines import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt index 5485bf5bf..dbf99854d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt @@ -34,7 +34,6 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import java.lang.ref.WeakReference -import kotlin.collections.plus @Stable class Chatroom : NotesGatherer { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt index b588ea572..428428bce 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.richtext -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import java.util.Base64 object Base64Image { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 70d90061d..b5dcf22fc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -45,7 +45,6 @@ import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL import kotlin.coroutines.cancellation.CancellationException -import kotlin.text.iterator class RichTextParser { fun createMediaContent( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt index 18e7a9873..1b0a019ed 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.desktop.network -import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket /** diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt index b8ac14da8..018385c6b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -49,7 +49,6 @@ import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils -import kotlinx.serialization.json.JsonNull.content @Immutable class TextNoteEvent( From c801e9ce8678321f893e7ac0c305596da03e5eba Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 20 Jan 2026 13:34:58 +0000 Subject: [PATCH 13/14] New Crowdin translations by GitHub Action --- .../src/main/res/values-fr-rFR/strings.xml | 64 +++++++++++++++++++ .../src/main/res/values-hi-rIN/strings.xml | 7 ++ .../src/main/res/values-hu-rHU/strings.xml | 6 ++ .../src/main/res/values-zh-rCN/strings.xml | 7 ++ 4 files changed, 84 insertions(+) diff --git a/amethyst/src/main/res/values-fr-rFR/strings.xml b/amethyst/src/main/res/values-fr-rFR/strings.xml index bbdb00c1e..5ca190f7b 100644 --- a/amethyst/src/main/res/values-fr-rFR/strings.xml +++ b/amethyst/src/main/res/values-fr-rFR/strings.xml @@ -135,6 +135,8 @@ Ajouter un utilisateur Ajouter un relais Nom + Nom (pour @tagging) + Mon nom @tag Nom visible du public Mon nom visible du public Ostrich McGénial @@ -170,6 +172,12 @@ Erreur lors de l\'envoi NIP-95 n\'est pas encore supporté pour les messages vocaux Échec de l\'envoi de la voix : %1$s + Aucun + Profond + Haute + Neutre + Anonymisation + Modifie votre niveau de voix. Remarque : les changements de hauteur de base peuvent potentiellement être inversés par des auditeurs déterminés. L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats "répondre ici" Copie l\'ID de note dans le presse-papiers pour le partage sur Nostr @@ -364,6 +372,7 @@ Division manuelle de zaps Favoris Signets par défaut + Vos favoris par défaut que de nombreux clients prennent en charge Brouillons Favoris Privés Favoris Publics @@ -371,6 +380,40 @@ Ajouter aux Favoris Publics Supprimer des Favoris Privés Supprimer des Favoris Publics + Listes de favoris + Icône pour la liste de favoris + Nouvelle liste de favoris + Métadonnées de la liste de favoris + Dupliquer la liste de favoris + Diffuser la liste de favoris + Supprimer la liste de favoris + Voir les publications + Voir les articles + Voir les liens + Voir les hashtags + Vous n\'avez pas encore de liste de favoris. Appuyez sur le bouton ci-dessous pour en créer une. + Publications privées + Publications privées (%1$s) + Publications publiques + Publications publiques (%1$s) + Articles privés + Articles privés (%1$s) + Articles publics + Articles publics (%1$s) + Gérer %1$s dans les listes de favoris + Ajouter cette publication aux favoris + Ajouter cet article aux favoris + est un favori public ici + est un favori privé ici + n\'est pas un favori ici + Supprimer le favori de la liste + Ajouter un favori à la liste + Ajouter comme favori public + Ajouter comme favori privé + Supprimer de la liste de favoris + Les métadonnées des listes de favoris peuvent être vues par n\'importe qui sur Nostr. Seuls vos membres privés sont chiffrés. + Déplacer en public + Déplacer en privé Service Wallet Connect Autorise un Nostr Secret à payer des zaps sans quitter l\'application. Gardez le secret en toute sécurité et utilisez un relais privé si possible Clé publique Wallet Connect @@ -604,12 +647,23 @@ Contact NIPs supportés Frais d\'admission + Publication + Paiements %1$s URL de payments + Audience cible + Politiques & liens + Frais & paiements Limites Pays Langages Étiquettes + Sujets + Tous les pays + Toutes les langues Politique de publication + Politique de confidentialité + Termes & Conditions + N/A Erreurs et Notifications de ce Relais Longueur du message Abonnements @@ -618,9 +672,19 @@ Préfixe minimum Tags d\'événement maximum Longueur du contenu + Longueur maximale du contenu + Rejeter plus anciens que + Accepte jusqu\'à + %1$s dans le futur + Il y a %1$s + Taille du contenu + Connectivité + Contrôle d\'accès PoW minimum Authentification + Authentification requise Paiement + Paiement requis Jeton Cashu Échanger Envoyer vers le portefeuille Zap diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 8fff23237..eeb0dfa93 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -172,6 +172,12 @@ अनपेक्षित आरोहण स्थिति निप॰९५ का आलम्बन अभी नहीं है ध्वनि सन्देशों के लिए ध्वनि आरोहण असफल : %1$s + कुछ भी नहीं + गहन + उच्च + मध्यम + अनामीकरण + आपके स्वर का परिवर्तन करता है। ध्यातव्य। आधारभूत स्वर परिवर्तन को पूर्ववत किया जा सकता है दृढ निश्चित श्रोताओं द्वारा। उपयोगकर्ता का कोई लैटनिंग पता स्थापित नहीं जिसपर साट्स प्राप्त कर सके "उत्तर यहाँ दें.. " नोस्ट्र में बाँटने के लिए टीका विभेदक की अनुकृति करता है टाँकाफलक में @@ -1232,4 +1238,5 @@ पोटली प्रसारण सूची मिटाएँ पोटली मिटाएँ + प्रकार diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index d6716c99c..aa9ccf9d0 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -172,6 +172,12 @@ Váratlan feltöltési állapot A NIP-95 még nem támogatja a hangüzeneteket Nem sikerült feltölteni a hangüzenetet: %1$s + Semmi + Mély + Magas + Semleges + Névtelenítés + Módosítja az Ön hangmagasságát. Megjegyzés: az alapvető hangmagasság-változásokat a figyelmes hallgatók visszafordíthatják. A felhasználó nem rendelkezik a satoshik fogadásához beállított lightning-címmel "Válasz írása… " A bejegyzés azonosítóját a vágólapra másolja a Nostr-ban való megosztáshoz diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 6cbc2e7d5..7f427f586 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -172,6 +172,12 @@ 未知的上传状态 语音消息尚不支持 NIP-95 语音上传失败:%1$s + + 深沉 + 高音 + 中性 + 匿名化 + 更改您的音高。注意:听众如果下定决定也许能逆转基础音高更改。 用户尚未设置闪电地址以接收聪 "🔏在此回复… " 复制笔记ID到剪贴板,供分享 @@ -1232,4 +1238,5 @@ 广播包 删除列表 删除包 + 类型 From 29faa7882d8270e8a7229ac7eacffecdaa267ffc Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Tue, 20 Jan 2026 18:27:20 +0000 Subject: [PATCH 14/14] Increase max voice record duration to 600 seconds --- .../vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt index c7fe883aa..7fe4340d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -const val MAX_VOICE_RECORD_SECONDS = 180 +const val MAX_VOICE_RECORD_SECONDS = 600 @OptIn(ExperimentalPermissionsApi::class) @Composable