diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListScreen.kt index 7502332a4..8e3522c9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -43,10 +42,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults.cardElevation @@ -97,7 +94,6 @@ import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.note.ClearTextIcon import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture -import com.vitorpamplona.amethyst.ui.note.UserComposeNoAction import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.AnimateOnNewSearch @@ -107,9 +103,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier -import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer import com.vitorpamplona.amethyst.ui.theme.LightRedColor import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect @@ -120,13 +113,11 @@ import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import java.lang.reflect.Modifier.isPrivate @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -219,7 +210,7 @@ private fun ListViewAndEditColumn( nav: INav, ) { Column(modifier = modifier) { - FollowSetListView( + PeopleListPager( viewModel = viewModel, pagerState = pagerState, modifier = Modifier.weight(1f), @@ -312,7 +303,7 @@ private fun RenderAddUserFieldAndSuggestions( } @Composable -private fun FollowSetListView( +private fun PeopleListPager( viewModel: PeopleListViewModel, pagerState: PagerState, modifier: Modifier, @@ -325,7 +316,7 @@ private fun FollowSetListView( HorizontalPager(state = pagerState, modifier) { page -> when (page) { 0 -> - FollowSetListView( + PeopleListView( memberList = selectedSet.privateMembersList, onDeleteUser = { user -> onDeleteUser(user, true) @@ -336,7 +327,7 @@ private fun FollowSetListView( ) 1 -> - FollowSetListView( + PeopleListView( memberList = selectedSet.publicMembersList, onDeleteUser = { user -> onDeleteUser(user, false) @@ -361,7 +352,7 @@ fun FollowSetListViewPreview() { ThemeComparisonRow { Column { - FollowSetListView( + PeopleListView( memberList = persistentListOf(user1, user2, user3), onDeleteUser = { user -> }, accountViewModel = accountViewModel, @@ -419,33 +410,6 @@ fun FollowSetListViewPreview() { } } -@Composable -private fun FollowSetListView( - memberList: ImmutableList, - modifier: Modifier = Modifier, - onDeleteUser: (User) -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) { - val listState = rememberLazyListState() - - LazyColumn( - modifier = modifier, - contentPadding = FeedPadding, - state = listState, - ) { - itemsIndexed(memberList, key = { _, item -> "u" + item.pubkeyHex }) { _, item -> - FollowSetListItem( - modifier = Modifier.animateContentSize(), - user = item, - accountViewModel = accountViewModel, - nav = nav, - onDeleteUser = onDeleteUser, - ) - } - } -} - @Composable fun ShowUserSuggestions( userSuggestions: UserSuggestionState, @@ -569,47 +533,6 @@ fun RowScope.HasUserTag( } } -@Composable -fun FollowSetListItem( - modifier: Modifier = Modifier, - user: User, - accountViewModel: AccountViewModel, - nav: INav, - onDeleteUser: (User) -> Unit, -) { - Column( - modifier = modifier, - ) { - Row(HalfHalfHorzModifier) { - UserComposeNoAction( - user, - modifier = HalfPadding.weight(1f, fill = false), - accountViewModel = accountViewModel, - nav = nav, - ) - IconButton( - onClick = { - onDeleteUser(user) - }, - modifier = - HalfPadding - .align(Alignment.CenterVertically) - .background( - color = MaterialTheme.colorScheme.errorContainer, - shape = RoundedCornerShape(percent = 80), - ), - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - } - } - HorizontalDivider(thickness = DividerThickness) - } -} - @Composable fun ListActionsMenuButton( viewModel: PeopleListViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt new file mode 100644 index 000000000..d8069b5a6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt @@ -0,0 +1,119 @@ +/** + * 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.lists.display + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.UserComposeNoAction +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.PeopleListItem +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier +import com.vitorpamplona.amethyst.ui.theme.HalfPadding +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun PeopleListView( + memberList: ImmutableList, + modifier: Modifier = Modifier, + onDeleteUser: (User) -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) { + val listState = rememberLazyListState() + + LazyColumn( + modifier = modifier, + contentPadding = FeedPadding, + state = listState, + ) { + itemsIndexed(memberList, key = { _, item -> "u" + item.pubkeyHex }) { _, item -> + PeopleListItem( + modifier = Modifier.animateContentSize(), + user = item, + accountViewModel = accountViewModel, + nav = nav, + onDeleteUser = onDeleteUser, + ) + } + } +} + +@Composable +fun PeopleListItem( + modifier: Modifier = Modifier, + user: User, + accountViewModel: AccountViewModel, + nav: INav, + onDeleteUser: (User) -> Unit, +) { + Column( + modifier = modifier, + ) { + Row(HalfHalfHorzModifier) { + UserComposeNoAction( + user, + modifier = HalfPadding.weight(1f, fill = false), + accountViewModel = accountViewModel, + nav = nav, + ) + IconButton( + onClick = { + onDeleteUser(user) + }, + modifier = + HalfPadding + .align(Alignment.CenterVertically) + .background( + color = MaterialTheme.colorScheme.errorContainer, + shape = RoundedCornerShape(percent = 80), + ), + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + } + } + HorizontalDivider(thickness = DividerThickness) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/ListOfPeopleListsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/ListOfPeopleListsScreen.kt index 387ffd5d9..627ea4534 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/ListOfPeopleListsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/ListOfPeopleListsScreen.kt @@ -20,29 +20,21 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.PlaylistAdd -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -50,7 +42,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import kotlinx.coroutines.flow.StateFlow @Composable @@ -184,70 +175,3 @@ private fun PeopleListFabsAndMenu(onAddSet: (name: String, description: String?) ) } } - -@Composable -fun NewPeopleListCreationDialog( - modifier: Modifier = Modifier, - onDismiss: () -> Unit, - onCreateList: (name: String, description: String?) -> Unit, -) { - val newListName = remember { mutableStateOf("") } - val newListDescription = remember { mutableStateOf(null) } - - AlertDialog( - onDismissRequest = onDismiss, - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringRes(R.string.follow_set_creation_dialog_title), - ) - } - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(5.dp), - ) { - // For the new list name - TextField( - value = newListName.value, - onValueChange = { newListName.value = it }, - label = { - Text(text = stringRes(R.string.follow_set_creation_name_label)) - }, - ) - Spacer(modifier = DoubleVertSpacer) - // For the set description - TextField( - value = - ( - if (newListDescription.value != null) newListDescription.value else "" - ).toString(), - onValueChange = { newListDescription.value = it }, - label = { - Text(text = stringRes(R.string.follow_set_creation_desc_label)) - }, - ) - } - }, - confirmButton = { - Button( - onClick = { - onCreateList(newListName.value, newListDescription.value) - onDismiss() - }, - ) { - Text(stringRes(R.string.follow_set_creation_action_btn_label)) - } - }, - dismissButton = { - Button( - onClick = onDismiss, - ) { - Text(stringRes(R.string.cancel)) - } - }, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/NewPeopleListCreationDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/NewPeopleListCreationDialog.kt new file mode 100644 index 000000000..0ce4be896 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/NewPeopleListCreationDialog.kt @@ -0,0 +1,107 @@ +/** + * 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.lists.list + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer + +@Composable +fun NewPeopleListCreationDialog( + modifier: Modifier = Modifier, + onDismiss: () -> Unit, + onCreateList: (name: String, description: String?) -> Unit, +) { + val newListName = remember { mutableStateOf("") } + val newListDescription = remember { mutableStateOf(null) } + + AlertDialog( + modifier = modifier, + onDismissRequest = onDismiss, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringRes(R.string.follow_set_creation_dialog_title), + ) + } + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(5.dp), + ) { + // For the new list name + TextField( + value = newListName.value, + onValueChange = { newListName.value = it }, + label = { + Text(text = stringRes(R.string.follow_set_creation_name_label)) + }, + ) + Spacer(modifier = DoubleVertSpacer) + // For the set description + TextField( + value = + ( + if (newListDescription.value != null) newListDescription.value else "" + ).toString(), + onValueChange = { newListDescription.value = it }, + label = { + Text(text = stringRes(R.string.follow_set_creation_desc_label)) + }, + ) + } + }, + confirmButton = { + Button( + onClick = { + onCreateList(newListName.value, newListDescription.value) + onDismiss() + }, + ) { + Text(stringRes(R.string.follow_set_creation_action_btn_label)) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + ) { + Text(stringRes(R.string.cancel)) + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt index 7e160fe9b..f97b712e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt @@ -90,7 +90,7 @@ private fun PeopleListItemPreview() { val samplePeopleList2 = PeopleList( - identifierTag = "00001-2222", + identifierTag = "00001-2223", title = "Sample List Title", description = "Sample List Description", setOf(user1, user3), @@ -99,7 +99,7 @@ private fun PeopleListItemPreview() { val samplePeopleList3 = PeopleList( - identifierTag = "00001-2222", + identifierTag = "00001-2224", title = "Sample List Title", description = "Sample List Description", emptySet(), @@ -108,7 +108,7 @@ private fun PeopleListItemPreview() { val samplePeopleList4 = PeopleList( - identifierTag = "00001-2222", + identifierTag = "00001-2225", title = "Sample List Title", description = "Sample List Description", setOf(user3), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/EditPeopleListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/EditPeopleListScreen.kt deleted file mode 100644 index 5bbaf2d5b..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/EditPeopleListScreen.kt +++ /dev/null @@ -1,451 +0,0 @@ -/** - * 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.lists.memberEdit - -import androidx.compose.foundation.background -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.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.recalculateWindowInsets -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.PlaylistAdd -import androidx.compose.material.icons.filled.PersonAdd -import androidx.compose.material.icons.filled.PersonRemove -import androidx.compose.material.icons.outlined.Groups -import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material.icons.outlined.Public -import androidx.compose.material.icons.outlined.RemoveCircleOutline -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -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.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName -import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.DisplayParticipantNumberAndStatus -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewPeopleListCreationDialog -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding -import com.vitorpamplona.amethyst.ui.theme.Size15Modifier -import com.vitorpamplona.amethyst.ui.theme.Size50ModifierOffset10 -import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.quartz.nip01Core.core.HexKey - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun EditPeopleListScreen( - userToAddOrRemove: HexKey, - accountViewModel: AccountViewModel, - nav: INav, -) { - var userBase by remember { mutableStateOf(LocalCache.getUserIfExists(userToAddOrRemove)) } - - if (userBase == null) { - LaunchedEffect(userToAddOrRemove) { - val newUserBase = LocalCache.checkGetOrCreateUser(userToAddOrRemove) - if (newUserBase != userBase) { - userBase = newUserBase - } - } - } - - userBase?.let { - EditPeopleListScreen(it, accountViewModel, nav) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun EditPeopleListScreen( - userToAddOrRemove: User, - accountViewModel: AccountViewModel, - nav: INav, -) { - Scaffold( - modifier = - Modifier - .fillMaxSize() - .recalculateWindowInsets(), - floatingActionButton = { - PeopleListFabsAndMenu(accountViewModel) - }, - topBar = { - TopBarWithBackButton(stringRes(id = R.string.follow_set_man_dialog_title2, userToAddOrRemove.toBestDisplayName()), nav::popBack) - }, - ) { contentPadding -> - Column( - modifier = - Modifier - .padding( - top = contentPadding.calculateTopPadding(), - bottom = contentPadding.calculateBottomPadding(), - ).consumeWindowInsets(contentPadding) - .imePadding(), - ) { - FollowSetManagementScreenBody(userToAddOrRemove, accountViewModel, nav) - } - } -} - -@Composable -private fun PeopleListFabsAndMenu(accountViewModel: AccountViewModel) { - var isOpen by remember { mutableStateOf(false) } - - ExtendedFloatingActionButton( - text = { - Text(text = stringRes(R.string.follow_set_create_btn_label)) - }, - icon = { - Icon( - imageVector = Icons.AutoMirrored.Filled.PlaylistAdd, - contentDescription = null, - ) - }, - onClick = { isOpen = !isOpen }, - shape = CircleShape, - containerColor = MaterialTheme.colorScheme.primary, - ) - - if (isOpen) { - NewPeopleListCreationDialog( - onDismiss = { - isOpen = false - }, - onCreateList = { name, description -> - accountViewModel.runIOCatching { - accountViewModel.account.peopleLists.addFollowList( - listName = name, - listDescription = description, - account = accountViewModel.account, - ) - } - isOpen = false - }, - ) - } -} - -@Composable -private fun FollowSetManagementScreenBody( - userToAddOrRemove: User, - accountViewModel: AccountViewModel, - nav: INav, -) { - val followSetsState by accountViewModel.account.peopleLists.uiListFlow - .collectAsStateWithLifecycle() - - if (followSetsState.isEmpty()) { - EmptyOrNoneFound() - } else { - val userName by observeUserName(userToAddOrRemove, accountViewModel) - - LazyColumn(modifier = Modifier.fillMaxWidth()) { - itemsIndexed(followSetsState, key = { _, item -> item.identifierTag }) { _, list -> - FollowSetItem( - modifier = Modifier.fillMaxWidth(), - listHeader = list.title, - listDescription = list.description ?: "", - userName = userName, - userIsPrivateMember = list.privateMembers.contains(userToAddOrRemove), - userIsPublicMember = list.publicMembers.contains(userToAddOrRemove), - onRemoveUser = { - accountViewModel.runIOCatching { - accountViewModel.account.peopleLists.removeUserFromSet( - userToAddOrRemove, - isPrivate = list.privateMembers.contains(userToAddOrRemove), - list.identifierTag, - accountViewModel.account, - ) - } - }, - privateMemberSize = list.privateMembers.size, - publicMemberSize = list.publicMembers.size, - onAddUserToList = { userShouldBePrivate -> - accountViewModel.runIOCatching { - accountViewModel.account.peopleLists.addUserToSet( - userToAddOrRemove, - list.identifierTag, - userShouldBePrivate, - accountViewModel.account, - ) - } - }, - ) - HorizontalDivider(thickness = DividerThickness) - } - } - } -} - -@Composable -private fun EmptyOrNoneFound() { - Column( - Modifier - .fillMaxWidth() - .fillMaxHeight(0.5f), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = stringRes(R.string.follow_set_empty_dialog_msg)) - Spacer(modifier = StdVertSpacer) - } -} - -@Preview -@Composable -fun FollowSetItemMemberPreview() { - ThemeComparisonColumn { - FollowSetItem( - modifier = Modifier.fillMaxWidth(), - listHeader = "list title", - listDescription = "desc", - userName = "User", - userIsPrivateMember = true, - userIsPublicMember = true, - privateMemberSize = 3, - publicMemberSize = 2, - onAddUserToList = {}, - onRemoveUser = {}, - ) - } -} - -@Preview -@Composable -fun FollowSetItemNotMemberPreview() { - ThemeComparisonColumn { - FollowSetItem( - modifier = Modifier.fillMaxWidth(), - listHeader = "list title", - listDescription = "desc", - userName = "User", - userIsPrivateMember = false, - userIsPublicMember = false, - privateMemberSize = 3, - publicMemberSize = 2, - onAddUserToList = {}, - onRemoveUser = {}, - ) - } -} - -@Composable -fun FollowSetItem( - modifier: Modifier = Modifier, - listHeader: String, - listDescription: String, - userName: String, - userIsPrivateMember: Boolean, - userIsPublicMember: Boolean, - publicMemberSize: Int, - privateMemberSize: Int, - onAddUserToList: (shouldBePrivateMember: Boolean) -> Unit, - onRemoveUser: () -> Unit, -) { - val isUserInList = userIsPrivateMember || userIsPublicMember - - ListItem( - modifier = modifier, - headlineContent = { - Text(listHeader, maxLines = 1, overflow = TextOverflow.Ellipsis) - }, - supportingContent = { - Row( - modifier = HalfHalfVertPadding, - horizontalArrangement = SpacedBy5dp, - verticalAlignment = Alignment.CenterVertically, - ) { - val text = - if (isUserInList) { - if (userIsPublicMember) { - stringRes(R.string.follow_set_public_presence_indicator, userName) - } else { - stringRes(R.string.follow_set_private_presence_indicator, userName) - } - } else { - stringRes(R.string.follow_set_absence_indicator2, userName) - } - - if (isUserInList) { - if (userIsPublicMember) { - Icon( - imageVector = Icons.Outlined.Public, - contentDescription = text, - modifier = Size15Modifier, - tint = MaterialTheme.colorScheme.primary, - ) - } else if (userIsPrivateMember) { - Icon( - imageVector = Icons.Outlined.Lock, - contentDescription = text, - modifier = Size15Modifier, - tint = MaterialTheme.colorScheme.primary, - ) - } - } else { - Icon( - imageVector = Icons.Outlined.RemoveCircleOutline, - contentDescription = text, - modifier = Size15Modifier, - ) - } - Text( - text = text, - overflow = TextOverflow.MiddleEllipsis, - maxLines = 1, - ) - } - }, - leadingContent = { - Box(contentAlignment = Alignment.Center) { - Icon( - imageVector = Icons.Outlined.Groups, - contentDescription = stringRes(R.string.follow_set_icon_description), - modifier = Size50ModifierOffset10, - ) - DisplayParticipantNumberAndStatus( - modifier = Modifier.align(Alignment.BottomCenter), - privateMembersSize = privateMemberSize, - publicMembersSize = publicMemberSize, - ) - } - }, - trailingContent = { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - val isUserAddTapped = remember { mutableStateOf(false) } - IconButton( - onClick = { - if (isUserInList) { - onRemoveUser() - } else { - isUserAddTapped.value = true - } - }, - modifier = - Modifier - .background( - color = - if (isUserInList) { - MaterialTheme.colorScheme.errorContainer - } else { - MaterialTheme.colorScheme.primary - }, - shape = RoundedCornerShape(percent = 80), - ), - ) { - if (isUserInList) { - Icon( - imageVector = Icons.Filled.PersonRemove, - contentDescription = stringRes(R.string.remove_user_from_the_list), - tint = MaterialTheme.colorScheme.onBackground, - ) - } else { - Icon( - imageVector = Icons.Filled.PersonAdd, - contentDescription = stringRes(R.string.add_user_to_the_list), - tint = Color.White, - ) - } - } - - UserAdditionOptionsMenu( - isExpanded = isUserAddTapped.value, - onUserAdd = { shouldBePrivateMember -> - onAddUserToList(shouldBePrivateMember) - }, - onDismiss = { isUserAddTapped.value = false }, - ) - } - }, - ) -} - -@Composable -private fun UserAdditionOptionsMenu( - isExpanded: Boolean, - onUserAdd: (asPrivateMember: Boolean) -> Unit, - onDismiss: () -> Unit, -) { - DropdownMenu( - expanded = isExpanded, - onDismissRequest = onDismiss, - ) { - DropdownMenuItem( - text = { - Text(text = stringRes(R.string.follow_set_public_member_add_label)) - }, - onClick = { - onUserAdd(false) - onDismiss() - }, - ) - DropdownMenuItem( - text = { - Text(text = stringRes(R.string.follow_set_private_member_add_label)) - }, - onClick = { - onUserAdd(true) - onDismiss() - }, - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserItem.kt new file mode 100644 index 000000000..70acacfce --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserItem.kt @@ -0,0 +1,255 @@ +/** + * 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.lists.memberEdit + +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PersonAdd +import androidx.compose.material.icons.filled.PersonRemove +import androidx.compose.material.icons.outlined.Groups +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Public +import androidx.compose.material.icons.outlined.RemoveCircleOutline +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.DisplayParticipantNumberAndStatus +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier +import com.vitorpamplona.amethyst.ui.theme.Size50ModifierOffset10 +import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn + +@Preview +@Composable +fun PeopleListAndUserMemberPreview() { + ThemeComparisonColumn { + PeopleListAndUserItem( + modifier = Modifier.fillMaxWidth(), + listHeader = "list title", + userName = "User", + userIsPrivateMember = true, + userIsPublicMember = true, + privateMemberSize = 3, + publicMemberSize = 2, + onAddUserToList = {}, + onRemoveUser = {}, + ) + } +} + +@Preview +@Composable +fun PeopleListAndUserNotMemberPreview() { + ThemeComparisonColumn { + PeopleListAndUserItem( + modifier = Modifier.fillMaxWidth(), + listHeader = "list title", + userName = "User", + userIsPrivateMember = false, + userIsPublicMember = false, + privateMemberSize = 3, + publicMemberSize = 2, + onAddUserToList = {}, + onRemoveUser = {}, + ) + } +} + +@Composable +fun PeopleListAndUserItem( + modifier: Modifier = Modifier, + listHeader: String, + userName: String, + userIsPrivateMember: Boolean, + userIsPublicMember: Boolean, + publicMemberSize: Int, + privateMemberSize: Int, + onAddUserToList: (shouldBePrivateMember: Boolean) -> Unit, + onRemoveUser: () -> Unit, +) { + ListItem( + modifier = modifier, + headlineContent = { + Text( + text = listHeader, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + supportingContent = { + UserStatusInList(userName, userIsPrivateMember, userIsPublicMember) + }, + leadingContent = { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Outlined.Groups, + contentDescription = stringRes(R.string.follow_set_icon_description), + modifier = Size50ModifierOffset10, + ) + DisplayParticipantNumberAndStatus( + modifier = Modifier.align(Alignment.BottomCenter), + privateMembersSize = privateMemberSize, + publicMembersSize = publicMemberSize, + ) + } + }, + trailingContent = { + val isUserInList = userIsPrivateMember || userIsPublicMember + UserAdditionOptions(isUserInList, onAddUserToList, onRemoveUser) + }, + ) +} + +@Composable +fun UserStatusInList( + userName: String, + userIsPrivateMember: Boolean, + userIsPublicMember: Boolean, +) { + Row( + modifier = HalfHalfVertPadding, + horizontalArrangement = SpacedBy5dp, + verticalAlignment = Alignment.CenterVertically, + ) { + val text = + if (userIsPublicMember) { + stringRes(R.string.follow_set_public_presence_indicator, userName) + } else if (userIsPrivateMember) { + stringRes(R.string.follow_set_private_presence_indicator, userName) + } else { + stringRes(R.string.follow_set_absence_indicator2, userName) + } + + val icon = + if (userIsPublicMember) { + Icons.Outlined.Public + } else if (userIsPrivateMember) { + Icons.Outlined.Lock + } else { + Icons.Outlined.RemoveCircleOutline + } + + Icon( + imageVector = icon, + contentDescription = text, + modifier = Size15Modifier, + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = text, + overflow = TextOverflow.MiddleEllipsis, + maxLines = 1, + ) + } +} + +@Composable +private fun UserAdditionOptions( + isUserInList: Boolean, + onAddUserToList: (asPrivateMember: Boolean) -> Unit, + onRemoveUser: () -> Unit, +) { + val isUserAddTapped = remember { mutableStateOf(false) } + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconButton( + onClick = { + if (isUserInList) { + onRemoveUser() + } else { + isUserAddTapped.value = true + } + }, + modifier = + Modifier + .background( + color = + if (isUserInList) { + MaterialTheme.colorScheme.errorContainer + } else { + MaterialTheme.colorScheme.primary + }, + shape = RoundedCornerShape(percent = 80), + ), + ) { + if (isUserInList) { + Icon( + imageVector = Icons.Filled.PersonRemove, + contentDescription = stringRes(R.string.remove_user_from_the_list), + tint = MaterialTheme.colorScheme.onErrorContainer, + ) + } else { + Icon( + imageVector = Icons.Filled.PersonAdd, + contentDescription = stringRes(R.string.add_user_to_the_list), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } + + DropdownMenu( + expanded = isUserAddTapped.value, + onDismissRequest = { isUserAddTapped.value = false }, + ) { + DropdownMenuItem( + text = { + Text(text = stringRes(R.string.follow_set_public_member_add_label)) + }, + onClick = { + onAddUserToList(false) + isUserAddTapped.value = false + }, + ) + DropdownMenuItem( + text = { + Text(text = stringRes(R.string.follow_set_private_member_add_label)) + }, + onClick = { + onAddUserToList(true) + isUserAddTapped.value = false + }, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserScreen.kt new file mode 100644 index 000000000..935f3f1e1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserScreen.kt @@ -0,0 +1,149 @@ +/** + * 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.lists.memberEdit + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.recalculateWindowInsets +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.PlaylistAdd +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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 com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewPeopleListCreationDialog +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditPeopleListScreen( + userToAddOrRemove: HexKey, + accountViewModel: AccountViewModel, + nav: INav, +) { + var userBase by remember { mutableStateOf(LocalCache.getUserIfExists(userToAddOrRemove)) } + + if (userBase == null) { + LaunchedEffect(userToAddOrRemove) { + val newUserBase = LocalCache.checkGetOrCreateUser(userToAddOrRemove) + if (newUserBase != userBase) { + userBase = newUserBase + } + } + } + + userBase?.let { + EditPeopleListScreen(it, accountViewModel, nav) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditPeopleListScreen( + userToAddOrRemove: User, + accountViewModel: AccountViewModel, + nav: INav, +) { + Scaffold( + modifier = Modifier.fillMaxSize().recalculateWindowInsets(), + floatingActionButton = { + PeopleListAndUserFab(accountViewModel) + }, + topBar = { + val userName by observeUserName(userToAddOrRemove, accountViewModel) + TopBarWithBackButton( + caption = stringRes(id = R.string.follow_set_man_dialog_title2, userName), + popBack = nav::popBack, + ) + }, + ) { contentPadding -> + Column( + modifier = + Modifier + .padding( + top = contentPadding.calculateTopPadding(), + bottom = contentPadding.calculateBottomPadding(), + ).consumeWindowInsets(contentPadding) + .imePadding(), + ) { + PeopleListAndUserView(userToAddOrRemove, accountViewModel, nav) + } + } +} + +@Composable +private fun PeopleListAndUserFab(accountViewModel: AccountViewModel) { + var isOpen by remember { mutableStateOf(false) } + + ExtendedFloatingActionButton( + text = { + Text(text = stringRes(R.string.follow_set_create_btn_label)) + }, + icon = { + Icon( + imageVector = Icons.AutoMirrored.Filled.PlaylistAdd, + contentDescription = null, + ) + }, + onClick = { isOpen = !isOpen }, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) + + if (isOpen) { + NewPeopleListCreationDialog( + onDismiss = { + isOpen = false + }, + onCreateList = { name, description -> + accountViewModel.runIOCatching { + accountViewModel.account.peopleLists.addFollowList( + listName = name, + listDescription = description, + account = accountViewModel.account, + ) + } + isOpen = false + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserView.kt new file mode 100644 index 000000000..09ff56919 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/PeopleListAndUserView.kt @@ -0,0 +1,104 @@ +/** + * 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.lists.memberEdit + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun PeopleListAndUserView( + userToAddOrRemove: User, + accountViewModel: AccountViewModel, + nav: INav, +) { + val followSetsState by accountViewModel.account.peopleLists.uiListFlow + .collectAsStateWithLifecycle() + + if (followSetsState.isEmpty()) { + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight(0.5f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = stringRes(R.string.follow_set_empty_dialog_msg)) + Spacer(modifier = StdVertSpacer) + } + } else { + val userName by observeUserName(userToAddOrRemove, accountViewModel) + + LazyColumn(modifier = Modifier.fillMaxWidth()) { + itemsIndexed(followSetsState, key = { _, item -> item.identifierTag }) { _, list -> + PeopleListAndUserItem( + modifier = Modifier.fillMaxWidth(), + listHeader = list.title, + userName = userName, + userIsPrivateMember = list.privateMembers.contains(userToAddOrRemove), + userIsPublicMember = list.publicMembers.contains(userToAddOrRemove), + onRemoveUser = { + accountViewModel.runIOCatching { + accountViewModel.account.peopleLists.removeUserFromSet( + userToAddOrRemove, + isPrivate = list.privateMembers.contains(userToAddOrRemove), + list.identifierTag, + accountViewModel.account, + ) + } + }, + privateMemberSize = list.privateMembers.size, + publicMemberSize = list.publicMembers.size, + onAddUserToList = { userShouldBePrivate -> + accountViewModel.runIOCatching { + accountViewModel.account.peopleLists.addUserToSet( + userToAddOrRemove, + list.identifierTag, + userShouldBePrivate, + accountViewModel.account, + ) + } + }, + ) + HorizontalDivider(thickness = DividerThickness) + } + } + } +}