diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt index ab0be6a24..7c611c4b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt @@ -30,15 +30,21 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -58,12 +64,12 @@ import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.NostrUserListFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.followsets.FollowSetScreen import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @Composable @@ -95,21 +101,43 @@ fun ListsScreen( val followSetsFlow by followSetsViewModel.feedContent.collectAsStateWithLifecycle() + val followSets by produceState(initialValue = FollowSetState.Loading) { + followSetsViewModel.feedContent.collectLatest { value = it } + } + + // TODO: Replace this with nav-based solution. + val isFollowSetSelected = remember { mutableStateOf(false) } + val selectedFollowListWithIndex = remember { mutableStateOf?>(null) } + CustomListsScreen( - followSetsFlow, + followSets, refresh = { followSetsViewModel.invalidateData() }, + addItem = { title: String, description: String?, listType: ListVisibility -> + followSetsViewModel.addFollowSet( + setName = title, + setDescription = description, + setType = listType, + ) + }, openItem = { - currentCoroutineScope.launch(Dispatchers.IO) { - val note = followSetsViewModel.getFollowSetAddressable(it, accountViewModel.account) - if (note != null) { - val event = note.event as PeopleListEvent - println("Found list, with title: ${event.nameOrTitle()}") - } else { - println("No corresponding note found for this list.") - } + val selectedFollowSet = followSetsViewModel.findFollowSet(identifier = it) + if (selectedFollowSet != null) { + selectedFollowListWithIndex.value = selectedFollowSet + isFollowSetSelected.value = true + } else { + println("This list has not been found. Could it not have been added/deleted properly?") } +// currentCoroutineScope.launch(Dispatchers.IO) { +// val note = followSetsViewModel.getFollowSetAddressable(it, accountViewModel.account) +// if (note != null) { +// val event = note.event as PeopleListEvent +// println("Found list, with title: ${event.nameOrTitle()}") +// } else { +// println("No corresponding note found for this list.") +// } +// } }, renameItem = { index, newValue -> followSetsViewModel.renameFollowSet(newName = newValue, setIndex = index) @@ -120,12 +148,41 @@ fun ListsScreen( accountViewModel, nav, ) + + // TODO: Replace this with nav-based solution. + if (isFollowSetSelected.value && selectedFollowListWithIndex.value != null) { + val leFollowSetIndex = selectedFollowListWithIndex.value!!.first + FollowSetScreen( + onClose = { + isFollowSetSelected.value = false + selectedFollowListWithIndex.value = null + }, + accountViewModel = accountViewModel, + navigator = nav, + selectedSet = selectedFollowListWithIndex.value!!.second, + onProfileRemove = { + val newSet = followSetsViewModel.removeUserFromSet(it, leFollowSetIndex) + selectedFollowListWithIndex.value = Pair(leFollowSetIndex, newSet) + }, + onListSave = { + accountViewModel.toastManager.toast("List Changes", "Changes already saved.") + }, + onListBroadcast = { + accountViewModel.toastManager.toast("List Status", "List has been broadcast.") + }, + onListDelete = { + isFollowSetSelected.value = false + followSetsViewModel.deleteFollowSet(leFollowSetIndex) + }, + ) + } } @Composable fun CustomListsScreen( followSetState: FollowSetState, refresh: () -> Unit, + addItem: (title: String, description: String?, listType: ListVisibility) -> Unit, openItem: (identifier: String) -> Unit, renameItem: (Int, String) -> Unit, deleteItem: (Int) -> Unit, @@ -168,7 +225,14 @@ fun CustomListsScreen( }, floatingButton = { // TODO: Show components based on current tab - FollowSetFabsAndMenu() + FollowSetFabsAndMenu( + onAddPrivateList = { name: String, description: String? -> + addItem(name, description, ListVisibility.Private) + }, + onAddPublicList = { name: String, description: String? -> + addItem(name, description, ListVisibility.Public) + }, + ) }, ) { Column( @@ -196,13 +260,22 @@ fun CustomListsScreen( } @Composable -fun FollowSetFabsAndMenu(modifier: Modifier = Modifier) { +fun FollowSetFabsAndMenu( + modifier: Modifier = Modifier, + onAddPrivateList: (name: String, description: String?) -> Unit, + onAddPublicList: (name: String, description: String?) -> Unit, +) { + val isListAdditionDialogOpen = remember { mutableStateOf(false) } + val isPrivateOptionTapped = remember { mutableStateOf(false) } + Row( horizontalArrangement = Arrangement.spacedBy(10.dp), ) { FloatingActionButton( onClick = { println("The private list addition...") + isPrivateOptionTapped.value = true + isListAdditionDialogOpen.value = true }, shape = CircleShape, containerColor = MaterialTheme.colorScheme.primary, @@ -216,6 +289,7 @@ fun FollowSetFabsAndMenu(modifier: Modifier = Modifier) { FloatingActionButton( onClick = { println("The public list creation...") + isListAdditionDialogOpen.value = true }, shape = CircleShape, containerColor = MaterialTheme.colorScheme.primary, @@ -227,6 +301,102 @@ fun FollowSetFabsAndMenu(modifier: Modifier = Modifier) { ) } } + + if (isListAdditionDialogOpen.value) { + NewListCreationDialog( + onDismiss = { + isListAdditionDialogOpen.value = false + isPrivateOptionTapped.value = false + }, + shouldBePrivate = isPrivateOptionTapped.value, + onCreateList = { name, description -> + if (isPrivateOptionTapped.value) { + onAddPrivateList(name, description) + } else { + onAddPublicList(name, description) + } + }, + ) + } +} + +@Composable +fun NewListCreationDialog( + modifier: Modifier = Modifier, + onDismiss: () -> Unit, + shouldBePrivate: Boolean, + onCreateList: (name: String, description: String?) -> Unit, +) { + val newListName = remember { mutableStateOf("") } + val newListDescription = remember { mutableStateOf(null) } + + val listTypeText = + when (shouldBePrivate) { + true -> "Private" + false -> "Public" + } + + val listTypeIcon = + when (shouldBePrivate) { + true -> R.drawable.lock + false -> R.drawable.ic_public + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Icon( + painter = painterResource(listTypeIcon), + contentDescription = null, + ) + Text( + text = "New $listTypeText List", + ) + } + }, + text = { + Column { + // For the new list name + TextField( + value = newListName.value, + onValueChange = { newListName.value = it }, + label = { + Text("List name") + }, + ) + Spacer(modifier = StdVertSpacer) + // For the list description + TextField( + value = + ( + if (newListDescription.value != null) newListDescription.value else "" + ).toString(), + onValueChange = { newListDescription.value = it }, + ) + } + }, + confirmButton = { + Button( + onClick = { + onCreateList(newListName.value, newListDescription.value) + onDismiss() + }, + ) { + Text("Create list") + } + }, + dismissButton = { + Button( + onClick = onDismiss, + ) { + Text("Cancel") + } + }, + ) } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt index 67058a140..da2e15ac6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt @@ -20,8 +20,233 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.followsets +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Row +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults 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.res.painterResource +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.ClickableBox +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.UserCompose +import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata +import kotlin.random.Random + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FollowSetScreen( + onClose: () -> Unit, + accountViewModel: AccountViewModel, + navigator: INav, + selectedSet: FollowSet, + onProfileRemove: (String) -> Unit, + onListSave: () -> Unit, + onListBroadcast: () -> Unit, + onListDelete: () -> Unit, +) { + BackHandler { onClose() } + + // TODO: Remove this line and the function it calls, as they are hacks. + val users = selectedSet.profileList.mapToUsers() + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = selectedSet.title, + ) + Icon( + painter = + painterResource( + when (selectedSet.listVisibility) { + ListVisibility.Public -> R.drawable.ic_public + ListVisibility.Private -> R.drawable.lock + ListVisibility.Mixed -> R.drawable.format_list_bulleted_type + }, + ), + contentDescription = null, + ) + } + }, + navigationIcon = { + BackButton( + onPress = onClose, + ) + }, + actions = { + ListActionsMenuButton( + onSaveList = onListSave, + onBroadcastList = onListBroadcast, + onDeleteList = onListDelete, + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { padding -> + FollowSetListView( + modifier = + Modifier + .fillMaxSize() + .padding( + start = 10.dp, + end = 10.dp, + top = padding.calculateTopPadding(), + bottom = padding.calculateBottomPadding(), + ).consumeWindowInsets(padding) + .imePadding(), + followSetList = users, + onDeleteUser = { onProfileRemove(it) }, + accountViewModel = accountViewModel, + nav = navigator, + ) + } +} + +fun Set.mapToUsers(): List = + map { + User(Random.nextBytes(32).toHexKey()).apply { + info = UserMetadata().apply { name = it } + } + } @Composable -fun FollowSetScreen() { +private fun FollowSetListView( + modifier: Modifier = Modifier, + followSetList: List, + onDeleteUser: (String) -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) { + val listState = rememberLazyListState() + + LazyColumn( + modifier = modifier, + contentPadding = FeedPadding, + state = listState, + ) { + itemsIndexed(followSetList, key = { _, item -> item.pubkeyHex }) { _, item -> + Row { + IconButton( + onClick = { + onDeleteUser(item.info?.name.toString()) + }, + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + ) + } + UserCompose(item, accountViewModel = accountViewModel, nav = nav) + HorizontalDivider( + thickness = DividerThickness, + ) + } + } + } +} + +@Composable +fun ListActionsMenuButton( + modifier: Modifier = Modifier, + onSaveList: () -> Unit, + onBroadcastList: () -> Unit, + onDeleteList: () -> Unit, +) { + val isActionListOpen = remember { mutableStateOf(false) } + + ClickableBox( + onClick = { isActionListOpen.value = true }, + ) { + VerticalDotsIcon() + ListActionsMenu( + onCloseMenu = { isActionListOpen.value = false }, + isOpen = isActionListOpen.value, + onSaveList = onSaveList, + onBroadcastList = onBroadcastList, + onDeleteList = onDeleteList, + ) + } +} + +@Composable +fun ListActionsMenu( + modifier: Modifier = Modifier, + onCloseMenu: () -> Unit, + isOpen: Boolean, + onSaveList: () -> Unit, + onBroadcastList: () -> Unit, + onDeleteList: () -> Unit, +) { + DropdownMenu( + expanded = isOpen, + onDismissRequest = onCloseMenu, + ) { + DropdownMenuItem( + text = { + Text("Save Changes") + }, + onClick = { + onSaveList() + onCloseMenu() + }, + ) + DropdownMenuItem( + text = { + Text("Broadcast List") + }, + onClick = { + onBroadcastList() + onCloseMenu() + }, + ) + DropdownMenuItem( + text = { + Text("Delete List") + }, + onClick = { + onDeleteList() + onCloseMenu() + }, + ) + } }