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 index 609de3ebe..5bbaf2d5b 100644 --- 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 @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit import androidx.compose.foundation.background -import androidx.compose.foundation.clickable 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 @@ -35,19 +35,24 @@ 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.PlaylistAddCheck +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.material3.ButtonDefaults +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.FilterChip +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 @@ -60,16 +65,8 @@ 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.res.painterResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache @@ -78,13 +75,14 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse 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.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.SpacedBy10dp +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.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -124,16 +122,17 @@ fun EditPeopleListScreen( Modifier .fillMaxSize() .recalculateWindowInsets(), + floatingActionButton = { + PeopleListFabsAndMenu(accountViewModel) + }, topBar = { - TopBarWithBackButton(stringRes(id = R.string.follow_set_man_dialog_title), nav::popBack) + TopBarWithBackButton(stringRes(id = R.string.follow_set_man_dialog_title2, userToAddOrRemove.toBestDisplayName()), nav::popBack) }, ) { contentPadding -> Column( modifier = Modifier .padding( - start = 10.dp, - end = 10.dp, top = contentPadding.calculateTopPadding(), bottom = contentPadding.calculateBottomPadding(), ).consumeWindowInsets(contentPadding) @@ -144,6 +143,44 @@ fun EditPeopleListScreen( } } +@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, @@ -163,6 +200,7 @@ private fun FollowSetManagementScreenBody( FollowSetItem( modifier = Modifier.fillMaxWidth(), listHeader = list.title, + listDescription = list.description ?: "", userName = userName, userIsPrivateMember = list.privateMembers.contains(userToAddOrRemove), userIsPublicMember = list.publicMembers.contains(userToAddOrRemove), @@ -176,6 +214,8 @@ private fun FollowSetManagementScreenBody( ) } }, + privateMemberSize = list.privateMembers.size, + publicMemberSize = list.publicMembers.size, onAddUserToList = { userShouldBePrivate -> accountViewModel.runIOCatching { accountViewModel.account.peopleLists.addUserToSet( @@ -191,21 +231,6 @@ private fun FollowSetManagementScreenBody( } } } - - FollowSetsCreationMenu( - userName = userToAddOrRemove.toBestDisplayName(), - onSetCreate = { setName, memberShouldBePrivate, description -> - accountViewModel.runIOCatching { - accountViewModel.account.peopleLists.addFollowList( - listName = setName, - listDescription = description, - isPrivate = memberShouldBePrivate, - member = userToAddOrRemove, - account = accountViewModel.account, - ) - } - }, - ) } @Composable @@ -229,9 +254,12 @@ fun FollowSetItemMemberPreview() { FollowSetItem( modifier = Modifier.fillMaxWidth(), listHeader = "list title", + listDescription = "desc", userName = "User", userIsPrivateMember = true, userIsPublicMember = true, + privateMemberSize = 3, + publicMemberSize = 2, onAddUserToList = {}, onRemoveUser = {}, ) @@ -245,9 +273,12 @@ fun FollowSetItemNotMemberPreview() { FollowSetItem( modifier = Modifier.fillMaxWidth(), listHeader = "list title", + listDescription = "desc", userName = "User", userIsPrivateMember = false, userIsPublicMember = false, + privateMemberSize = 3, + publicMemberSize = 2, onAddUserToList = {}, onRemoveUser = {}, ) @@ -258,114 +289,134 @@ fun FollowSetItemNotMemberPreview() { 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 - Row( - modifier = modifier.padding(all = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = SpacedBy10dp, - ) { - Column( - modifier = modifier.weight(1f), - verticalArrangement = SpacedBy5dp, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = SpacedBy5dp, - ) { - Icon( - painter = painterResource(R.drawable.format_list_bulleted_type), - contentDescription = stringRes(R.string.follow_set_icon_description), - ) - Text(listHeader, fontWeight = FontWeight.Bold) - } - FilterChip( - selected = true, - onClick = {}, - label = { - Text( - 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_indicator, userName) - }, - ) - }, - leadingIcon = + ListItem( + modifier = modifier, + headlineContent = { + Text(listHeader, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + supportingContent = { + Row( + modifier = HalfHalfVertPadding, + horizontalArrangement = SpacedBy5dp, + verticalAlignment = Alignment.CenterVertically, + ) { + val text = if (isUserInList) { - { - Icon( - imageVector = Icons.AutoMirrored.Filled.PlaylistAddCheck, - contentDescription = null, - ) + if (userIsPublicMember) { + stringRes(R.string.follow_set_public_presence_indicator, userName) + } else { + stringRes(R.string.follow_set_private_presence_indicator, userName) } } else { - null - }, - shape = ButtonBorder, - ) - } - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - val isUserAddTapped = remember { mutableStateOf(false) } - IconButton( - onClick = { - if (isUserInList) { - onRemoveUser() - } else { - isUserAddTapped.value = true + stringRes(R.string.follow_set_absence_indicator2, userName) } - }, - 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 = null, - tint = MaterialTheme.colorScheme.onBackground, - ) + 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.Filled.PersonAdd, - contentDescription = null, - tint = Color.White, + 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 }, - ) - } - } + UserAdditionOptionsMenu( + isExpanded = isUserAddTapped.value, + onUserAdd = { shouldBePrivateMember -> + onAddUserToList(shouldBePrivateMember) + }, + onDismiss = { isUserAddTapped.value = false }, + ) + } + }, + ) } @Composable @@ -398,117 +449,3 @@ private fun UserAdditionOptionsMenu( ) } } - -@Composable -fun FollowSetsCreationMenu( - modifier: Modifier = Modifier, - userName: String, - onSetCreate: (setName: String, memberShouldBePrivate: Boolean, description: String?) -> Unit, -) { - val isListAdditionDialogOpen = remember { mutableStateOf(false) } - val isPrivateOptionTapped = remember { mutableStateOf(false) } - - Column( - modifier = modifier.padding(vertical = 30.dp), - ) { - Text( - stringRes(R.string.follow_set_creation_menu_title), - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Start, - ) - HorizontalDivider( - modifier = Modifier.fillMaxWidth(0.5f), - thickness = 3.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - Spacer(modifier = StdVertSpacer) - FollowSetCreationItem( - setIsPrivate = false, - userName = userName, - onClick = { - isListAdditionDialogOpen.value = true - }, - ) - FollowSetCreationItem( - setIsPrivate = true, - userName = userName, - onClick = { - isPrivateOptionTapped.value = true - isListAdditionDialogOpen.value = true - }, - ) - } - - if (isListAdditionDialogOpen.value) { - NewPeopleListCreationDialog( - onDismiss = { - isListAdditionDialogOpen.value = false - isPrivateOptionTapped.value = false - }, - onCreateList = { name, description -> - onSetCreate(name, isPrivateOptionTapped.value, description) - }, - ) - } -} - -@Composable -fun FollowSetCreationItem( - modifier: Modifier = Modifier, - setIsPrivate: Boolean, - userName: String, - onClick: () -> Unit, -) { - val context = LocalContext.current - val setTypeLabel = stringRes(context, if (setIsPrivate) R.string.follow_set_type_private else R.string.follow_set_type_public) - - HorizontalDivider(thickness = DividerThickness) - Column( - modifier = - modifier - .fillMaxWidth() - .background( - color = - ButtonDefaults - .filledTonalButtonColors() - .containerColor - .copy(alpha = 0.2f), - ).padding(vertical = 15.dp) - .clickable(role = Role.Button, onClick = onClick), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = - stringRes( - R.string.follow_set_creation_item_label, - setTypeLabel, - ), - fontWeight = FontWeight.SemiBold, - ) - Spacer(modifier = StdHorzSpacer) - Icon( - painter = - painterResource( - if (setIsPrivate) R.drawable.lock_plus else R.drawable.earth_plus, - ), - contentDescription = null, - ) - } - Spacer(modifier = StdVertSpacer) - Text( - stringRes( - R.string.follow_set_creation_item_description, - userName, - setTypeLabel.lowercase(Locale.current.platformLocale), - ), - fontWeight = FontWeight.Light, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - color = Color.Gray, - ) - } - HorizontalDivider(thickness = DividerThickness) -} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index ac4aac39d..aee6b7f40 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -538,6 +538,8 @@ No members Empty %1$s is not in this list + %1$s is not a member + Your Lists and %1$s Your Lists No follow lists were found, or you don\'t have any follow lists. Tap below to refresh, or use the menu to create one. There was a problem while fetching: %1$s @@ -1344,4 +1346,6 @@ Already in the list Private Members Public Members + Add user to the list + Add user to the list