diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index fc83584fb..353533471 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -105,6 +105,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListSel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.PicturesScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen @@ -374,6 +375,26 @@ fun BuildNavigation( ) } + composableFromBottomArgs { + PollPostScreen( + isZapPoll = false, + message = it.message, + draftId = it.draft, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + composableFromBottomArgs { + PollPostScreen( + isZapPoll = true, + message = it.message, + draftId = it.draft, + accountViewModel = accountViewModel, + nav = nav, + ) + } + composableFromBottomArgs { VoiceReplyScreen( replyToNoteId = it.replyToNoteId, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index f2a0c7424..cb5ddb66f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -360,6 +360,18 @@ sealed class Route { val draft: String? = null, ) : Route() + @Serializable + data class NewPoll( + val message: String? = null, + val draft: String? = null, + ) : Route() + + @Serializable + data class NewZapPoll( + val message: String? = null, + val draft: String? = null, + ) : Route() + @Serializable data class VoiceReply( val replyToNoteId: String, 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 625119771..871810b34 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 @@ -186,7 +186,7 @@ fun ShortNotePostScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun NewPostScreenInner( +internal fun NewPostScreenInner( postViewModel: ShortNotePostViewModel, accountViewModel: AccountViewModel, nav: Nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt index f30c361c4..f79711e74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt @@ -76,6 +76,9 @@ fun LongsScreen( } } }, + floatingButton = { + NewLongVideoButton(accountViewModel, nav, longsFeedContentState::sendToTop) + }, accountViewModel = accountViewModel, ) { paddingValues -> Column(Modifier.padding(paddingValues)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt new file mode 100644 index 000000000..3f889ad7b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt @@ -0,0 +1,195 @@ +/* + * 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.screen.loggedIn.longs + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddPhotoAlternate +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.NewMediaModel +import com.vitorpamplona.amethyst.ui.actions.NewMediaView +import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.painterRes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier +import com.vitorpamplona.amethyst.ui.theme.Size55Modifier +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Composable +fun NewLongVideoButton( + accountViewModel: AccountViewModel, + nav: INav, + navScrollToTop: () -> Unit, +) { + var isOpen by remember { mutableStateOf(false) } + var wantsToRecordVideo by remember { mutableStateOf(false) } + var wantsToPostFromGallery by remember { mutableStateOf(false) } + var pickedURIs by remember { mutableStateOf>(persistentListOf()) } + + val scope = rememberCoroutineScope() + val postViewModel: NewMediaModel = viewModel() + postViewModel.onceUploaded { + scope.launch(Dispatchers.IO) { + delay(500) + withContext(Dispatchers.Main) { navScrollToTop() } + } + } + + if (wantsToRecordVideo) { + TakeVideo { uri -> + wantsToRecordVideo = false + pickedURIs = uri + } + } + + if (wantsToPostFromGallery) { + GallerySelect( + onImageUri = { uri -> + wantsToPostFromGallery = false + pickedURIs = uri + }, + ) + } + + if (pickedURIs.isNotEmpty()) { + NewMediaView( + uris = pickedURIs, + onClose = { pickedURIs = persistentListOf() }, + postViewModel = postViewModel, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + Column { + AnimatedVisibility( + visible = isOpen, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + Column { + FloatingActionButton( + onClick = { + wantsToRecordVideo = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Default.Videocam, + contentDescription = stringRes(id = R.string.record_a_video), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + FloatingActionButton( + onClick = { + wantsToPostFromGallery = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Default.AddPhotoAlternate, + contentDescription = stringRes(id = R.string.upload_image), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } + } + + FloatingActionButton( + onClick = { isOpen = !isOpen }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + AnimatedVisibility( + visible = isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringRes(id = R.string.new_long_video), + modifier = Size26Modifier, + tint = Color.White, + ) + } + + AnimatedVisibility( + visible = !isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + painter = painterRes(R.drawable.ic_compose, 5), + contentDescription = stringRes(id = R.string.new_long_video), + modifier = Size26Modifier, + tint = Color.White, + ) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt new file mode 100644 index 000000000..964ef3250 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt @@ -0,0 +1,195 @@ +/* + * 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.screen.loggedIn.pictures + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddPhotoAlternate +import androidx.compose.material.icons.filled.CameraAlt +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.NewMediaModel +import com.vitorpamplona.amethyst.ui.actions.NewMediaView +import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.actions.uploads.TakePicture +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.painterRes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier +import com.vitorpamplona.amethyst.ui.theme.Size55Modifier +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Composable +fun NewPictureButton( + accountViewModel: AccountViewModel, + nav: INav, + navScrollToTop: () -> Unit, +) { + var isOpen by remember { mutableStateOf(false) } + var wantsToPostFromCamera by remember { mutableStateOf(false) } + var wantsToPostFromGallery by remember { mutableStateOf(false) } + var pickedURIs by remember { mutableStateOf>(persistentListOf()) } + + val scope = rememberCoroutineScope() + val postViewModel: NewMediaModel = viewModel() + postViewModel.onceUploaded { + scope.launch(Dispatchers.IO) { + delay(500) + withContext(Dispatchers.Main) { navScrollToTop() } + } + } + + if (wantsToPostFromCamera) { + TakePicture { uri -> + wantsToPostFromCamera = false + pickedURIs = uri + } + } + + if (wantsToPostFromGallery) { + GallerySelect( + onImageUri = { uri -> + wantsToPostFromGallery = false + pickedURIs = uri + }, + ) + } + + if (pickedURIs.isNotEmpty()) { + NewMediaView( + uris = pickedURIs, + onClose = { pickedURIs = persistentListOf() }, + postViewModel = postViewModel, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + Column { + AnimatedVisibility( + visible = isOpen, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + Column { + FloatingActionButton( + onClick = { + wantsToPostFromCamera = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Default.CameraAlt, + contentDescription = stringRes(id = R.string.take_a_picture), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + FloatingActionButton( + onClick = { + wantsToPostFromGallery = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Default.AddPhotoAlternate, + contentDescription = stringRes(id = R.string.upload_image), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } + } + + FloatingActionButton( + onClick = { isOpen = !isOpen }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + AnimatedVisibility( + visible = isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringRes(id = R.string.new_picture), + modifier = Size26Modifier, + tint = Color.White, + ) + } + + AnimatedVisibility( + visible = !isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + painter = painterRes(R.drawable.ic_compose, 5), + contentDescription = stringRes(id = R.string.new_picture), + modifier = Size26Modifier, + tint = Color.White, + ) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt index 7442d2ee9..89b1629d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt @@ -76,6 +76,9 @@ fun PicturesScreen( } } }, + floatingButton = { + NewPictureButton(accountViewModel, nav, picturesFeedContentState::sendToTop) + }, accountViewModel = accountViewModel, ) { paddingValues -> Column(Modifier.padding(paddingValues)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/NewPollButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/NewPollButton.kt new file mode 100644 index 000000000..a51a5b7fe --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/NewPollButton.kt @@ -0,0 +1,138 @@ +/* + * 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.screen.loggedIn.polls + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Poll +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.painterRes +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier +import com.vitorpamplona.amethyst.ui.theme.Size55Modifier + +@Composable +fun NewPollButton(nav: INav) { + var isOpen by remember { mutableStateOf(false) } + + Column { + AnimatedVisibility( + visible = isOpen, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + Column { + FloatingActionButton( + onClick = { + nav.nav(Route.NewPoll()) + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Outlined.Poll, + contentDescription = stringRes(id = R.string.new_regular_poll), + modifier = Size26Modifier, + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + FloatingActionButton( + onClick = { + nav.nav(Route.NewZapPoll()) + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + painter = painterRes(R.drawable.ic_poll, 1), + contentDescription = stringRes(id = R.string.new_zap_poll), + modifier = Size26Modifier, + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } + } + + FloatingActionButton( + onClick = { isOpen = !isOpen }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + AnimatedVisibility( + visible = isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringRes(id = R.string.new_poll), + modifier = Size26Modifier, + tint = Color.White, + ) + } + + AnimatedVisibility( + visible = !isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + painter = painterRes(R.drawable.ic_compose, 4), + contentDescription = stringRes(id = R.string.new_poll), + modifier = Size26Modifier, + tint = Color.White, + ) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt new file mode 100644 index 000000000..288592f7e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt @@ -0,0 +1,62 @@ +/* + * 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.screen.loggedIn.polls + +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlinx.coroutines.FlowPreview + +@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) +@Composable +fun PollPostScreen( + isZapPoll: Boolean, + message: String? = null, + draftId: HexKey? = null, + accountViewModel: AccountViewModel, + nav: Nav, +) { + val postViewModel: ShortNotePostViewModel = viewModel() + postViewModel.init(accountViewModel) + + LaunchedEffect(postViewModel, accountViewModel) { + val draft = draftId?.let { accountViewModel.getNoteIfExists(it) } + postViewModel.load(null, null, null, null, draft) + message?.ifBlank { null }?.let { + postViewModel.message.setTextAndPlaceCursorAtEnd(it) + postViewModel.onMessageChanged() + } + if (isZapPoll) { + postViewModel.wantsZapPoll = true + } else { + postViewModel.wantsPoll = true + } + } + + NewPostScreenInner(postViewModel, accountViewModel, nav) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt index 973fea9e9..b765a25dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt @@ -76,6 +76,9 @@ fun PollsScreen( } } }, + floatingButton = { + NewPollButton(nav) + }, accountViewModel = accountViewModel, ) { paddingValues -> Column(Modifier.padding(paddingValues)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt new file mode 100644 index 000000000..37f4220cc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt @@ -0,0 +1,195 @@ +/* + * 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.screen.loggedIn.shorts + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddPhotoAlternate +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.NewMediaModel +import com.vitorpamplona.amethyst.ui.actions.NewMediaView +import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.painterRes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier +import com.vitorpamplona.amethyst.ui.theme.Size55Modifier +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Composable +fun NewShortVideoButton( + accountViewModel: AccountViewModel, + nav: INav, + navScrollToTop: () -> Unit, +) { + var isOpen by remember { mutableStateOf(false) } + var wantsToRecordVideo by remember { mutableStateOf(false) } + var wantsToPostFromGallery by remember { mutableStateOf(false) } + var pickedURIs by remember { mutableStateOf>(persistentListOf()) } + + val scope = rememberCoroutineScope() + val postViewModel: NewMediaModel = viewModel() + postViewModel.onceUploaded { + scope.launch(Dispatchers.IO) { + delay(500) + withContext(Dispatchers.Main) { navScrollToTop() } + } + } + + if (wantsToRecordVideo) { + TakeVideo { uri -> + wantsToRecordVideo = false + pickedURIs = uri + } + } + + if (wantsToPostFromGallery) { + GallerySelect( + onImageUri = { uri -> + wantsToPostFromGallery = false + pickedURIs = uri + }, + ) + } + + if (pickedURIs.isNotEmpty()) { + NewMediaView( + uris = pickedURIs, + onClose = { pickedURIs = persistentListOf() }, + postViewModel = postViewModel, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + Column { + AnimatedVisibility( + visible = isOpen, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + Column { + FloatingActionButton( + onClick = { + wantsToRecordVideo = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Default.Videocam, + contentDescription = stringRes(id = R.string.record_a_video), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + FloatingActionButton( + onClick = { + wantsToPostFromGallery = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Default.AddPhotoAlternate, + contentDescription = stringRes(id = R.string.upload_image), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } + } + + FloatingActionButton( + onClick = { isOpen = !isOpen }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + AnimatedVisibility( + visible = isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringRes(id = R.string.new_short_video), + modifier = Size26Modifier, + tint = Color.White, + ) + } + + AnimatedVisibility( + visible = !isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + painter = painterRes(R.drawable.ic_compose, 5), + contentDescription = stringRes(id = R.string.new_short_video), + modifier = Size26Modifier, + tint = Color.White, + ) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt index 68ed14421..a993b7f16 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt @@ -76,6 +76,9 @@ fun ShortsScreen( } } }, + floatingButton = { + NewShortVideoButton(accountViewModel, nav, shortsFeedContentState::sendToTop) + }, accountViewModel = accountViewModel, ) { paddingValues -> Column(Modifier.padding(paddingValues)) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 866522906..7a5b83da7 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1340,6 +1340,12 @@ New Product New Geo-Exclusive Post New Article + New Poll + New Zap Poll + New Regular Poll + New Picture + New Short Video + New Long Video Title Summary (optional)