Write down implementation for FollowSetScreen. Introduce functions(hacks, really) for making sure of the UI/UX design.

- Implement dialog for creating a new list.
This commit is contained in:
KotlinGeekDev
2025-05-12 20:47:12 +01:00
parent 5a6cfad848
commit 44b567b01f
2 changed files with 409 additions and 14 deletions
@@ -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<FollowSetState>(initialValue = FollowSetState.Loading) {
followSetsViewModel.feedContent.collectLatest { value = it }
}
// TODO: Replace this with nav-based solution.
val isFollowSetSelected = remember { mutableStateOf(false) }
val selectedFollowListWithIndex = remember { mutableStateOf<Pair<Int, FollowSet>?>(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<String?>(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
@@ -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<String>.mapToUsers(): List<User> =
map {
User(Random.nextBytes(32).toHexKey()).apply {
info = UserMetadata().apply { name = it }
}
}
@Composable
fun FollowSetScreen() {
private fun FollowSetListView(
modifier: Modifier = Modifier,
followSetList: List<User>,
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()
},
)
}
}