Adds creation, edits and member update for Follow Packs
This commit is contained in:
+171
-2
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.peopleList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -30,8 +32,16 @@ import com.vitorpamplona.amethyst.model.filter
|
||||
import com.vitorpamplona.amethyst.model.updateFlow
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.update
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.description
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.person
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.personFirst
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.removePerson
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.title
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.utils.flattenToSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -46,6 +56,7 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Maintains several stateflows for each step in processing PeopleLists
|
||||
@@ -59,7 +70,7 @@ class FollowListsState(
|
||||
) {
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun existingPeopleListNotes() = cache.addressables.filter(FollowListEvent.Companion.KIND, user.pubkeyHex)
|
||||
fun existingPeopleListNotes() = cache.addressables.filter(FollowListEvent.KIND, user.pubkeyHex)
|
||||
|
||||
val followListVersions = MutableStateFlow(0)
|
||||
|
||||
@@ -83,7 +94,7 @@ class FollowListsState(
|
||||
.transformLatest { emitAll(it.updateFlow<FollowListEvent>()) }
|
||||
.onStart { emit(followListNotes.value.events()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
|
||||
|
||||
@@ -112,6 +123,14 @@ class FollowListsState(
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
||||
|
||||
fun selectListFlow(selectedDTag: String) =
|
||||
uiListFlow
|
||||
.map { peopleLists ->
|
||||
peopleLists.firstOrNull { list -> list.identifierTag == selectedDTag }
|
||||
}.onStart {
|
||||
emit(uiListFlow.value.firstOrNull { it.identifierTag == selectedDTag })
|
||||
}
|
||||
|
||||
fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex)
|
||||
|
||||
fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.Companion.KIND) || deletesAnyEventIn(followListsEventIds.value)
|
||||
@@ -140,4 +159,154 @@ class FollowListsState(
|
||||
fun forceRefresh() {
|
||||
followListVersions.update { it + 1 }
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Updating Lists
|
||||
// --------------
|
||||
|
||||
fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier }
|
||||
|
||||
fun getPeopleList(noteIdentifier: String): FollowListEvent = getPeopleListNote(noteIdentifier)?.event as FollowListEvent
|
||||
|
||||
fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint())
|
||||
|
||||
fun Set<User>.toUserTags() = map { it.toUserTag() }
|
||||
|
||||
suspend fun addFollowList(
|
||||
name: String,
|
||||
desc: String?,
|
||||
member: User? = null,
|
||||
isPrivate: Boolean = false,
|
||||
account: Account,
|
||||
) {
|
||||
val newListTemplate =
|
||||
FollowListEvent.build(
|
||||
name = name,
|
||||
people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
dTag = UUID.randomUUID().toString(),
|
||||
) {
|
||||
if (desc != null) description(desc)
|
||||
}
|
||||
|
||||
val newList = signer.sign(newListTemplate)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun renameFollowList(
|
||||
newName: String,
|
||||
followPack: PeopleList,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(followPack.identifierTag)
|
||||
|
||||
val template =
|
||||
listEvent.update {
|
||||
title(newName)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun modifyFollowSetDescription(
|
||||
newDescription: String,
|
||||
followPack: PeopleList,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(followPack.identifierTag)
|
||||
|
||||
val template =
|
||||
listEvent.update {
|
||||
description(newDescription)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun cloneFollowSet(
|
||||
currentFollowPack: PeopleList,
|
||||
customCloneName: String?,
|
||||
customCloneDescription: String?,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(currentFollowPack.identifierTag)
|
||||
|
||||
val template =
|
||||
listEvent.update {
|
||||
// new list
|
||||
dTag(UUID.randomUUID().toString())
|
||||
|
||||
// updates names
|
||||
if (customCloneName != null) title(customCloneName)
|
||||
if (customCloneDescription != null) description(customCloneDescription)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun deleteFollowSet(
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent)))
|
||||
account.sendMyPublicAndPrivateOutbox(deletionEvent)
|
||||
}
|
||||
|
||||
suspend fun addUserToSet(
|
||||
user: User,
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
|
||||
val template =
|
||||
followListEvent.update {
|
||||
person(user.pubkeyHex, user.bestRelayHint())
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun addUserFirstToSet(
|
||||
user: User,
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
|
||||
val template =
|
||||
followListEvent.update {
|
||||
personFirst(user.pubkeyHex, user.bestRelayHint())
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun removeUserFromSet(
|
||||
user: User,
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
|
||||
val template =
|
||||
followListEvent.update {
|
||||
removePerson(user.pubkeyHex)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +79,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists.PeopleListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.packs.FollowPackScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.ListOfPeopleListsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.EditPeopleListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
||||
@@ -127,8 +128,9 @@ fun AppNavigation(
|
||||
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
||||
|
||||
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.PeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
|
||||
composableFromBottomArgs<Route.PeopleListManagement> { EditPeopleListScreen(it.userToAdd, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.MyPeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
|
||||
composableFromBottomArgs<Route.PeopleListManagement> { FollowListAndPackAndUserScreen(it.userToAdd, accountViewModel, nav) }
|
||||
|
||||
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||
|
||||
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.navigation.routes
|
||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.toRoute
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route.Room
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
@@ -56,7 +55,11 @@ sealed class Route {
|
||||
|
||||
@Serializable object Lists : Route()
|
||||
|
||||
@Serializable data class PeopleListView(
|
||||
@Serializable data class MyPeopleListView(
|
||||
val dTag: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class MyFollowPackView(
|
||||
val dTag: String,
|
||||
) : Route()
|
||||
|
||||
|
||||
+104
@@ -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.display
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
|
||||
@Composable
|
||||
fun ListActionsMenuButton(
|
||||
onBroadcastList: () -> Unit,
|
||||
onDeleteList: () -> Unit,
|
||||
) {
|
||||
val isActionListOpen = remember { mutableStateOf(false) }
|
||||
|
||||
ClickableBox(
|
||||
modifier =
|
||||
StdPadding
|
||||
.size(30.dp)
|
||||
.border(
|
||||
width = Dp.Hairline,
|
||||
color = ButtonDefaults.filledTonalButtonColors().containerColor,
|
||||
shape = ButtonBorder,
|
||||
).background(
|
||||
color = ButtonDefaults.filledTonalButtonColors().containerColor,
|
||||
shape = ButtonBorder,
|
||||
),
|
||||
onClick = { isActionListOpen.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
ListActionsMenu(
|
||||
onCloseMenu = { isActionListOpen.value = false },
|
||||
isOpen = isActionListOpen.value,
|
||||
onBroadcastList = onBroadcastList,
|
||||
onDeleteList = onDeleteList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListActionsMenu(
|
||||
onCloseMenu: () -> Unit,
|
||||
isOpen: Boolean,
|
||||
onBroadcastList: () -> Unit,
|
||||
onDeleteList: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isOpen,
|
||||
onDismissRequest = onCloseMenu,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text("Broadcast List")
|
||||
},
|
||||
onClick = {
|
||||
onBroadcastList()
|
||||
onCloseMenu()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text("Delete List")
|
||||
},
|
||||
onClick = {
|
||||
onDeleteList()
|
||||
onCloseMenu()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 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.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults.cardElevation
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.AnimateOnNewSearch
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
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.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.LightRedColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun RenderAddUserFieldAndSuggestions(
|
||||
userSuggestions: UserSuggestionState,
|
||||
hasUserFlow: (User) -> Flow<Boolean>,
|
||||
addUserToSet: (User) -> Unit,
|
||||
removeUserFromSet: (User) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
UserSearchDataSourceSubscription(userSuggestions, accountViewModel)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
userSuggestions.invalidateData()
|
||||
}
|
||||
}
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.deletedEventBundles.collect {
|
||||
userSuggestions.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(HalfVertSpacer)
|
||||
|
||||
var userName by remember(userSuggestions) { mutableStateOf(TextFieldValue(userSuggestions.currentWord.value)) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.search_and_add_a_user)) },
|
||||
modifier = Modifier.padding(horizontal = Size10dp).fillMaxWidth(),
|
||||
value = userName,
|
||||
onValueChange = {
|
||||
userName = it
|
||||
userSuggestions.processCurrentWord(it.text)
|
||||
},
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
userName = TextFieldValue("")
|
||||
userSuggestions.processCurrentWord("")
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
) {
|
||||
ClearTextIcon()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
ShowUserSuggestions(
|
||||
userSuggestions = userSuggestions,
|
||||
hasUserFlow = hasUserFlow,
|
||||
onSelect = { user ->
|
||||
addUserToSet(user)
|
||||
userName =
|
||||
userName.copy(
|
||||
selection = TextRange(0, userName.text.length),
|
||||
)
|
||||
},
|
||||
onDelete = { user ->
|
||||
removeUserFromSet(user)
|
||||
userName =
|
||||
userName.copy(
|
||||
selection = TextRange(0, userName.text.length),
|
||||
)
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowUserSuggestions(
|
||||
userSuggestions: UserSuggestionState,
|
||||
hasUserFlow: (User) -> Flow<Boolean>,
|
||||
onSelect: (User) -> Unit,
|
||||
onDelete: (User) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
AnimateOnNewSearch(userSuggestions, listState)
|
||||
|
||||
val suggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (suggestions.isNotEmpty()) {
|
||||
Card(
|
||||
modifier = Modifier.padding(start = 11.dp, end = 11.dp),
|
||||
elevation = cardElevation(5.dp),
|
||||
shape = PopupUpEffect,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(top = 10.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.heightIn(0.dp, 200.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, baseUser ->
|
||||
DrawUser(baseUser, hasUserFlow, onSelect, onDelete, accountViewModel)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(StdVertSpacer)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawUser(
|
||||
baseUser: User,
|
||||
hasUserFlow: (User) -> Flow<Boolean>,
|
||||
onSelect: (User) -> Unit,
|
||||
onDelete: (User) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { onSelect(baseUser) })
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ClickableUserPicture(baseUser, 55.dp, accountViewModel, Modifier, null)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
UsernameDisplay(
|
||||
baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
HasUserTag(baseUser, hasUserFlow, onDelete)
|
||||
}
|
||||
|
||||
AboutDisplay(baseUser, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.HasUserTag(
|
||||
baseUser: User,
|
||||
hasUserFlow: (User) -> Flow<Boolean>,
|
||||
onDelete: (User) -> Unit,
|
||||
) {
|
||||
val hasUserState by hasUserFlow(baseUser).collectAsStateWithLifecycle(false)
|
||||
if (hasUserState) {
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(
|
||||
text = stringRes(id = R.string.in_the_list),
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
IconButton(
|
||||
modifier = Modifier.size(30.dp).padding(start = 10.dp),
|
||||
onClick = { onDelete(baseUser) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
contentDescription = stringRes(id = R.string.remove),
|
||||
modifier = Modifier.size(15.dp),
|
||||
tint = LightRedColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-285
@@ -18,16 +18,10 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -35,23 +29,15 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults.cardElevation
|
||||
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.OutlinedTextField
|
||||
@@ -62,60 +48,39 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
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.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
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.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.AnimateOnNewSearch
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.DrawUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.ListActionsMenuButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions
|
||||
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.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.LightRedColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
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.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -173,7 +138,7 @@ fun PeopleListScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopAppTabs(
|
||||
private fun TopAppTabs(
|
||||
viewModel: PeopleListViewModel,
|
||||
pagerState: PagerState,
|
||||
) {
|
||||
@@ -212,7 +177,7 @@ fun TopAppTabs(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitleAndDescription(viewModel: PeopleListViewModel) {
|
||||
private fun TitleAndDescription(viewModel: PeopleListViewModel) {
|
||||
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||
selectedSetState.value?.let { selectedSet ->
|
||||
Text(
|
||||
@@ -255,70 +220,20 @@ private fun RenderAddUserFieldAndSuggestions(
|
||||
pagerState: PagerState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
UserSearchDataSourceSubscription(viewModel.userSuggestions, accountViewModel)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
viewModel.userSuggestions.invalidateData()
|
||||
}
|
||||
}
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.deletedEventBundles.collect {
|
||||
viewModel.userSuggestions.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(HalfVertSpacer)
|
||||
|
||||
var userName by remember(viewModel) { mutableStateOf(TextFieldValue(viewModel.userSuggestions.currentWord.value)) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.search_and_add_a_user)) },
|
||||
modifier = Modifier.padding(horizontal = Size10dp).fillMaxWidth(),
|
||||
value = userName,
|
||||
onValueChange = {
|
||||
userName = it
|
||||
viewModel.userSuggestions.processCurrentWord(it.text)
|
||||
},
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
userName = TextFieldValue("")
|
||||
viewModel.userSuggestions.processCurrentWord("")
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
) {
|
||||
ClearTextIcon()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
ShowUserSuggestions(
|
||||
userSuggestions = viewModel.userSuggestions,
|
||||
RenderAddUserFieldAndSuggestions(
|
||||
viewModel.userSuggestions,
|
||||
hasUserFlow = { user ->
|
||||
viewModel.hasUserFlow(user, pagerState.currentPage == 1)
|
||||
},
|
||||
onSelect = { user ->
|
||||
addUserToSet = { user ->
|
||||
accountViewModel.launchSigner {
|
||||
viewModel.addUserToSet(user, pagerState.currentPage == 1)
|
||||
}
|
||||
userName =
|
||||
userName.copy(
|
||||
selection = TextRange(0, userName.text.length),
|
||||
)
|
||||
},
|
||||
onDelete = { user ->
|
||||
removeUserFromSet = { user ->
|
||||
accountViewModel.launchSigner {
|
||||
viewModel.removeUserFromSet(user, pagerState.currentPage == 1)
|
||||
}
|
||||
userName =
|
||||
userName.copy(
|
||||
selection = TextRange(0, userName.text.length),
|
||||
)
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
@@ -365,7 +280,7 @@ private fun PeopleListPager(
|
||||
|
||||
@Composable
|
||||
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
||||
fun FollowSetListViewPreview() {
|
||||
private fun PeopleListViewPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
|
||||
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||
@@ -433,130 +348,7 @@ fun FollowSetListViewPreview() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowUserSuggestions(
|
||||
userSuggestions: UserSuggestionState,
|
||||
hasUserFlow: (User) -> Flow<Boolean>,
|
||||
onSelect: (User) -> Unit,
|
||||
onDelete: (User) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
AnimateOnNewSearch(userSuggestions, listState)
|
||||
|
||||
val suggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (suggestions.isNotEmpty()) {
|
||||
Card(
|
||||
modifier = Modifier.padding(start = 11.dp, end = 11.dp),
|
||||
elevation = cardElevation(5.dp),
|
||||
shape = PopupUpEffect,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(top = 10.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.heightIn(0.dp, 200.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, baseUser ->
|
||||
DrawUser(baseUser, hasUserFlow, onSelect, onDelete, accountViewModel)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(StdVertSpacer)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawUser(
|
||||
baseUser: User,
|
||||
hasUserFlow: (User) -> Flow<Boolean>,
|
||||
onSelect: (User) -> Unit,
|
||||
onDelete: (User) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { onSelect(baseUser) })
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ClickableUserPicture(baseUser, 55.dp, accountViewModel, Modifier, null)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
UsernameDisplay(
|
||||
baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
HasUserTag(baseUser, hasUserFlow, onDelete)
|
||||
}
|
||||
|
||||
AboutDisplay(baseUser, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.HasUserTag(
|
||||
baseUser: User,
|
||||
hasUserFlow: (User) -> Flow<Boolean>,
|
||||
onDelete: (User) -> Unit,
|
||||
) {
|
||||
val hasUserState by hasUserFlow(baseUser).collectAsStateWithLifecycle(false)
|
||||
if (hasUserState) {
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(
|
||||
text = stringRes(id = R.string.in_the_list),
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
IconButton(
|
||||
modifier = Modifier.size(30.dp).padding(start = 10.dp),
|
||||
onClick = { onDelete(baseUser) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
contentDescription = stringRes(id = R.string.remove),
|
||||
modifier = Modifier.size(15.dp),
|
||||
tint = LightRedColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListActionsMenuButton(
|
||||
private fun ListActionsMenuButton(
|
||||
viewModel: PeopleListViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
@@ -577,67 +369,3 @@ fun ListActionsMenuButton(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListActionsMenuButton(
|
||||
onBroadcastList: () -> Unit,
|
||||
onDeleteList: () -> Unit,
|
||||
) {
|
||||
val isActionListOpen = remember { mutableStateOf(false) }
|
||||
|
||||
ClickableBox(
|
||||
modifier =
|
||||
StdPadding
|
||||
.size(30.dp)
|
||||
.border(
|
||||
width = Dp.Hairline,
|
||||
color = ButtonDefaults.filledTonalButtonColors().containerColor,
|
||||
shape = ButtonBorder,
|
||||
).background(
|
||||
color = ButtonDefaults.filledTonalButtonColors().containerColor,
|
||||
shape = ButtonBorder,
|
||||
),
|
||||
onClick = { isActionListOpen.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
ListActionsMenu(
|
||||
onCloseMenu = { isActionListOpen.value = false },
|
||||
isOpen = isActionListOpen.value,
|
||||
onBroadcastList = onBroadcastList,
|
||||
onDeleteList = onDeleteList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListActionsMenu(
|
||||
onCloseMenu: () -> Unit,
|
||||
isOpen: Boolean,
|
||||
onBroadcastList: () -> Unit,
|
||||
onDeleteList: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isOpen,
|
||||
onDismissRequest = onCloseMenu,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text("Broadcast List")
|
||||
},
|
||||
onClick = {
|
||||
onBroadcastList()
|
||||
onCloseMenu()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text("Delete List")
|
||||
},
|
||||
onClick = {
|
||||
onDeleteList()
|
||||
onCloseMenu()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* 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.packs
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
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.material3.Card
|
||||
import androidx.compose.material3.CardDefaults.cardElevation
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.DrawUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.ListActionsMenuButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FollowPackScreen(
|
||||
selectedDTag: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val viewModel: FollowPackViewModel = viewModel()
|
||||
viewModel.init(accountViewModel.account, selectedDTag)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = {
|
||||
TitleAndDescription(viewModel)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(nav::popBack) {
|
||||
ArrowBackIcon()
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
ListActionsMenuButton(viewModel, accountViewModel, nav)
|
||||
},
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
ListViewAndEditColumn(
|
||||
viewModel = viewModel,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = padding.calculateTopPadding(),
|
||||
bottom = padding.calculateBottomPadding(),
|
||||
).consumeWindowInsets(padding)
|
||||
.imePadding(),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleAndDescription(viewModel: FollowPackViewModel) {
|
||||
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||
selectedSetState.value?.let { selectedSet ->
|
||||
Text(
|
||||
text = selectedSet.title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListViewAndEditColumn(
|
||||
viewModel: FollowPackViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
PeopleListPager(
|
||||
viewModel = viewModel,
|
||||
modifier = Modifier.weight(1f),
|
||||
onDeleteUser = { user ->
|
||||
accountViewModel.launchSigner {
|
||||
viewModel.removeUserFromSet(user)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
RenderAddUserFieldAndSuggestions(viewModel, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderAddUserFieldAndSuggestions(
|
||||
viewModel: FollowPackViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
RenderAddUserFieldAndSuggestions(
|
||||
viewModel.userSuggestions,
|
||||
hasUserFlow = { user ->
|
||||
viewModel.hasUserFlow(user)
|
||||
},
|
||||
addUserToSet = { user ->
|
||||
accountViewModel.launchSigner {
|
||||
viewModel.addUserToSet(user)
|
||||
}
|
||||
},
|
||||
removeUserFromSet = { user ->
|
||||
accountViewModel.launchSigner {
|
||||
viewModel.removeUserFromSet(user)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PeopleListPager(
|
||||
viewModel: FollowPackViewModel,
|
||||
modifier: Modifier,
|
||||
onDeleteUser: (User) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||
selectedSetState.value?.let { selectedSet ->
|
||||
PeopleListView(
|
||||
memberList = selectedSet.publicMembersList,
|
||||
onDeleteUser = onDeleteUser,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListActionsMenuButton(
|
||||
viewModel: FollowPackViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
ListActionsMenuButton(
|
||||
onBroadcastList = {
|
||||
accountViewModel.launchSigner {
|
||||
viewModel.loadNote()?.let { updatedSetNote ->
|
||||
accountViewModel.broadcast(updatedSetNote)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDeleteList = {
|
||||
accountViewModel.launchSigner {
|
||||
viewModel.deleteFollowSet()
|
||||
}
|
||||
nav.popBack()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
||||
fun FollowPackViewPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
|
||||
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||
val user2: User = LocalCache.getOrCreateUser("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b")
|
||||
val user3: User = LocalCache.getOrCreateUser("7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf")
|
||||
|
||||
ThemeComparisonRow {
|
||||
Column {
|
||||
PeopleListView(
|
||||
memberList = persistentListOf(user1, user2, user3),
|
||||
onDeleteUser = { user -> },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = EmptyNav(),
|
||||
)
|
||||
|
||||
Spacer(HalfVertSpacer)
|
||||
|
||||
var userName by remember { mutableStateOf("") }
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.search_and_add_a_user)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = Size10dp)
|
||||
.fillMaxWidth(),
|
||||
value = userName,
|
||||
onValueChange = {
|
||||
userName = it
|
||||
},
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {},
|
||||
) {
|
||||
ClearTextIcon()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.padding(horizontal = 10.dp),
|
||||
elevation = cardElevation(5.dp),
|
||||
shape = PopupUpEffect,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(top = 10.dp),
|
||||
modifier = Modifier.heightIn(0.dp, 200.dp),
|
||||
) {
|
||||
itemsIndexed(persistentListOf(user1, user2, user3), key = { _, item -> item.pubkeyHex }) { _, baseUser ->
|
||||
DrawUser(
|
||||
baseUser,
|
||||
{ MutableStateFlow(false) },
|
||||
{},
|
||||
{},
|
||||
accountViewModel,
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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.packs
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
@Stable
|
||||
class FollowPackViewModel : ViewModel() {
|
||||
lateinit var account: Account
|
||||
lateinit var userSuggestions: UserSuggestionState
|
||||
|
||||
var userSuggestionFocus by mutableStateOf<UserSuggestionState?>(null)
|
||||
|
||||
val selectedDTag = MutableStateFlow("")
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val selectedList =
|
||||
selectedDTag
|
||||
.transformLatest {
|
||||
emitAll(
|
||||
account.followLists.selectListFlow(it).flowOn(Dispatchers.IO),
|
||||
)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
fun init(
|
||||
account: Account,
|
||||
selectedDTag: String,
|
||||
) {
|
||||
if (!this::account.isInitialized || this.account != account) {
|
||||
this.account = account
|
||||
this.userSuggestions = UserSuggestionState(account, false)
|
||||
}
|
||||
|
||||
this.selectedDTag.tryEmit(selectedDTag)
|
||||
}
|
||||
|
||||
suspend fun deleteFollowSet() {
|
||||
account.followLists.deleteFollowSet(selectedDTag.value, account)
|
||||
}
|
||||
|
||||
fun loadNote(): AddressableNote? = account.followLists.getPeopleListNote(selectedDTag.value)
|
||||
|
||||
suspend fun removeUserFromSet(user: User) {
|
||||
account.followLists.removeUserFromSet(user, selectedDTag.value, account)
|
||||
}
|
||||
|
||||
suspend fun addUserToSet(user: User) {
|
||||
account.followLists.addUserToSet(user, selectedDTag.value, account)
|
||||
}
|
||||
|
||||
fun hasUserFlow(user: User): Flow<Boolean> =
|
||||
selectedList.map {
|
||||
if (it == null) {
|
||||
false
|
||||
} else {
|
||||
user in it.publicMembers
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Stable
|
||||
class FollowPackViewModel : ViewModel() {
|
||||
lateinit var accountViewModel: AccountViewModel
|
||||
|
||||
fun init(accountViewModel: AccountViewModel) {
|
||||
this.accountViewModel = accountViewModel
|
||||
}
|
||||
|
||||
fun listFlow() = accountViewModel.account.followLists.uiListFlow
|
||||
|
||||
fun addItem(
|
||||
title: String,
|
||||
description: String?,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.addFollowList(
|
||||
name = title,
|
||||
desc = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openItem(dTag: String) = Route.MyFollowPackView(dTag)
|
||||
|
||||
fun renameItem(
|
||||
followSet: PeopleList,
|
||||
newValue: String,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.renameFollowList(
|
||||
newName = newValue,
|
||||
followPack = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeItemDescription(
|
||||
followSet: PeopleList,
|
||||
newDescription: String,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.modifyFollowSetDescription(
|
||||
newDescription = newDescription,
|
||||
followPack = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cloneItem(
|
||||
followSet: PeopleList,
|
||||
customName: String?,
|
||||
customDescription: String?,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.cloneFollowSet(
|
||||
currentFollowPack = followSet,
|
||||
customCloneName = customName,
|
||||
customCloneDescription = customDescription,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteItem(followSet: PeopleList) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.deleteFollowSet(
|
||||
identifierTag = followSet.identifierTag,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
-17
@@ -22,40 +22,44 @@ 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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
@Composable
|
||||
fun AllPeopleListFeedView(
|
||||
listFlow: StateFlow<List<PeopleList>>,
|
||||
onOpenItem: (String) -> Unit = {},
|
||||
onRenameItem: (targetSet: PeopleList, newName: String) -> Unit,
|
||||
onItemDescriptionChange: (peopleList: PeopleList, newDescription: String?) -> Unit,
|
||||
onItemClone: (peopleList: PeopleList, customName: String?, customDesc: String?) -> Unit,
|
||||
onDeleteItem: (peopleList: PeopleList) -> Unit,
|
||||
peopleListModel: PeopleListViewModel,
|
||||
followPackModel: FollowPackViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val followSetFeedState by listFlow.collectAsStateWithLifecycle()
|
||||
val peopleListFeedState by peopleListModel.listFlow().collectAsStateWithLifecycle()
|
||||
val followPackFeedState by followPackModel.listFlow().collectAsStateWithLifecycle()
|
||||
|
||||
if (followSetFeedState.isEmpty()) {
|
||||
if (peopleListFeedState.isEmpty() && followPackFeedState.isEmpty()) {
|
||||
AllPeopleListFeedEmpty(
|
||||
message = stringRes(R.string.follow_set_empty_feed_msg),
|
||||
)
|
||||
@@ -64,15 +68,75 @@ fun AllPeopleListFeedView(
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = FeedPadding,
|
||||
) {
|
||||
itemsIndexed(followSetFeedState, key = { _, item -> item.identifierTag }) { _, list ->
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
PeopleListFabsAndMenu(
|
||||
title = R.string.follow_set_creation_dialog_title,
|
||||
onAddSet = peopleListModel::addItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(peopleListFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||
PeopleListItem(
|
||||
modifier = Modifier.fillMaxSize().animateItem(),
|
||||
peopleList = list,
|
||||
onClick = { onOpenItem(list.identifierTag) },
|
||||
onRename = { onRenameItem(list, it) },
|
||||
onDescriptionChange = { newDescription -> onItemDescriptionChange(list, newDescription) },
|
||||
onClone = { cloneName, cloneDescription -> onItemClone(list, cloneName, cloneDescription) },
|
||||
onDelete = { onDeleteItem(list) },
|
||||
peopleList = followSet,
|
||||
onClick = { nav.nav(peopleListModel.openItem(followSet.identifierTag)) },
|
||||
onRename = { peopleListModel.renameItem(followSet, it) },
|
||||
onDescriptionChange = { newDescription -> peopleListModel.changeItemDescription(followSet, newDescription) },
|
||||
onClone = { cloneName, cloneDescription -> peopleListModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||
onDelete = { peopleListModel.deleteItem(followSet) },
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
PeopleListFabsAndMenu(
|
||||
title = R.string.follow_pack_creation_dialog_title,
|
||||
onAddSet = followPackModel::addItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||
PeopleListItem(
|
||||
modifier = Modifier.fillMaxSize().animateItem(),
|
||||
peopleList = followSet,
|
||||
onClick = { nav.nav(followPackModel.openItem(followSet.identifierTag)) },
|
||||
onRename = { followPackModel.renameItem(followSet, it) },
|
||||
onDescriptionChange = { newDescription -> followPackModel.changeItemDescription(followSet, newDescription) },
|
||||
onClone = { cloneName, cloneDescription -> followPackModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||
onDelete = { followPackModel.deleteItem(followSet) },
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
|
||||
+28
-90
@@ -21,108 +21,52 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
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.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
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 kotlinx.coroutines.flow.StateFlow
|
||||
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||
|
||||
@Composable
|
||||
fun ListOfPeopleListsScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
ListOfPeopleListsScreen(
|
||||
listFlow = accountViewModel.account.peopleLists.uiListFlow,
|
||||
addItem = { title: String, description: String? ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.addFollowList(
|
||||
listName = title,
|
||||
listDescription = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
openItem = {
|
||||
nav.nav(Route.PeopleListView(it))
|
||||
},
|
||||
renameItem = { followSet, newValue ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.renameFollowList(
|
||||
newName = newValue,
|
||||
peopleList = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
changeItemDescription = { followSet, newDescription ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.modifyFollowSetDescription(
|
||||
newDescription = newDescription,
|
||||
peopleList = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
cloneItem = { followSet, customName, customDescription ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.cloneFollowSet(
|
||||
currentPeopleList = followSet,
|
||||
customCloneName = customName,
|
||||
customCloneDescription = customDescription,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
deleteItem = { followSet ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.deleteFollowSet(
|
||||
identifierTag = followSet.identifierTag,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
nav,
|
||||
)
|
||||
val list: PeopleListViewModel = viewModel()
|
||||
list.init(accountViewModel)
|
||||
|
||||
val pack: FollowPackViewModel = viewModel()
|
||||
pack.init(accountViewModel)
|
||||
|
||||
ListOfPeopleListsScreen(list, pack, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListOfPeopleListsScreen(
|
||||
listFlow: StateFlow<List<PeopleList>>,
|
||||
addItem: (title: String, description: String?) -> Unit,
|
||||
openItem: (identifier: String) -> Unit,
|
||||
renameItem: (peopleList: PeopleList, newName: String) -> Unit,
|
||||
changeItemDescription: (peopleList: PeopleList, newDescription: String?) -> Unit,
|
||||
cloneItem: (peopleList: PeopleList, customName: String?, customDesc: String?) -> Unit,
|
||||
deleteItem: (peopleList: PeopleList) -> Unit,
|
||||
list: PeopleListViewModel,
|
||||
pack: FollowPackViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBarWithBackButton(stringRes(R.string.my_lists), nav::popBack)
|
||||
},
|
||||
floatingActionButton = {
|
||||
PeopleListFabsAndMenu(
|
||||
onAddSet = addItem,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
@@ -131,41 +75,35 @@ fun ListOfPeopleListsScreen(
|
||||
bottom = paddingValues.calculateBottomPadding(),
|
||||
).fillMaxHeight(),
|
||||
) {
|
||||
AllPeopleListFeedView(
|
||||
listFlow = listFlow,
|
||||
onOpenItem = openItem,
|
||||
onRenameItem = renameItem,
|
||||
onItemDescriptionChange = changeItemDescription,
|
||||
onItemClone = cloneItem,
|
||||
onDeleteItem = deleteItem,
|
||||
)
|
||||
AllPeopleListFeedView(list, pack, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PeopleListFabsAndMenu(onAddSet: (name: String, description: String?) -> Unit) {
|
||||
fun PeopleListFabsAndMenu(
|
||||
title: Int = R.string.follow_set_creation_dialog_title,
|
||||
onAddSet: (name: String, description: String?) -> Unit,
|
||||
) {
|
||||
val isSetAdditionDialogOpen = remember { mutableStateOf(false) }
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_create_btn_label))
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
isSetAdditionDialogOpen.value = true
|
||||
},
|
||||
icon = {
|
||||
) {
|
||||
Row(horizontalArrangement = SpacedBy5dp, verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
isSetAdditionDialogOpen.value = true
|
||||
},
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(stringRes(R.string.follow_set_create_btn_label))
|
||||
}
|
||||
}
|
||||
|
||||
if (isSetAdditionDialogOpen.value) {
|
||||
NewPeopleListCreationDialog(
|
||||
title = title,
|
||||
onDismiss = {
|
||||
isSetAdditionDialogOpen.value = false
|
||||
},
|
||||
|
||||
+6
-6
@@ -162,7 +162,7 @@ fun PeopleListItem(
|
||||
peopleList: PeopleList,
|
||||
onClick: () -> Unit,
|
||||
onRename: (String) -> Unit,
|
||||
onDescriptionChange: (String?) -> Unit,
|
||||
onDescriptionChange: (String) -> Unit,
|
||||
onClone: (customName: String?, customDescription: String?) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
@@ -279,7 +279,7 @@ private fun PeopleListOptionsButton(
|
||||
peopleListName: String,
|
||||
peopleListDescription: String?,
|
||||
onListRename: (String) -> Unit,
|
||||
onListDescriptionChange: (String?) -> Unit,
|
||||
onListDescriptionChange: (String) -> Unit,
|
||||
onListCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
||||
onListDelete: () -> Unit,
|
||||
) {
|
||||
@@ -310,7 +310,7 @@ private fun ListOptionsMenu(
|
||||
setName: String,
|
||||
setDescription: String?,
|
||||
onListRename: (String) -> Unit,
|
||||
onListDescriptionChange: (String?) -> Unit,
|
||||
onListDescriptionChange: (String) -> Unit,
|
||||
onListClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
@@ -470,9 +470,9 @@ private fun ListModifyDescriptionDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
currentDescription: String?,
|
||||
onDismissDialog: () -> Unit,
|
||||
onModifyDescription: (String?) -> Unit,
|
||||
onModifyDescription: (String) -> Unit,
|
||||
) {
|
||||
val updatedDescription = remember { mutableStateOf<String?>(null) }
|
||||
val updatedDescription = remember { mutableStateOf<String>("") }
|
||||
|
||||
val modifyIndicatorLabel =
|
||||
if (currentDescription == null) {
|
||||
@@ -509,7 +509,7 @@ private fun ListModifyDescriptionDialog(
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
TextField(
|
||||
value = updatedDescription.value ?: "",
|
||||
value = updatedDescription.value,
|
||||
onValueChange = { updatedDescription.value = it },
|
||||
)
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Stable
|
||||
class PeopleListViewModel : ViewModel() {
|
||||
lateinit var accountViewModel: AccountViewModel
|
||||
|
||||
fun init(accountViewModel: AccountViewModel) {
|
||||
this.accountViewModel = accountViewModel
|
||||
}
|
||||
|
||||
fun listFlow() = accountViewModel.account.peopleLists.uiListFlow
|
||||
|
||||
fun addItem(
|
||||
title: String,
|
||||
description: String?,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.addFollowList(
|
||||
listName = title,
|
||||
listDescription = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openItem(dTag: String) = Route.MyPeopleListView(dTag)
|
||||
|
||||
fun renameItem(
|
||||
followSet: PeopleList,
|
||||
newValue: String,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.renameFollowList(
|
||||
newName = newValue,
|
||||
peopleList = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeItemDescription(
|
||||
followSet: PeopleList,
|
||||
newDescription: String?,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.modifyFollowSetDescription(
|
||||
newDescription = newDescription,
|
||||
peopleList = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cloneItem(
|
||||
followSet: PeopleList,
|
||||
customName: String?,
|
||||
customDescription: String?,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.cloneFollowSet(
|
||||
currentPeopleList = followSet,
|
||||
customCloneName = customName,
|
||||
customCloneDescription = customDescription,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteItem(followSet: PeopleList) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.deleteFollowSet(
|
||||
identifierTag = followSet.identifierTag,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-53
@@ -26,15 +26,8 @@ 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
|
||||
@@ -49,13 +42,12 @@ 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.NewPeopleListCreationDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EditPeopleListScreen(
|
||||
fun FollowListAndPackAndUserScreen(
|
||||
userToAddOrRemove: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
@@ -72,22 +64,19 @@ fun EditPeopleListScreen(
|
||||
}
|
||||
|
||||
userBase?.let {
|
||||
EditPeopleListScreen(it, accountViewModel, nav)
|
||||
FollowListAndPackAndUserScreen(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EditPeopleListScreen(
|
||||
fun FollowListAndPackAndUserScreen(
|
||||
userToAddOrRemove: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize().recalculateWindowInsets(),
|
||||
floatingActionButton = {
|
||||
PeopleListAndUserFab(accountViewModel)
|
||||
},
|
||||
topBar = {
|
||||
val userName by observeUserName(userToAddOrRemove, accountViewModel)
|
||||
TopBarWithBackButton(
|
||||
@@ -105,45 +94,7 @@ fun EditPeopleListScreen(
|
||||
).consumeWindowInsets(contentPadding)
|
||||
.imePadding(),
|
||||
) {
|
||||
PeopleListAndUserView(userToAddOrRemove, accountViewModel, nav)
|
||||
FollowListAndPackAndUserView(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.launchSigner {
|
||||
accountViewModel.account.peopleLists.addFollowList(
|
||||
listName = name,
|
||||
listDescription = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
isOpen = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.compose.ui.unit.dp
|
||||
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.screen.loggedIn.lists.list.PeopleListFabsAndMenu
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
@Composable
|
||||
fun FollowListAndPackAndUserView(
|
||||
userToAddOrRemove: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val followSetsState by accountViewModel.account.peopleLists.uiListFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val followPackFeedState by accountViewModel.account.followLists.uiListFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
if (followSetsState.isEmpty() && followPackFeedState.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()) {
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
PeopleListFabsAndMenu(
|
||||
title = R.string.follow_set_creation_dialog_title,
|
||||
onAddSet = { title, description ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.addFollowList(
|
||||
listName = title,
|
||||
listDescription = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
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.launchSigner {
|
||||
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.launchSigner {
|
||||
accountViewModel.account.peopleLists.addUserToSet(
|
||||
userToAddOrRemove,
|
||||
list.identifierTag,
|
||||
userShouldBePrivate,
|
||||
accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
PeopleListFabsAndMenu(
|
||||
title = R.string.follow_pack_creation_dialog_title,
|
||||
onAddSet = { title, description ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.addFollowList(
|
||||
name = title,
|
||||
desc = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, list ->
|
||||
FollowPackAndUserItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
listHeader = list.title,
|
||||
userName = userName,
|
||||
isMember = list.publicMembers.contains(userToAddOrRemove),
|
||||
onRemoveUser = {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.removeUserFromSet(
|
||||
userToAddOrRemove,
|
||||
list.identifierTag,
|
||||
accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
memberSize = list.publicMembers.size,
|
||||
onAddUserToList = {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.addUserToSet(
|
||||
userToAddOrRemove,
|
||||
list.identifierTag,
|
||||
accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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.Public
|
||||
import androidx.compose.material.icons.outlined.RemoveCircleOutline
|
||||
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.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 FollowPackAndUserMemberPreview() {
|
||||
ThemeComparisonColumn {
|
||||
FollowPackAndUserItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
listHeader = "list title",
|
||||
userName = "User",
|
||||
isMember = true,
|
||||
memberSize = 2,
|
||||
onAddUserToList = {},
|
||||
onRemoveUser = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun FollowPackAndUserNotMemberPreview() {
|
||||
ThemeComparisonColumn {
|
||||
FollowPackAndUserItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
listHeader = "list title",
|
||||
userName = "User",
|
||||
isMember = false,
|
||||
memberSize = 2,
|
||||
onAddUserToList = {},
|
||||
onRemoveUser = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowPackAndUserItem(
|
||||
modifier: Modifier = Modifier,
|
||||
listHeader: String,
|
||||
userName: String,
|
||||
isMember: Boolean,
|
||||
memberSize: Int,
|
||||
onAddUserToList: () -> Unit,
|
||||
onRemoveUser: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = listHeader,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
UserStatusInList(userName, isMember)
|
||||
},
|
||||
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 = 0,
|
||||
publicMembersSize = memberSize,
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
UserAdditionOptions(isMember, onAddUserToList, onRemoveUser)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserStatusInList(
|
||||
userName: String,
|
||||
isMember: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = HalfHalfVertPadding,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val text =
|
||||
if (isMember) {
|
||||
stringRes(R.string.follow_set_public_presence_indicator, userName)
|
||||
} else {
|
||||
stringRes(R.string.follow_set_absence_indicator2, userName)
|
||||
}
|
||||
|
||||
val icon =
|
||||
if (isMember) {
|
||||
Icons.Outlined.Public
|
||||
} 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: () -> Unit,
|
||||
onRemoveUser: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (isUserInList) {
|
||||
onRemoveUser()
|
||||
} else {
|
||||
onAddUserToList()
|
||||
}
|
||||
},
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -139,7 +139,7 @@ fun PeopleListAndUserItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserStatusInList(
|
||||
private fun UserStatusInList(
|
||||
userName: String,
|
||||
userIsPrivateMember: Boolean,
|
||||
userIsPublicMember: Boolean,
|
||||
|
||||
-104
@@ -1,104 +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.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.launchSigner {
|
||||
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.launchSigner {
|
||||
accountViewModel.account.peopleLists.addUserToSet(
|
||||
userToAddOrRemove,
|
||||
list.identifierTag,
|
||||
userShouldBePrivate,
|
||||
accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,6 +549,7 @@
|
||||
<string name="follow_set_creation_item_label">New list with %1$s membership</string>
|
||||
<string name="follow_set_creation_item_description">Creates a new follow set, and adds %1$s as a %2$s member.</string>
|
||||
<string name="follow_set_creation_dialog_title">New Follow List</string>
|
||||
<string name="follow_pack_creation_dialog_title">New Follow Pack</string>
|
||||
<string name="follow_set_copy_dialog_title">Copy/Clone Follow List</string>
|
||||
<string name="follow_set_desc_modify_label">Modify description</string>
|
||||
<string name="follow_set_empty_desc_label">This list doesn\'t have a description</string>
|
||||
|
||||
Reference in New Issue
Block a user