diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MetadataStripper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MetadataStripper.kt index 5056f3d1f..63ce315ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MetadataStripper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MetadataStripper.kt @@ -252,11 +252,7 @@ class MetadataStripper { return StrippingResult(uri, false) } - val extension = - when { - primaryMime.contains("mp4a") || primaryMime.contains("aac") -> ".m4a" - else -> ".mp4" - } + val extension = ".m4a" tempOutputFile = File.createTempFile("stripped_audio_", extension, context.cacheDir) muxer = MediaMuxer(tempOutputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) @@ -321,41 +317,61 @@ class MetadataStripper { } } ?: return StrippingResult(uri, false) - val bytes = tempInputFile.readBytes() + val fileSize = tempInputFile.length().toInt() var startOffset = 0 - var endOffset = bytes.size + var endOffset = fileSize - // Strip ID3v2 header (at beginning of file) - if (bytes.size >= 10 && - bytes[0] == 'I'.code.toByte() && - bytes[1] == 'D'.code.toByte() && - bytes[2] == '3'.code.toByte() + // Read first 10 bytes to check for ID3v2 header + val header = ByteArray(10) + tempInputFile.inputStream().use { it.read(header) } + + if (fileSize >= 10 && + header[0] == 'I'.code.toByte() && + header[1] == 'D'.code.toByte() && + header[2] == '3'.code.toByte() ) { val size = - (bytes[6].toInt() and 0x7F shl 21) or - (bytes[7].toInt() and 0x7F shl 14) or - (bytes[8].toInt() and 0x7F shl 7) or - (bytes[9].toInt() and 0x7F) + (header[6].toInt() and 0x7F shl 21) or + (header[7].toInt() and 0x7F shl 14) or + (header[8].toInt() and 0x7F shl 7) or + (header[9].toInt() and 0x7F) startOffset = 10 + size } - // Strip ID3v1 tag (last 128 bytes) - if (endOffset - startOffset >= 128 && - bytes[endOffset - 128] == 'T'.code.toByte() && - bytes[endOffset - 127] == 'A'.code.toByte() && - bytes[endOffset - 126] == 'G'.code.toByte() - ) { - endOffset -= 128 + // Read last 128 bytes to check for ID3v1 tag + if (endOffset - startOffset >= 128) { + val tail = ByteArray(128) + java.io.RandomAccessFile(tempInputFile, "r").use { raf -> + raf.seek((endOffset - 128).toLong()) + raf.readFully(tail) + } + if (tail[0] == 'T'.code.toByte() && + tail[1] == 'A'.code.toByte() && + tail[2] == 'G'.code.toByte() + ) { + endOffset -= 128 + } } - if (startOffset == 0 && endOffset == bytes.size) { + if (startOffset == 0 && endOffset == fileSize) { tempInputFile.delete() return StrippingResult(uri, true) // no tags found, already clean } val tempOutputFile = File.createTempFile("stripped_mp3_", ".mp3", context.cacheDir) - tempOutputFile.outputStream().use { output -> - output.write(bytes, startOffset, endOffset - startOffset) + java.io.RandomAccessFile(tempInputFile, "r").use { raf -> + raf.seek(startOffset.toLong()) + tempOutputFile.outputStream().use { output -> + val buffer = ByteArray(8192) + var remaining = endOffset - startOffset + while (remaining > 0) { + val toRead = minOf(buffer.size, remaining) + val read = raf.read(buffer, 0, toRead) + if (read <= 0) break + output.write(buffer, 0, read) + remaining -= read + } + } } tempInputFile.delete() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 4958bf3c8..64bcbc75f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -39,8 +39,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CurrencyBitcoin -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -127,23 +125,7 @@ fun EditPostView( postViewModel.load(edit, versionLookingAt) } - postViewModel.strippingFailureDialog?.let { dialogState -> - AlertDialog( - onDismissRequest = { dialogState.onCancel() }, - title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, - text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, - confirmButton = { - Button(onClick = { dialogState.onConfirm() }) { - Text(stringRes(R.string.metadata_strip_failed_upload)) - } - }, - dismissButton = { - Button(onClick = { dialogState.onCancel() }) { - Text(stringRes(R.string.cancel)) - } - }, - ) - } + StrippingFailureDialog(postViewModel.strippingFailureDialog) Dialog( onDismissRequest = { onClose() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index a624f73a4..8432c20b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -96,6 +96,21 @@ open class EditPostViewModel : ViewModel() { var strippingFailureDialog by mutableStateOf(null) private set + private suspend fun showStrippingFailureDialog(): Boolean = + suspendCancellableCoroutine { continuation -> + strippingFailureDialog = + StrippingFailureState( + onConfirm = { + strippingFailureDialog = null + continuation.resume(true) {} + }, + onCancel = { + strippingFailureDialog = null + continuation.resume(false) {} + }, + ) + } + // Codec selection: false = H264, true = H265 var useH265Codec by mutableStateOf(false) @@ -203,21 +218,7 @@ open class EditPostViewModel : ViewModel() { context, useH265Codec, stripMetadata, - onStrippingFailed = { - suspendCancellableCoroutine { continuation -> - strippingFailureDialog = - StrippingFailureState( - onConfirm = { - strippingFailureDialog = null - continuation.resume(true) {} - }, - onCancel = { - strippingFailureDialog = null - continuation.resume(false) {} - }, - ) - } - }, + onStrippingFailed = ::showStrippingFailureDialog, ) if (results.allGood) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 9f889b335..a5e3c40d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -65,6 +65,21 @@ open class NewMediaModel : ViewModel() { var strippingFailureDialog by mutableStateOf(null) private set + private suspend fun showStrippingFailureDialog(): Boolean = + suspendCancellableCoroutine { continuation -> + strippingFailureDialog = + StrippingFailureState( + onConfirm = { + strippingFailureDialog = null + continuation.resume(true) {} + }, + onCancel = { + strippingFailureDialog = null + continuation.resume(false) {} + }, + ) + } + // 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED var mediaQualitySlider by mutableIntStateOf(1) @@ -126,21 +141,7 @@ open class NewMediaModel : ViewModel() { context, useH265Codec, stripMetadata, - onStrippingFailed = { - suspendCancellableCoroutine { continuation -> - strippingFailureDialog = - StrippingFailureState( - onConfirm = { - strippingFailureDialog = null - continuation.resume(true) {} - }, - onCancel = { - strippingFailureDialog = null - continuation.resume(false) {} - }, - ) - } - }, + onStrippingFailed = ::showStrippingFailureDialog, ) if (results.allGood) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 147a12215..f5dee6fb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -32,8 +32,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -91,23 +89,7 @@ fun NewMediaView( postViewModel.load(account, uris) } - postViewModel.strippingFailureDialog?.let { dialogState -> - AlertDialog( - onDismissRequest = { dialogState.onCancel() }, - title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, - text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, - confirmButton = { - Button(onClick = { dialogState.onConfirm() }) { - Text(stringRes(R.string.metadata_strip_failed_upload)) - } - }, - dismissButton = { - Button(onClick = { dialogState.onCancel() }) { - Text(stringRes(R.string.cancel)) - } - }, - ) - } + StrippingFailureDialog(postViewModel.strippingFailureDialog) Dialog( onDismissRequest = { onClose() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/StrippingFailureDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/StrippingFailureDialog.kt new file mode 100644 index 000000000..6088dcd1f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/StrippingFailureDialog.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.uploads.StrippingFailureState +import com.vitorpamplona.amethyst.ui.stringRes + +@Composable +fun StrippingFailureDialog(dialogState: StrippingFailureState?) { + dialogState ?: return + + AlertDialog( + onDismissRequest = { dialogState.onCancel() }, + title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, + text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, + confirmButton = { + Button(onClick = { dialogState.onConfirm() }) { + Text(stringRes(R.string.metadata_strip_failed_upload)) + } + }, + dismissButton = { + Button(onClick = { dialogState.onCancel() }) { + Text(stringRes(R.string.cancel)) + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index d5cabd69d..ab51d4cb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -250,47 +250,52 @@ fun ImageVideoDescription( ) } - SettingSwitchItem( - title = R.string.strip_metadata_label, - description = R.string.strip_metadata_description, - modifier = - Modifier - .fillMaxWidth() - .padding(top = 8.dp), - checked = stripMetadata, - onCheckedChange = { stripMetadata = it }, - ) + // Hide privacy toggle when video compression is selected (compression already strips metadata) + val isVideoWithCompression = + uris.first().media.isVideo() == true && mediaQualitySlider != 3 - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) - .padding(vertical = 8.dp), - ) { - Column( - modifier = Modifier.weight(1.0f), - verticalArrangement = Arrangement.spacedBy(Size5dp), - ) { - Text( - text = stringRes(R.string.media_compression_quality_label), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(R.string.media_compression_quality_explainer), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 5, - overflow = TextOverflow.Ellipsis, - ) - } + if (!isVideoWithCompression) { + SettingSwitchItem( + title = R.string.strip_metadata_label, + description = R.string.strip_metadata_description, + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + checked = stripMetadata, + onCheckedChange = { stripMetadata = it }, + ) } val firstMedia = uris.first().media if (firstMedia.isVideo() == true || firstMedia.isImage() == true) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .padding(vertical = 8.dp), + ) { + Column( + modifier = Modifier.weight(1.0f), + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.media_compression_quality_label), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.media_compression_quality_explainer), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 5, + overflow = TextOverflow.Ellipsis, + ) + } + } Column(horizontalAlignment = Alignment.CenterHorizontally) { Box(modifier = Modifier.fillMaxWidth()) { Text( @@ -334,7 +339,10 @@ fun ImageVideoDescription( .fillMaxWidth() .padding(vertical = 10.dp), enabled = !isUploading, - onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec, stripMetadata) }, + onClick = { + val effectiveStripMetadata = if (isVideoWithCompression) false else stripMetadata + onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec, effectiveStripMetadata) + }, shape = QuoteBorder, colors = ButtonDefaults.buttonColors( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index da687602a..7c9f77096 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -168,6 +168,21 @@ open class CommentPostViewModel : var strippingFailureDialog by mutableStateOf(null) private set + private suspend fun showStrippingFailureDialog(): Boolean = + suspendCancellableCoroutine { continuation -> + strippingFailureDialog = + StrippingFailureState( + onConfirm = { + strippingFailureDialog = null + continuation.resume(true) {} + }, + onCancel = { + strippingFailureDialog = null + continuation.resume(false) {} + }, + ) + } + // Invoices var canAddInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false) @@ -509,21 +524,7 @@ open class CommentPostViewModel : account, context, stripMetadata = stripMetadata, - onStrippingFailed = { - suspendCancellableCoroutine { continuation -> - strippingFailureDialog = - StrippingFailureState( - onConfirm = { - strippingFailureDialog = null - continuation.resume(true) {} - }, - onCancel = { - strippingFailureDialog = null - continuation.resume(false) {} - }, - ) - } - }, + onStrippingFailed = ::showStrippingFailureDialog, ) if (results.allGood) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index fa11b4639..bcdcce7a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -34,13 +34,10 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api 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.ui.Alignment.Companion.CenterVertically @@ -51,6 +48,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -143,23 +141,7 @@ fun GenericCommentPostScreen( ) { WatchAndLoadMyEmojiList(accountViewModel) - postViewModel.strippingFailureDialog?.let { dialogState -> - AlertDialog( - onDismissRequest = { dialogState.onCancel() }, - title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, - text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, - confirmButton = { - Button(onClick = { dialogState.onConfirm() }) { - Text(stringRes(R.string.metadata_strip_failed_upload)) - } - }, - dismissButton = { - Button(onClick = { dialogState.onCancel() }) { - Text(stringRes(R.string.cancel)) - } - }, - ) - } + StrippingFailureDialog(postViewModel.strippingFailureDialog) BackHandler { accountViewModel.launchSigner { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 4bcf304ad..a3194c8ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send -import android.R.attr.maxLines import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -42,6 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField @@ -118,23 +118,7 @@ fun PrivateMessageEditFieldRow( } } - channelScreenModel.strippingFailureDialog?.let { dialogState -> - AlertDialog( - onDismissRequest = { dialogState.onCancel() }, - title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, - text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, - confirmButton = { - TextButton(onClick = { dialogState.onConfirm() }) { - Text(stringRes(R.string.metadata_strip_failed_upload)) - } - }, - dismissButton = { - TextButton(onClick = { dialogState.onCancel() }) { - Text(stringRes(R.string.cancel)) - } - }, - ) - } + StrippingFailureDialog(channelScreenModel.strippingFailureDialog) channelScreenModel.encryptedUploadErrorTitle?.let { title -> EncryptedUploadErrorDialog( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 24bc7de3d..5fe146ef2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -136,6 +136,21 @@ open class ChannelNewMessageViewModel : var strippingFailureDialog by mutableStateOf(null) private set + private suspend fun showStrippingFailureDialog(): Boolean = + suspendCancellableCoroutine { continuation -> + strippingFailureDialog = + StrippingFailureState( + onConfirm = { + strippingFailureDialog = null + continuation.resume(true) {} + }, + onCancel = { + strippingFailureDialog = null + continuation.resume(false) {} + }, + ) + } + val iMetaAttachments = IMetaAttachments() var nip95attachments by mutableStateOf>>(emptyList()) @@ -356,21 +371,7 @@ open class ChannelNewMessageViewModel : account, context, stripMetadata = uploadState.stripMetadata, - onStrippingFailed = { - suspendCancellableCoroutine { continuation -> - strippingFailureDialog = - StrippingFailureState( - onConfirm = { - strippingFailureDialog = null - continuation.resume(true) {} - }, - onCancel = { - strippingFailureDialog = null - continuation.resume(false) {} - }, - ) - } - }, + onStrippingFailed = ::showStrippingFailureDialog, ) if (results.allGood) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index 9972d2c42..ae428a45f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -26,8 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -39,6 +37,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField @@ -72,23 +71,7 @@ fun EditFieldRow( nav.popBack() } - channelScreenModel.strippingFailureDialog?.let { dialogState -> - AlertDialog( - onDismissRequest = { dialogState.onCancel() }, - title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, - text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, - confirmButton = { - Button(onClick = { dialogState.onConfirm() }) { - Text(stringRes(R.string.metadata_strip_failed_upload)) - } - }, - dismissButton = { - Button(onClick = { dialogState.onCancel() }) { - Text(stringRes(R.string.cancel)) - } - }, - ) - } + StrippingFailureDialog(channelScreenModel.strippingFailureDialog) channelScreenModel.replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 6f4ad9b82..7b83939d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -33,13 +33,10 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api 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.ui.Alignment.Companion.CenterVertically @@ -50,6 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -140,23 +138,7 @@ fun NewProductScreen( ) { WatchAndLoadMyEmojiList(accountViewModel) - postViewModel.strippingFailureDialog?.let { dialogState -> - AlertDialog( - onDismissRequest = { dialogState.onCancel() }, - title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, - text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, - confirmButton = { - Button(onClick = { dialogState.onConfirm() }) { - Text(stringRes(R.string.metadata_strip_failed_upload)) - } - }, - dismissButton = { - Button(onClick = { dialogState.onCancel() }) { - Text(stringRes(R.string.cancel)) - } - }, - ) - } + StrippingFailureDialog(postViewModel.strippingFailureDialog) BackHandler { accountViewModel.launchSigner { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 718359e38..07e7663db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -152,6 +152,21 @@ open class NewProductViewModel : var strippingFailureDialog by mutableStateOf(null) private set + private suspend fun showStrippingFailureDialog(): Boolean = + suspendCancellableCoroutine { continuation -> + strippingFailureDialog = + StrippingFailureState( + onConfirm = { + strippingFailureDialog = null + continuation.resume(true) {} + }, + onCancel = { + strippingFailureDialog = null + continuation.resume(false) {} + }, + ) + } + // Classifieds var title by mutableStateOf(TextFieldValue("")) var price by mutableStateOf(TextFieldValue("")) @@ -417,21 +432,7 @@ open class NewProductViewModel : myAccount, context, stripMetadata = stripMetadata, - onStrippingFailed = { - suspendCancellableCoroutine { continuation -> - strippingFailureDialog = - StrippingFailureState( - onConfirm = { - strippingFailureDialog = null - continuation.resume(true) {} - }, - onCancel = { - strippingFailureDialog = null - continuation.resume(false) {} - }, - ) - } - }, + onStrippingFailed = ::showStrippingFailureDialog, ) if (results.allGood) { 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 09ac0dd61..2d093cd49 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 @@ -39,8 +39,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Poll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -48,7 +46,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -63,6 +60,7 @@ import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog 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.RecordVoiceButton @@ -182,23 +180,7 @@ private fun NewPostScreenInner( ) { WatchAndLoadMyEmojiList(accountViewModel) - postViewModel.strippingFailureDialog?.let { dialogState -> - AlertDialog( - onDismissRequest = { dialogState.onCancel() }, - title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, - text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, - confirmButton = { - Button(onClick = { dialogState.onConfirm() }) { - Text(stringRes(R.string.metadata_strip_failed_upload)) - } - }, - dismissButton = { - Button(onClick = { dialogState.onCancel() }) { - Text(stringRes(R.string.cancel)) - } - }, - ) - } + StrippingFailureDialog(postViewModel.strippingFailureDialog) BackHandler { accountViewModel.launchSigner { 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 74c2679fc..31386bd75 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 @@ -197,6 +197,21 @@ open class ShortNotePostViewModel : var strippingFailureDialog by mutableStateOf(null) private set + private suspend fun showStrippingFailureDialog(): Boolean = + suspendCancellableCoroutine { continuation -> + strippingFailureDialog = + StrippingFailureState( + onConfirm = { + strippingFailureDialog = null + continuation.resume(true) {} + }, + onCancel = { + strippingFailureDialog = null + continuation.resume(false) {} + }, + ) + } + // Voice Messages var voiceRecording by mutableStateOf(null) var voiceLocalFile by mutableStateOf(null) @@ -852,21 +867,7 @@ open class ShortNotePostViewModel : context, useH265, stripMetadata, - onStrippingFailed = { - suspendCancellableCoroutine { continuation -> - strippingFailureDialog = - StrippingFailureState( - onConfirm = { - strippingFailureDialog = null - continuation.resume(true) {} - }, - onCancel = { - strippingFailureDialog = null - continuation.resume(false) {} - }, - ) - } - }, + onStrippingFailed = ::showStrippingFailureDialog, ) if (results.allGood) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 8c5e7463c..2834ce0a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -33,8 +33,6 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme @@ -58,6 +56,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery @@ -133,23 +132,7 @@ fun NewPublicMessageScreen( WatchAndLoadMyEmojiList(accountViewModel) - postViewModel.strippingFailureDialog?.let { dialogState -> - AlertDialog( - onDismissRequest = { dialogState.onCancel() }, - title = { Text(stringRes(R.string.metadata_strip_failed_title)) }, - text = { Text(stringRes(R.string.metadata_strip_failed_body)) }, - confirmButton = { - Button(onClick = { dialogState.onConfirm() }) { - Text(stringRes(R.string.metadata_strip_failed_upload)) - } - }, - dismissButton = { - Button(onClick = { dialogState.onCancel() }) { - Text(stringRes(R.string.cancel)) - } - }, - ) - } + StrippingFailureDialog(postViewModel.strippingFailureDialog) BackHandler { accountViewModel.launchSigner { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index a6bd5ebbc..f7825d963 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -170,6 +170,21 @@ class NewPublicMessageViewModel : var strippingFailureDialog by mutableStateOf(null) private set + private suspend fun showStrippingFailureDialog(): Boolean = + suspendCancellableCoroutine { continuation -> + strippingFailureDialog = + StrippingFailureState( + onConfirm = { + strippingFailureDialog = null + continuation.resume(true) {} + }, + onCancel = { + strippingFailureDialog = null + continuation.resume(false) {} + }, + ) + } + // Invoices var canAddInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false) @@ -461,21 +476,7 @@ class NewPublicMessageViewModel : account, context, stripMetadata = stripMetadata, - onStrippingFailed = { - suspendCancellableCoroutine { continuation -> - strippingFailureDialog = - StrippingFailureState( - onConfirm = { - strippingFailureDialog = null - continuation.resume(true) {} - }, - onCancel = { - strippingFailureDialog = null - continuation.resume(false) {} - }, - ) - } - }, + onStrippingFailed = ::showStrippingFailureDialog, ) if (results.allGood) {