From c290a1c7793c9c398f751b60f953afbd41259f36 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 12:58:18 +0100 Subject: [PATCH 1/4] - SelectFromGallery and SelectFromFiles now accept an enabled param separate from isUploading - All ViewModels track isUploadingFile alongside isUploadingImage, set via hasNonMedia() - All screen call sites pass source-specific flags so only the initiating button spins - canPost() methods block posting during file uploads too - ChatNewMessageViewModel delegates to ChatFileUploadState (fixes pre-existing dead state) - cancel() methods reset both flags in all ViewModels --- .../amethyst/ui/actions/EditPostView.kt | 4 +++- .../amethyst/ui/actions/EditPostViewModel.kt | 11 +++++++++-- .../amethyst/ui/actions/uploads/SelectFromFiles.kt | 6 ++++-- .../ui/actions/uploads/SelectFromGallery.kt | 9 ++++++--- .../ui/note/nip22Comments/CommentPostViewModel.kt | 10 +++++++++- .../note/nip22Comments/GenericCommentPostScreen.kt | 4 +++- .../privateDM/send/ChatNewMessageViewModel.kt | 4 +++- .../chats/privateDM/send/NewGroupDMScreen.kt | 4 +++- .../privateDM/send/upload/ChatFileUploader.kt | 14 ++++++++++++-- .../send/ChannelNewMessageViewModel.kt | 12 +++++++++++- .../loggedIn/chats/utils/ChatFileUploadState.kt | 2 ++ .../discover/nip99Classifieds/NewProductScreen.kt | 4 +++- .../nip99Classifieds/NewProductViewModel.kt | 10 +++++++++- .../ui/screen/loggedIn/home/ShortNotePostScreen.kt | 4 +++- .../screen/loggedIn/home/ShortNotePostViewModel.kt | 12 ++++++++++-- .../publicMessages/NewPublicMessageScreen.kt | 4 +++- .../publicMessages/NewPublicMessageViewModel.kt | 12 +++++++++++- 17 files changed, 104 insertions(+), 22 deletions(-) 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 d27935f49..14b9bb5cc 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 @@ -372,6 +372,7 @@ private fun BottomRowActions(postViewModel: EditPostViewModel) { ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -379,7 +380,8 @@ private fun BottomRowActions(postViewModel: EditPostViewModel) { } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { 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 1c9d57d60..12a7482ba 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 @@ -80,6 +80,7 @@ open class EditPostViewModel : ViewModel() { var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -180,7 +181,11 @@ open class EditPostViewModel : ViewModel() { val myAccount = account val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -244,6 +249,7 @@ open class EditPostViewModel : ViewModel() { } isUploadingImage = false + isUploadingFile = false } } @@ -256,6 +262,7 @@ open class EditPostViewModel : ViewModel() { multiOrchestrator = null urlPreview = null isUploadingImage = false + isUploadingFile = false wantsInvoice = false @@ -296,7 +303,7 @@ open class EditPostViewModel : ViewModel() { } } - fun canPost() = message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && multiOrchestrator == null + fun canPost() = message.text.isNotBlank() && !isUploadingImage && !isUploadingFile && !wantsInvoice && multiOrchestrator == null fun selectImage(uris: ImmutableList) { multiOrchestrator = MultiOrchestrator(uris) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt index 354853720..7ed03deb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt @@ -48,6 +48,7 @@ import java.util.concurrent.atomic.AtomicBoolean @Composable fun SelectFromFiles( isUploading: Boolean, + enabled: Boolean = true, tint: Color, modifier: Modifier, onFilesChosen: (ImmutableList) -> Unit, @@ -64,19 +65,20 @@ fun SelectFromFiles( ) } - FileSelectButton(isUploading, tint, modifier) { showFileSelect = true } + FileSelectButton(isUploading, enabled, tint, modifier) { showFileSelect = true } } @Composable private fun FileSelectButton( isUploading: Boolean, + enabled: Boolean, tint: Color, modifier: Modifier, onClick: () -> Unit, ) { IconButton( modifier = modifier, - enabled = !isUploading, + enabled = enabled && !isUploading, onClick = { onClick() }, ) { if (!isUploading) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt index 0e38451c5..bd7eb8fe2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt @@ -69,6 +69,7 @@ class SelectedMedia( @Composable fun SelectFromGallery( isUploading: Boolean, + enabled: Boolean = true, tint: Color, modifier: Modifier, onImageChosen: (ImmutableList) -> Unit, @@ -85,12 +86,13 @@ fun SelectFromGallery( ) } - GallerySelectButton(isUploading, tint, modifier) { showGallerySelect = true } + GallerySelectButton(isUploading, enabled, tint, modifier) { showGallerySelect = true } } @Composable fun SelectSingleFromGallery( isUploading: Boolean, + enabled: Boolean = true, tint: Color, modifier: Modifier, onImageChosen: (SelectedMedia) -> Unit, @@ -107,19 +109,20 @@ fun SelectSingleFromGallery( ) } - GallerySelectButton(isUploading, tint, modifier) { showGallerySelect = true } + GallerySelectButton(isUploading, enabled, tint, modifier) { showGallerySelect = true } } @Composable private fun GallerySelectButton( isUploading: Boolean, + enabled: Boolean, tint: Color, modifier: Modifier, onClick: () -> Unit, ) { IconButton( modifier = modifier, - enabled = !isUploading, + enabled = enabled && !isUploading, onClick = { onClick() }, ) { if (!isUploading) { 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 c60b0a42e..91c5662e5 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 @@ -150,6 +150,7 @@ open class CommentPostViewModel : val urlPreviews = PreviewState() var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -487,7 +488,11 @@ open class CommentPostViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -552,6 +557,7 @@ open class CommentPostViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -565,6 +571,7 @@ open class CommentPostViewModel : multiOrchestrator = null isUploadingImage = false + isUploadingFile = false notifying = null @@ -677,6 +684,7 @@ open class CommentPostViewModel : fun canPost(): Boolean = message.text.isNotBlank() && !isUploadingImage && + !isUploadingFile && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && multiOrchestrator == null 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 ed922a496..5e8c197c7 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 @@ -391,6 +391,7 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) { ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -398,7 +399,8 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) { } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 747920b86..db6f377f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -141,7 +141,8 @@ class ChatNewMessageViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) + val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false + val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -692,6 +693,7 @@ class ChatNewMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && uploadState?.isUploadingImage != true && + uploadState?.isUploadingFile != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && (toUsers.text.isNotBlank()) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index 3a2f1f859..ef5f32b11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -386,6 +386,7 @@ private fun BottomRowActions( if (postViewModel.room != null) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -393,7 +394,8 @@ private fun BottomRowActions( } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index ddc001805..b6f8f3f91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -43,7 +43,11 @@ class ChatFileUploader( onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return - viewState.isUploadingImage = true + if (orchestrator.hasNonMedia()) { + viewState.isUploadingFile = true + } else { + viewState.isUploadingImage = true + } val cipher = AESGCM() @@ -77,6 +81,7 @@ class ChatFileUploader( } viewState.isUploadingImage = false + viewState.isUploadingFile = false } // ------ @@ -90,7 +95,11 @@ class ChatFileUploader( onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return - viewState.isUploadingImage = true + if (orchestrator.hasNonMedia()) { + viewState.isUploadingFile = true + } else { + viewState.isUploadingImage = true + } val results = orchestrator.upload( @@ -121,5 +130,6 @@ class ChatFileUploader( } viewState.isUploadingImage = false + viewState.isUploadingFile = false } } 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 98b36ef0e..0c6c59dcf 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 @@ -135,6 +135,7 @@ open class ChannelNewMessageViewModel : var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -337,7 +338,11 @@ open class ChannelNewMessageViewModel : val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -379,6 +384,7 @@ open class ChannelNewMessageViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -546,6 +552,9 @@ open class ChannelNewMessageViewModel : userSuggestions?.reset() userSuggestionsMainMessage = null + isUploadingImage = false + isUploadingFile = false + iMetaAttachments.reset() emojiSuggestions?.reset() @@ -633,6 +642,7 @@ open class ChannelNewMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && uploadState?.isUploadingImage != true && + uploadState?.isUploadingFile != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount != null) && uploadState?.multiOrchestrator == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index 9475afa73..384c8299e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -37,6 +37,7 @@ class ChatFileUploadState( val defaultServer: ServerName, ) { var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var selectedServer by mutableStateOf(defaultServer) var caption by mutableStateOf("") @@ -65,6 +66,7 @@ class ChatFileUploadState( fun reset() { multiOrchestrator = null isUploadingImage = false + isUploadingFile = false caption = "" selectedServer = defaultServer } 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 aa651348e..d4c3e1853 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 @@ -359,6 +359,7 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) { ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -366,7 +367,8 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) { } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { 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 be86ac151..b2014fc66 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 @@ -134,6 +134,7 @@ open class NewProductViewModel : val urlPreviews = PreviewState() var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -399,7 +400,11 @@ open class NewProductViewModel : val myAccount = account ?: return@launch val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -445,6 +450,7 @@ open class NewProductViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -455,6 +461,7 @@ open class NewProductViewModel : multiOrchestrator = null isUploadingImage = false + isUploadingFile = false wantsInvoice = false wantsZapraiser = false @@ -576,6 +583,7 @@ open class NewProductViewModel : fun canPost(): Boolean = message.text.isNotBlank() && !isUploadingImage && + !isUploadingFile && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && title.text.isNotBlank() && 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 8f799892e..d6d473a9d 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 @@ -494,6 +494,7 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -501,7 +502,8 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { 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 422394b5b..d9018502f 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 @@ -179,6 +179,7 @@ open class ShortNotePostViewModel : val urlPreviews = PreviewState() var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -829,7 +830,11 @@ open class ShortNotePostViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -886,6 +891,7 @@ open class ShortNotePostViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -898,6 +904,7 @@ open class ShortNotePostViewModel : multiOrchestrator = null isUploadingImage = false + isUploadingFile = false voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null @@ -1032,12 +1039,13 @@ open class ShortNotePostViewModel : fun canPost(): Boolean { // Voice messages can be posted without text (with either uploaded or pending recording) if (voiceMetadata != null || voiceRecording != null) { - return !isUploadingVoice && !isUploadingImage && processingPreset == null + return !isUploadingVoice && !isUploadingImage && !isUploadingFile && processingPreset == null } // Regular text/media posts require text return message.text.isNotBlank() && !isUploadingImage && + !isUploadingFile && !isUploadingVoice && !wantsInvoice && (!wantsZapRaiser || zapRaiserAmount.value != null) && 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 a442d467c..0c2ff9fc1 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 @@ -321,6 +321,7 @@ private fun BottomRowActions( ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -328,7 +329,8 @@ private fun BottomRowActions( } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { 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 d3ccd5421..14495a37a 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 @@ -150,6 +150,7 @@ class NewPublicMessageViewModel : val urlPreviews = PreviewState() var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -439,7 +440,11 @@ class NewPublicMessageViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -495,6 +500,7 @@ class NewPublicMessageViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -523,6 +529,9 @@ class NewPublicMessageViewModel : userSuggestions?.reset() userSuggestionsMainMessage = null + isUploadingImage = false + isUploadingFile = false + iMetaAttachments.reset() emojiSuggestions?.reset() @@ -629,6 +638,7 @@ class NewPublicMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && !isUploadingImage && + !isUploadingFile && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && (toUsers.text.isNotBlank()) && From 42542159d456bc0b6f9c0bb1405a87e1cca20c59 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 13:06:12 +0100 Subject: [PATCH 2/4] bugfix: remove dead code bugfix: ChannelNewMessageViewModel canPost checks wrong state --- .../ui/actions/uploads/SelectFromGallery.kt | 3 +-- .../send/ChannelNewMessageViewModel.kt | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt index bd7eb8fe2..fcae06fc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt @@ -92,7 +92,6 @@ fun SelectFromGallery( @Composable fun SelectSingleFromGallery( isUploading: Boolean, - enabled: Boolean = true, tint: Color, modifier: Modifier, onImageChosen: (SelectedMedia) -> Unit, @@ -109,7 +108,7 @@ fun SelectSingleFromGallery( ) } - GallerySelectButton(isUploading, enabled, tint, modifier) { showGallerySelect = true } + GallerySelectButton(isUploading, true, tint, modifier) { showGallerySelect = true } } @Composable 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 0c6c59dcf..cd69ddf43 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 @@ -134,8 +134,8 @@ open class ChannelNewMessageViewModel : var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false + val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -339,9 +339,9 @@ open class ChannelNewMessageViewModel : val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true + uploadState.isUploadingFile = true } else { - isUploadingImage = true + uploadState.isUploadingImage = true } val results = @@ -383,8 +383,8 @@ open class ChannelNewMessageViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + uploadState.isUploadingImage = false + uploadState.isUploadingFile = false } } @@ -552,8 +552,7 @@ open class ChannelNewMessageViewModel : userSuggestions?.reset() userSuggestionsMainMessage = null - isUploadingImage = false - isUploadingFile = false + uploadState?.reset() iMetaAttachments.reset() From 1fc0ba8628a764a99d41b953f6a854288edfb9d9 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 16:01:23 +0100 Subject: [PATCH 3/4] refactor: extract MediaUploadTracker with common methods bugfixes: ChatFileUploadState.canPost() was missing isUploadingFile check ChatNewMessageViewModel.cancel() was never resetting upload flags Double-reset in ChatFileUploader and ChannelNewMessageViewModel --- .../amethyst/ui/actions/EditPostViewModel.kt | 20 ++++---- .../ui/actions/uploads/MediaUploadTracker.kt | 47 +++++++++++++++++++ .../nip22Comments/CommentPostViewModel.kt | 21 ++++----- .../privateDM/send/ChatNewMessageViewModel.kt | 4 +- .../privateDM/send/upload/ChatFileUploader.kt | 18 ++----- .../send/ChannelNewMessageViewModel.kt | 12 ++--- .../chats/utils/ChatFileUploadState.kt | 11 +++-- .../nip99Classifieds/NewProductViewModel.kt | 21 ++++----- .../loggedIn/home/ShortNotePostViewModel.kt | 23 ++++----- .../NewPublicMessageViewModel.kt | 21 ++++----- 10 files changed, 103 insertions(+), 95 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt 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 12a7482ba..fec34ac2a 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 @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState @@ -79,8 +80,9 @@ open class EditPostViewModel : ViewModel() { var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -181,11 +183,7 @@ open class EditPostViewModel : ViewModel() { val myAccount = account val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -248,8 +246,7 @@ open class EditPostViewModel : ViewModel() { onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -261,8 +258,7 @@ open class EditPostViewModel : ViewModel() { multiOrchestrator = null urlPreview = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() wantsInvoice = false @@ -303,7 +299,7 @@ open class EditPostViewModel : ViewModel() { } } - fun canPost() = message.text.isNotBlank() && !isUploadingImage && !isUploadingFile && !wantsInvoice && multiOrchestrator == null + fun canPost() = message.text.isNotBlank() && !mediaUploadTracker.isUploading && !wantsInvoice && multiOrchestrator == null fun selectImage(uris: ImmutableList) { multiOrchestrator = MultiOrchestrator(uris) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt new file mode 100644 index 000000000..6ff5e72ec --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt @@ -0,0 +1,47 @@ +/* + * 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.uploads + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +class MediaUploadTracker { + var isUploadingImage by mutableStateOf(false) + private set + var isUploadingFile by mutableStateOf(false) + private set + + val isUploading: Boolean get() = isUploadingImage || isUploadingFile + + fun startUpload(hasNonMedia: Boolean) { + if (hasNonMedia) { + isUploadingFile = true + } else { + isUploadingImage = true + } + } + + fun finishUpload() { + isUploadingImage = false + isUploadingFile = false + } +} 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 91c5662e5..b8682a34d 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 @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState @@ -149,8 +150,9 @@ open class CommentPostViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -488,11 +490,7 @@ open class CommentPostViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -556,8 +554,7 @@ open class CommentPostViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -570,8 +567,7 @@ open class CommentPostViewModel : externalIdentity = null multiOrchestrator = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() notifying = null @@ -683,8 +679,7 @@ open class CommentPostViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - !isUploadingImage && - !isUploadingFile && + !mediaUploadTracker.isUploading && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && multiOrchestrator == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index db6f377f9..8ae6448bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -561,6 +561,7 @@ class ChatNewMessageViewModel : userSuggestionsMainMessage = null uploadsWaitingToBeSent = emptyList() + uploadState?.reset() iMetaAttachments.reset() @@ -692,8 +693,7 @@ class ChatNewMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - uploadState?.isUploadingImage != true && - uploadState?.isUploadingFile != true && + uploadState?.mediaUploadTracker?.isUploading != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && (toUsers.text.isNotBlank()) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index b6f8f3f91..33a9e456e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -43,11 +43,7 @@ class ChatFileUploader( onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return - if (orchestrator.hasNonMedia()) { - viewState.isUploadingFile = true - } else { - viewState.isUploadingImage = true - } + viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia()) val cipher = AESGCM() @@ -80,8 +76,7 @@ class ChatFileUploader( onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - viewState.isUploadingImage = false - viewState.isUploadingFile = false + viewState.mediaUploadTracker.finishUpload() } // ------ @@ -95,11 +90,7 @@ class ChatFileUploader( onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return - if (orchestrator.hasNonMedia()) { - viewState.isUploadingFile = true - } else { - viewState.isUploadingImage = true - } + viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia()) val results = orchestrator.upload( @@ -129,7 +120,6 @@ class ChatFileUploader( onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - viewState.isUploadingImage = false - viewState.isUploadingFile = false + viewState.mediaUploadTracker.finishUpload() } } 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 cd69ddf43..b9eecab72 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 @@ -338,11 +338,7 @@ open class ChannelNewMessageViewModel : val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - uploadState.isUploadingFile = true - } else { - uploadState.isUploadingImage = true - } + uploadState.mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -383,8 +379,7 @@ open class ChannelNewMessageViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - uploadState.isUploadingImage = false - uploadState.isUploadingFile = false + uploadState.mediaUploadTracker.finishUpload() } } @@ -640,8 +635,7 @@ open class ChannelNewMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - uploadState?.isUploadingImage != true && - uploadState?.isUploadingFile != true && + uploadState?.mediaUploadTracker?.isUploading != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount != null) && uploadState?.multiOrchestrator == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index 384c8299e..32105a29c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import kotlinx.collections.immutable.ImmutableList @@ -36,8 +37,9 @@ import kotlinx.collections.immutable.ImmutableList class ChatFileUploadState( val defaultServer: ServerName, ) { - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var selectedServer by mutableStateOf(defaultServer) var caption by mutableStateOf("") @@ -65,8 +67,7 @@ class ChatFileUploadState( fun reset() { multiOrchestrator = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() caption = "" selectedServer = defaultServer } @@ -75,7 +76,7 @@ class ChatFileUploadState( multiOrchestrator?.remove(selected) } - fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null + fun canPost(): Boolean = !mediaUploadTracker.isUploading && multiOrchestrator != null fun hasPickedMedia() = multiOrchestrator != null 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 b2014fc66..c3d359d08 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 @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState @@ -133,8 +134,9 @@ open class NewProductViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -400,11 +402,7 @@ open class NewProductViewModel : val myAccount = account ?: return@launch val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -449,8 +447,7 @@ open class NewProductViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -460,8 +457,7 @@ open class NewProductViewModel : message = TextFieldValue("") multiOrchestrator = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() wantsInvoice = false wantsZapraiser = false @@ -582,8 +578,7 @@ open class NewProductViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - !isUploadingImage && - !isUploadingFile && + !mediaUploadTracker.isUploading && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && title.text.isNotBlank() && 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 d9018502f..4b63c9df4 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 @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing @@ -178,8 +179,9 @@ open class ShortNotePostViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -830,11 +832,7 @@ open class ShortNotePostViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -890,8 +888,7 @@ open class ShortNotePostViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -903,8 +900,7 @@ open class ShortNotePostViewModel : forkedFromNote = null multiOrchestrator = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null @@ -1039,13 +1035,12 @@ open class ShortNotePostViewModel : fun canPost(): Boolean { // Voice messages can be posted without text (with either uploaded or pending recording) if (voiceMetadata != null || voiceRecording != null) { - return !isUploadingVoice && !isUploadingImage && !isUploadingFile && processingPreset == null + return !isUploadingVoice && !mediaUploadTracker.isUploading && processingPreset == null } // Regular text/media posts require text return message.text.isNotBlank() && - !isUploadingImage && - !isUploadingFile && + !mediaUploadTracker.isUploading && !isUploadingVoice && !wantsInvoice && (!wantsZapRaiser || zapRaiserAmount.value != null) && 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 14495a37a..fbda70c24 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 @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState @@ -149,8 +150,9 @@ class NewPublicMessageViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -440,11 +442,7 @@ class NewPublicMessageViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -499,8 +497,7 @@ class NewPublicMessageViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -529,8 +526,7 @@ class NewPublicMessageViewModel : userSuggestions?.reset() userSuggestionsMainMessage = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() iMetaAttachments.reset() @@ -637,8 +633,7 @@ class NewPublicMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - !isUploadingImage && - !isUploadingFile && + !mediaUploadTracker.isUploading && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && (toUsers.text.isNotBlank()) && From 795da8cf1b4e32b5d68fdecc8d00063f41d30aec Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 17:54:51 +0100 Subject: [PATCH 4/4] Block Add to message button while uploading --- .../java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt | 1 + .../amethyst/ui/note/creators/uploads/ImageVideoDescription.kt | 2 ++ .../amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt | 1 + .../ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt | 1 + .../loggedIn/discover/nip99Classifieds/NewProductScreen.kt | 1 + .../amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt | 1 + .../notifications/publicMessages/NewPublicMessageScreen.kt | 1 + 7 files changed, 8 insertions(+) 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 14b9bb5cc..f4978575b 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 @@ -264,6 +264,7 @@ fun EditPostView( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context) accountViewModel.account.settings.changeDefaultFileServer(server) 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 9e40f6e1d..09f9339dc 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 @@ -78,6 +78,7 @@ import kotlinx.collections.immutable.toImmutableList fun ImageVideoDescription( uris: MultiOrchestrator, defaultServer: ServerName, + isUploading: Boolean, onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit, onDelete: (SelectedMediaProcessing) -> Unit, onCancel: () -> Unit, @@ -319,6 +320,7 @@ fun ImageVideoDescription( Modifier .fillMaxWidth() .padding(vertical = 10.dp), + enabled = !isUploading, onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec) }, shape = QuoteBorder, colors = 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 5e8c197c7..c4f826825 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 @@ -300,6 +300,7 @@ private fun GenericCommentPostBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index ef5f32b11..c8f1bf6c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -287,6 +287,7 @@ fun GroupDMScreenContent( ImageVideoDescription( selectedFiles, accountViewModel.account.settings.defaultFileServer, + isUploading = uploading.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.uploadAndHold( accountViewModel.toastManager::toast, 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 d4c3e1853..0400191e6 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 @@ -269,6 +269,7 @@ private fun NewProductBody( ImageVideoDescription( uris = it, defaultServer = accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) accountViewModel.account.settings.changeDefaultFileServer(server) 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 d6d473a9d..7239f8e82 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 @@ -356,6 +356,7 @@ private fun NewPostScreenBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, useH265 -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265) accountViewModel.account.settings.changeDefaultFileServer(server) 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 0c2ff9fc1..d236275d1 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 @@ -246,6 +246,7 @@ fun PublicMessageScreenContent( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) accountViewModel.account.settings.changeDefaultFileServer(server)