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.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape 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.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue 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.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.NostrUserListFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold 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.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@@ -95,21 +101,43 @@ fun ListsScreen(
val followSetsFlow by followSetsViewModel.feedContent.collectAsStateWithLifecycle() 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( CustomListsScreen(
followSetsFlow, followSets,
refresh = { refresh = {
followSetsViewModel.invalidateData() followSetsViewModel.invalidateData()
}, },
addItem = { title: String, description: String?, listType: ListVisibility ->
followSetsViewModel.addFollowSet(
setName = title,
setDescription = description,
setType = listType,
)
},
openItem = { openItem = {
currentCoroutineScope.launch(Dispatchers.IO) { val selectedFollowSet = followSetsViewModel.findFollowSet(identifier = it)
val note = followSetsViewModel.getFollowSetAddressable(it, accountViewModel.account) if (selectedFollowSet != null) {
if (note != null) { selectedFollowListWithIndex.value = selectedFollowSet
val event = note.event as PeopleListEvent isFollowSetSelected.value = true
println("Found list, with title: ${event.nameOrTitle()}") } else {
} else { println("This list has not been found. Could it not have been added/deleted properly?")
println("No corresponding note found for this list.")
}
} }
// 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 -> renameItem = { index, newValue ->
followSetsViewModel.renameFollowSet(newName = newValue, setIndex = index) followSetsViewModel.renameFollowSet(newName = newValue, setIndex = index)
@@ -120,12 +148,41 @@ fun ListsScreen(
accountViewModel, accountViewModel,
nav, 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 @Composable
fun CustomListsScreen( fun CustomListsScreen(
followSetState: FollowSetState, followSetState: FollowSetState,
refresh: () -> Unit, refresh: () -> Unit,
addItem: (title: String, description: String?, listType: ListVisibility) -> Unit,
openItem: (identifier: String) -> Unit, openItem: (identifier: String) -> Unit,
renameItem: (Int, String) -> Unit, renameItem: (Int, String) -> Unit,
deleteItem: (Int) -> Unit, deleteItem: (Int) -> Unit,
@@ -168,7 +225,14 @@ fun CustomListsScreen(
}, },
floatingButton = { floatingButton = {
// TODO: Show components based on current tab // 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( Column(
@@ -196,13 +260,22 @@ fun CustomListsScreen(
} }
@Composable @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( Row(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
println("The private list addition...") println("The private list addition...")
isPrivateOptionTapped.value = true
isListAdditionDialogOpen.value = true
}, },
shape = CircleShape, shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -216,6 +289,7 @@ fun FollowSetFabsAndMenu(modifier: Modifier = Modifier) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
println("The public list creation...") println("The public list creation...")
isListAdditionDialogOpen.value = true
}, },
shape = CircleShape, shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primary, 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 @Composable
@@ -20,8 +20,233 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.followsets 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.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 @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()
},
)
}
} }