From 583855de4f6f52952b4fa5862f67944b4e93afbf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 11 Mar 2025 13:46:36 -0400 Subject: [PATCH] Generalizes the chat upload UI and applies it to channels --- .../send/PrivateMessageEditFieldRow.kt | 22 +- .../send/upload/ChatFileUploadDialog.kt | 266 +++-------------- .../privateDM/send/upload/ChatFileUploader.kt | 1 + .../send/ChannelFileUploadDialog.kt | 142 ++++++++++ .../send/ChannelNewMessageViewModel.kt | 4 +- .../chats/publicChannels/send/EditFieldRow.kt | 13 + .../chats/utils/ChatFileUploadDialog.kt | 267 ++++++++++++++++++ .../upload => utils}/ChatFileUploadState.kt | 2 +- 8 files changed, 463 insertions(+), 254 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/{privateDM/send/upload => utils}/ChatFileUploadState.kt (97%) 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 a27d6d035d..415208e928 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 @@ -40,14 +40,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization 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.UrlUserTagTransformation -import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.navigation.EmptyNav @@ -58,7 +56,7 @@ import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel @@ -119,22 +117,10 @@ fun PrivateMessageEditFieldRow( channelScreenModel.uploadState?.let { uploading -> uploading.multiOrchestrator?.let { selectedFiles -> - val context = LocalContext.current - - ChatFileUploadDialog( - room = channelScreenModel.room!!, + RoomChatFileUploadDialog( + channelScreenModel = channelScreenModel, state = uploading, - upload = { - channelScreenModel.upload( - onError = accountViewModel::toast, - context = context, - onceUploaded = onSendNewMessage, - ) - - if (uploading.selectedServer.type != ServerType.NIP95) { - accountViewModel.account.settings.changeDefaultFileServer(uploading.selectedServer) - } - }, + onUpload = onSendNewMessage, onCancel = uploading::reset, accountViewModel = accountViewModel, nav = nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt index 8e72986a45..65ab604ff4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt @@ -20,264 +20,62 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding 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.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType -import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TitleIconModifier -import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser -import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingSwitchItem -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameOnlyDisplay -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow -import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.ui.theme.Size34dp -import com.vitorpamplona.amethyst.ui.theme.Size5dp -import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import kotlinx.collections.immutable.toImmutableList -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun ChatFileUploadDialog( - room: ChatroomKey, +fun RoomChatFileUploadDialog( + channelScreenModel: ChatNewMessageViewModel, state: ChatFileUploadState, - upload: () -> Unit, + onUpload: () -> Unit, onCancel: () -> Unit, accountViewModel: AccountViewModel, nav: INav, ) { - val scrollState = rememberScrollState() + val room = channelScreenModel.room ?: return + val context = LocalContext.current - Dialog( - onDismissRequest = { onCancel() }, - properties = - DialogProperties( - usePlatformDefaultWidth = false, - dismissOnClickOutside = false, - decorFitsSystemWindows = false, - ), - ) { - SetDialogToEdgeToEdge() - Scaffold( - topBar = { - TopAppBar( - scrollBehavior = rememberHeightDecreaser(), - modifier = Modifier, - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - NonClickableUserPictures( - room = room, - accountViewModel = accountViewModel, - size = Size34dp, - ) - - RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp), FontWeight.Normal, accountViewModel) - } - }, - navigationIcon = { - IconButton( - modifier = TitleIconModifier, - onClick = { - state.reset() - onCancel() - }, - ) { - ArrowBackIcon() - } - }, - actions = { - SendButton( - modifier = Modifier.padding(end = 5.dp), - onPost = upload, - isActive = state.canPost(), - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + ChatFileUploadDialog( + state, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + NonClickableUserPictures( + room = room, + accountViewModel = accountViewModel, + size = Size34dp, ) - }, - ) { pad -> - Surface( - modifier = - Modifier - .padding(pad) - .consumeWindowInsets(pad) - .imePadding(), - ) { - Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) { - Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) { - ImageVideoPostChat(state, accountViewModel) - } - } + + RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp), FontWeight.Normal, accountViewModel) } - } - } -} - -@Composable -private fun ImageVideoPostChat( - fileUploadState: ChatFileUploadState, - accountViewModel: AccountViewModel, -) { - val fileServers by accountViewModel.account.liveServerList.collectAsState() - - val fileServerOptions = - remember(fileServers) { - fileServers - .mapNotNull { - if (it.type != ServerType.NIP95) { - TitleExplainer(it.name, it.baseUrl) - } else { - null - } - }.toImmutableList() - } - - fileUploadState.multiOrchestrator?.let { - ShowImageUploadGallery( - it, - fileUploadState::deleteMediaToUpload, - accountViewModel, - ) - } - - OutlinedTextField( - label = { Text(text = stringRes(R.string.content_description)) }, - modifier = Modifier.fillMaxWidth().padding(top = 3.dp).height(150.dp), - maxLines = 10, - value = fileUploadState.caption, - onValueChange = { fileUploadState.caption = it }, - placeholder = { - Text( - text = stringRes(R.string.content_description_example), - color = MaterialTheme.colorScheme.placeholderText, - ) }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - ), - ) - - SettingSwitchItem( - title = R.string.add_sensitive_content_label, - description = R.string.add_sensitive_content_description, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - checked = fileUploadState.contentWarning, - onCheckedChange = fileUploadState::updateContentWarning, - ) - - SettingsRow(R.string.file_server, R.string.file_server_description) { - TextSpinner( - label = "", - placeholder = - fileServers - .firstOrNull { it == accountViewModel.account.settings.defaultFileServer } - ?.name - ?: fileServers[0].name, - options = fileServerOptions, - onSelect = { fileUploadState.selectedServer = fileServers[it] }, - ) - } - - Column( - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), - 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( - text = - when (fileUploadState.mediaQualitySlider) { - 0 -> stringRes(R.string.media_compression_quality_low) - 1 -> stringRes(R.string.media_compression_quality_medium) - 2 -> stringRes(R.string.media_compression_quality_high) - 3 -> stringRes(R.string.media_compression_quality_uncompressed) - else -> stringRes(R.string.media_compression_quality_medium) - }, - modifier = Modifier.align(Alignment.Center), + upload = { + channelScreenModel.upload( + onError = accountViewModel::toast, + context = context, + onceUploaded = onUpload, ) - } - Slider( - value = fileUploadState.mediaQualitySlider.toFloat(), - onValueChange = { fileUploadState.mediaQualitySlider = it.toInt() }, - valueRange = 0f..3f, - steps = 2, - ) - } -} - -@Composable -fun SendButton( - onPost: () -> Unit = {}, - isActive: Boolean, - modifier: Modifier = Modifier, -) { - Button( - modifier = modifier, - enabled = isActive, - onClick = onPost, - ) { - Text(text = stringRes(R.string.accessibility_send)) - } + if (state.selectedServer.type != ServerType.NIP95) { + accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer) + } + }, + onCancel, + accountViewModel, + nav, + ) } 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 ea44e754a9..f4b779c3c4 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 @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt new file mode 100644 index 0000000000..6b7def77bc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2024 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.screen.loggedIn.chats.publicChannels.send + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier +import com.vitorpamplona.amethyst.ui.theme.Size34dp + +@Composable +fun ChannelFileUploadDialog( + channelScreenModel: ChannelNewMessageViewModel, + state: ChatFileUploadState, + onUpload: () -> Unit, + onCancel: () -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) { + val channel = channelScreenModel.channel ?: return + val context = LocalContext.current + + ChatFileUploadDialog( + state, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + when (channel) { + is PublicChatChannel -> { + channel.profilePicture()?.let { + RobohashFallbackAsyncImage( + robot = channel.idHex, + model = it, + contentDescription = stringRes(R.string.profile_image), + contentScale = ContentScale.Crop, + modifier = HeaderPictureModifier, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, + loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + ) + } + + Column( + Modifier + .padding(start = 10.dp) + .height(35.dp) + .weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = channel.toBestDisplayName(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + is LiveActivitiesChannel -> { + channel.creator?.let { + UserPicture( + user = it, + size = Size34dp, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + Column( + modifier = + Modifier + .padding(start = 10.dp) + .height(35.dp) + .weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = channel.toBestDisplayName(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + } + }, + upload = { + channelScreenModel.upload( + onError = accountViewModel::toast, + context = context, + onceUploaded = onUpload, + ) + + if (state.selectedServer.type != ServerType.NIP95) { + accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer) + } + }, + onCancel, + accountViewModel, + nav, + ) +} 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 19be89ceca..9ac1ec88de 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 @@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.components.Split import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.UserSuggestions -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent @@ -307,6 +307,7 @@ open class ChannelNewMessageViewModel : ViewModel() { fun upload( onError: (title: String, message: String) -> Unit, context: Context, + onceUploaded: () -> Unit, ) { viewModelScope.launch(Dispatchers.Default) { val myAccount = account ?: return@launch @@ -349,6 +350,7 @@ open class ChannelNewMessageViewModel : ViewModel() { } uploadState.reset() + onceUploaded() } else { val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() 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 2148f4ce04..96f6dfaa8f 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 @@ -81,6 +81,19 @@ fun EditFieldRow( } } + channelScreenModel.uploadState?.let { uploading -> + uploading.multiOrchestrator?.let { selectedFiles -> + ChannelFileUploadDialog( + channelScreenModel = channelScreenModel, + state = uploading, + onUpload = onSendNewMessage, + onCancel = uploading::reset, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + Column( modifier = EditFieldModifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt new file mode 100644 index 0000000000..84cf9f4bf3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2024 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.screen.loggedIn.chats.utils + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +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.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery +import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.TitleIconModifier +import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser +import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingSwitchItem +import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner +import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.collections.immutable.toImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChatFileUploadDialog( + state: ChatFileUploadState, + title: @Composable () -> Unit, + upload: () -> Unit, + onCancel: () -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) { + val scrollState = rememberScrollState() + + Dialog( + onDismissRequest = { onCancel() }, + properties = + DialogProperties( + usePlatformDefaultWidth = false, + dismissOnClickOutside = false, + decorFitsSystemWindows = false, + ), + ) { + SetDialogToEdgeToEdge() + Scaffold( + topBar = { + TopAppBar( + scrollBehavior = rememberHeightDecreaser(), + modifier = Modifier, + title = title, + navigationIcon = { + IconButton( + modifier = TitleIconModifier, + onClick = { + state.reset() + onCancel() + }, + ) { + ArrowBackIcon() + } + }, + actions = { + SendButton( + modifier = Modifier.padding(end = 5.dp), + onPost = upload, + isActive = state.canPost(), + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { pad -> + Surface( + modifier = + Modifier + .padding(pad) + .consumeWindowInsets(pad) + .imePadding(), + ) { + Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) { + Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) { + ImageVideoPostChat(state, accountViewModel) + } + } + } + } + } +} + +@Composable +private fun ImageVideoPostChat( + fileUploadState: ChatFileUploadState, + accountViewModel: AccountViewModel, +) { + val fileServers by accountViewModel.account.liveServerList.collectAsState() + + val fileServerOptions = + remember(fileServers) { + fileServers + .mapNotNull { + if (it.type != ServerType.NIP95) { + TitleExplainer(it.name, it.baseUrl) + } else { + null + } + }.toImmutableList() + } + + fileUploadState.multiOrchestrator?.let { + ShowImageUploadGallery( + it, + fileUploadState::deleteMediaToUpload, + accountViewModel, + ) + } + + OutlinedTextField( + label = { Text(text = stringRes(R.string.content_description)) }, + modifier = Modifier.fillMaxWidth().padding(top = 3.dp).height(150.dp), + maxLines = 10, + value = fileUploadState.caption, + onValueChange = { fileUploadState.caption = it }, + placeholder = { + Text( + text = stringRes(R.string.content_description_example), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + ) + + SettingSwitchItem( + title = R.string.add_sensitive_content_label, + description = R.string.add_sensitive_content_description, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + checked = fileUploadState.contentWarning, + onCheckedChange = fileUploadState::updateContentWarning, + ) + + SettingsRow(R.string.file_server, R.string.file_server_description) { + TextSpinner( + label = "", + placeholder = + fileServers + .firstOrNull { it == accountViewModel.account.settings.defaultFileServer } + ?.name + ?: fileServers[0].name, + options = fileServerOptions, + onSelect = { fileUploadState.selectedServer = fileServers[it] }, + ) + } + + Column( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + 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( + text = + when (fileUploadState.mediaQualitySlider) { + 0 -> stringRes(R.string.media_compression_quality_low) + 1 -> stringRes(R.string.media_compression_quality_medium) + 2 -> stringRes(R.string.media_compression_quality_high) + 3 -> stringRes(R.string.media_compression_quality_uncompressed) + else -> stringRes(R.string.media_compression_quality_medium) + }, + modifier = Modifier.align(Alignment.Center), + ) + } + + Slider( + value = fileUploadState.mediaQualitySlider.toFloat(), + onValueChange = { fileUploadState.mediaQualitySlider = it.toInt() }, + valueRange = 0f..3f, + steps = 2, + ) + } +} + +@Composable +fun SendButton( + onPost: () -> Unit = {}, + isActive: Boolean, + modifier: Modifier = Modifier, +) { + Button( + modifier = modifier, + enabled = isActive, + onClick = onPost, + ) { + Text(text = stringRes(R.string.accessibility_send)) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index 3919a2cc5c..1252f75974 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -18,7 +18,7 @@ * 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.screen.loggedIn.chats.privateDM.send.upload +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue