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
|
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.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
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.amethyst.model.updateFlow
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
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.nip09Deletions.DeletionEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
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 com.vitorpamplona.quartz.utils.flattenToSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -46,6 +56,7 @@ import kotlinx.coroutines.flow.onStart
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintains several stateflows for each step in processing PeopleLists
|
* Maintains several stateflows for each step in processing PeopleLists
|
||||||
@@ -59,7 +70,7 @@ class FollowListsState(
|
|||||||
) {
|
) {
|
||||||
val user = cache.getOrCreateUser(signer.pubKey)
|
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)
|
val followListVersions = MutableStateFlow(0)
|
||||||
|
|
||||||
@@ -83,7 +94,7 @@ class FollowListsState(
|
|||||||
.transformLatest { emitAll(it.updateFlow<FollowListEvent>()) }
|
.transformLatest { emitAll(it.updateFlow<FollowListEvent>()) }
|
||||||
.onStart { emit(followListNotes.value.events()) }
|
.onStart { emit(followListNotes.value.events()) }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
|
fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
|
||||||
|
|
||||||
@@ -112,6 +123,14 @@ class FollowListsState(
|
|||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
.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 isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex)
|
||||||
|
|
||||||
fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.Companion.KIND) || deletesAnyEventIn(followListsEventIds.value)
|
fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.Companion.KIND) || deletesAnyEventIn(followListsEventIds.value)
|
||||||
@@ -140,4 +159,154 @@ class FollowListsState(
|
|||||||
fun forceRefresh() {
|
fun forceRefresh() {
|
||||||
followListVersions.update { it + 1 }
|
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.hashtag.HashtagScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
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.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.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.NotificationScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
||||||
@@ -127,8 +128,9 @@ fun AppNavigation(
|
|||||||
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.PeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
|
composableFromEndArgs<Route.MyPeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
|
||||||
composableFromBottomArgs<Route.PeopleListManagement> { EditPeopleListScreen(it.userToAdd, accountViewModel, nav) }
|
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
|
||||||
|
composableFromBottomArgs<Route.PeopleListManagement> { FollowListAndPackAndUserScreen(it.userToAdd, accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
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.NavDestination.Companion.hasRoute
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.toRoute
|
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.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
@@ -56,7 +55,11 @@ sealed class Route {
|
|||||||
|
|
||||||
@Serializable object Lists : Route()
|
@Serializable object Lists : Route()
|
||||||
|
|
||||||
@Serializable data class PeopleListView(
|
@Serializable data class MyPeopleListView(
|
||||||
|
val dTag: String,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class MyFollowPackView(
|
||||||
val dTag: String,
|
val dTag: String,
|
||||||
) : Route()
|
) : 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
|
* 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.
|
* 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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.heightIn
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
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.Card
|
||||||
import androidx.compose.material3.CardDefaults.cardElevation
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -62,60 +48,39 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
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.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
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.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
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.ArrowBackIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
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.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.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
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.PopupUpEffect
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
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.TabRowHeight
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -173,7 +138,7 @@ fun PeopleListScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopAppTabs(
|
private fun TopAppTabs(
|
||||||
viewModel: PeopleListViewModel,
|
viewModel: PeopleListViewModel,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
) {
|
) {
|
||||||
@@ -212,7 +177,7 @@ fun TopAppTabs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TitleAndDescription(viewModel: PeopleListViewModel) {
|
private fun TitleAndDescription(viewModel: PeopleListViewModel) {
|
||||||
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||||
selectedSetState.value?.let { selectedSet ->
|
selectedSetState.value?.let { selectedSet ->
|
||||||
Text(
|
Text(
|
||||||
@@ -255,70 +220,20 @@ private fun RenderAddUserFieldAndSuggestions(
|
|||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
UserSearchDataSourceSubscription(viewModel.userSuggestions, accountViewModel)
|
RenderAddUserFieldAndSuggestions(
|
||||||
|
viewModel.userSuggestions,
|
||||||
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,
|
|
||||||
hasUserFlow = { user ->
|
hasUserFlow = { user ->
|
||||||
viewModel.hasUserFlow(user, pagerState.currentPage == 1)
|
viewModel.hasUserFlow(user, pagerState.currentPage == 1)
|
||||||
},
|
},
|
||||||
onSelect = { user ->
|
addUserToSet = { user ->
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
viewModel.addUserToSet(user, pagerState.currentPage == 1)
|
viewModel.addUserToSet(user, pagerState.currentPage == 1)
|
||||||
}
|
}
|
||||||
userName =
|
|
||||||
userName.copy(
|
|
||||||
selection = TextRange(0, userName.text.length),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onDelete = { user ->
|
removeUserFromSet = { user ->
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
viewModel.removeUserFromSet(user, pagerState.currentPage == 1)
|
viewModel.removeUserFromSet(user, pagerState.currentPage == 1)
|
||||||
}
|
}
|
||||||
userName =
|
|
||||||
userName.copy(
|
|
||||||
selection = TextRange(0, userName.text.length),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
@@ -365,7 +280,7 @@ private fun PeopleListPager(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
||||||
fun FollowSetListViewPreview() {
|
private fun PeopleListViewPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||||
@@ -433,130 +348,7 @@ fun FollowSetListViewPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShowUserSuggestions(
|
private fun ListActionsMenuButton(
|
||||||
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(
|
|
||||||
viewModel: PeopleListViewModel,
|
viewModel: PeopleListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
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
|
* 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.
|
* 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.Stable
|
||||||
import androidx.compose.runtime.getValue
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
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.Size40dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AllPeopleListFeedView(
|
fun AllPeopleListFeedView(
|
||||||
listFlow: StateFlow<List<PeopleList>>,
|
peopleListModel: PeopleListViewModel,
|
||||||
onOpenItem: (String) -> Unit = {},
|
followPackModel: FollowPackViewModel,
|
||||||
onRenameItem: (targetSet: PeopleList, newName: String) -> Unit,
|
nav: INav,
|
||||||
onItemDescriptionChange: (peopleList: PeopleList, newDescription: String?) -> Unit,
|
|
||||||
onItemClone: (peopleList: PeopleList, customName: String?, customDesc: String?) -> Unit,
|
|
||||||
onDeleteItem: (peopleList: PeopleList) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
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(
|
AllPeopleListFeedEmpty(
|
||||||
message = stringRes(R.string.follow_set_empty_feed_msg),
|
message = stringRes(R.string.follow_set_empty_feed_msg),
|
||||||
)
|
)
|
||||||
@@ -64,15 +68,75 @@ fun AllPeopleListFeedView(
|
|||||||
state = rememberLazyListState(),
|
state = rememberLazyListState(),
|
||||||
contentPadding = FeedPadding,
|
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(
|
PeopleListItem(
|
||||||
modifier = Modifier.fillMaxSize().animateItem(),
|
modifier = Modifier.fillMaxSize().animateItem(),
|
||||||
peopleList = list,
|
peopleList = followSet,
|
||||||
onClick = { onOpenItem(list.identifierTag) },
|
onClick = { nav.nav(peopleListModel.openItem(followSet.identifierTag)) },
|
||||||
onRename = { onRenameItem(list, it) },
|
onRename = { peopleListModel.renameItem(followSet, it) },
|
||||||
onDescriptionChange = { newDescription -> onItemDescriptionChange(list, newDescription) },
|
onDescriptionChange = { newDescription -> peopleListModel.changeItemDescription(followSet, newDescription) },
|
||||||
onClone = { cloneName, cloneDescription -> onItemClone(list, cloneName, cloneDescription) },
|
onClone = { cloneName, cloneDescription -> peopleListModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||||
onDelete = { onDeleteItem(list) },
|
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)
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-90
@@ -21,108 +21,52 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListOfPeopleListsScreen(
|
fun ListOfPeopleListsScreen(
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
ListOfPeopleListsScreen(
|
val list: PeopleListViewModel = viewModel()
|
||||||
listFlow = accountViewModel.account.peopleLists.uiListFlow,
|
list.init(accountViewModel)
|
||||||
addItem = { title: String, description: String? ->
|
|
||||||
accountViewModel.launchSigner {
|
val pack: FollowPackViewModel = viewModel()
|
||||||
accountViewModel.account.peopleLists.addFollowList(
|
pack.init(accountViewModel)
|
||||||
listName = title,
|
|
||||||
listDescription = description,
|
ListOfPeopleListsScreen(list, pack, nav)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListOfPeopleListsScreen(
|
fun ListOfPeopleListsScreen(
|
||||||
listFlow: StateFlow<List<PeopleList>>,
|
list: PeopleListViewModel,
|
||||||
addItem: (title: String, description: String?) -> Unit,
|
pack: FollowPackViewModel,
|
||||||
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,
|
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBarWithBackButton(stringRes(R.string.my_lists), nav::popBack)
|
TopBarWithBackButton(stringRes(R.string.my_lists), nav::popBack)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
|
||||||
PeopleListFabsAndMenu(
|
|
||||||
onAddSet = addItem,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -131,41 +75,35 @@ fun ListOfPeopleListsScreen(
|
|||||||
bottom = paddingValues.calculateBottomPadding(),
|
bottom = paddingValues.calculateBottomPadding(),
|
||||||
).fillMaxHeight(),
|
).fillMaxHeight(),
|
||||||
) {
|
) {
|
||||||
AllPeopleListFeedView(
|
AllPeopleListFeedView(list, pack, nav)
|
||||||
listFlow = listFlow,
|
|
||||||
onOpenItem = openItem,
|
|
||||||
onRenameItem = renameItem,
|
|
||||||
onItemDescriptionChange = changeItemDescription,
|
|
||||||
onItemClone = cloneItem,
|
|
||||||
onDeleteItem = deleteItem,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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) }
|
val isSetAdditionDialogOpen = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
ExtendedFloatingActionButton(
|
OutlinedButton(
|
||||||
text = {
|
onClick = {
|
||||||
Text(text = stringRes(R.string.follow_set_create_btn_label))
|
isSetAdditionDialogOpen.value = true
|
||||||
},
|
},
|
||||||
icon = {
|
) {
|
||||||
|
Row(horizontalArrangement = SpacedBy5dp, verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
Text(stringRes(R.string.follow_set_create_btn_label))
|
||||||
onClick = {
|
}
|
||||||
isSetAdditionDialogOpen.value = true
|
}
|
||||||
},
|
|
||||||
shape = CircleShape,
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isSetAdditionDialogOpen.value) {
|
if (isSetAdditionDialogOpen.value) {
|
||||||
NewPeopleListCreationDialog(
|
NewPeopleListCreationDialog(
|
||||||
|
title = title,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
isSetAdditionDialogOpen.value = false
|
isSetAdditionDialogOpen.value = false
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-6
@@ -162,7 +162,7 @@ fun PeopleListItem(
|
|||||||
peopleList: PeopleList,
|
peopleList: PeopleList,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onRename: (String) -> Unit,
|
onRename: (String) -> Unit,
|
||||||
onDescriptionChange: (String?) -> Unit,
|
onDescriptionChange: (String) -> Unit,
|
||||||
onClone: (customName: String?, customDescription: String?) -> Unit,
|
onClone: (customName: String?, customDescription: String?) -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -279,7 +279,7 @@ private fun PeopleListOptionsButton(
|
|||||||
peopleListName: String,
|
peopleListName: String,
|
||||||
peopleListDescription: String?,
|
peopleListDescription: String?,
|
||||||
onListRename: (String) -> Unit,
|
onListRename: (String) -> Unit,
|
||||||
onListDescriptionChange: (String?) -> Unit,
|
onListDescriptionChange: (String) -> Unit,
|
||||||
onListCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
onListCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
||||||
onListDelete: () -> Unit,
|
onListDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -310,7 +310,7 @@ private fun ListOptionsMenu(
|
|||||||
setName: String,
|
setName: String,
|
||||||
setDescription: String?,
|
setDescription: String?,
|
||||||
onListRename: (String) -> Unit,
|
onListRename: (String) -> Unit,
|
||||||
onListDescriptionChange: (String?) -> Unit,
|
onListDescriptionChange: (String) -> Unit,
|
||||||
onListClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit,
|
onListClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
@@ -470,9 +470,9 @@ private fun ListModifyDescriptionDialog(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
currentDescription: String?,
|
currentDescription: String?,
|
||||||
onDismissDialog: () -> Unit,
|
onDismissDialog: () -> Unit,
|
||||||
onModifyDescription: (String?) -> Unit,
|
onModifyDescription: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val updatedDescription = remember { mutableStateOf<String?>(null) }
|
val updatedDescription = remember { mutableStateOf<String>("") }
|
||||||
|
|
||||||
val modifyIndicatorLabel =
|
val modifyIndicatorLabel =
|
||||||
if (currentDescription == null) {
|
if (currentDescription == null) {
|
||||||
@@ -509,7 +509,7 @@ private fun ListModifyDescriptionDialog(
|
|||||||
fontStyle = FontStyle.Italic,
|
fontStyle = FontStyle.Italic,
|
||||||
)
|
)
|
||||||
TextField(
|
TextField(
|
||||||
value = updatedDescription.value ?: "",
|
value = updatedDescription.value,
|
||||||
onValueChange = { updatedDescription.value = it },
|
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.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.recalculateWindowInsets
|
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.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.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EditPeopleListScreen(
|
fun FollowListAndPackAndUserScreen(
|
||||||
userToAddOrRemove: HexKey,
|
userToAddOrRemove: HexKey,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
@@ -72,22 +64,19 @@ fun EditPeopleListScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
userBase?.let {
|
userBase?.let {
|
||||||
EditPeopleListScreen(it, accountViewModel, nav)
|
FollowListAndPackAndUserScreen(it, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EditPeopleListScreen(
|
fun FollowListAndPackAndUserScreen(
|
||||||
userToAddOrRemove: User,
|
userToAddOrRemove: User,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize().recalculateWindowInsets(),
|
modifier = Modifier.fillMaxSize().recalculateWindowInsets(),
|
||||||
floatingActionButton = {
|
|
||||||
PeopleListAndUserFab(accountViewModel)
|
|
||||||
},
|
|
||||||
topBar = {
|
topBar = {
|
||||||
val userName by observeUserName(userToAddOrRemove, accountViewModel)
|
val userName by observeUserName(userToAddOrRemove, accountViewModel)
|
||||||
TopBarWithBackButton(
|
TopBarWithBackButton(
|
||||||
@@ -105,45 +94,7 @@ fun EditPeopleListScreen(
|
|||||||
).consumeWindowInsets(contentPadding)
|
).consumeWindowInsets(contentPadding)
|
||||||
.imePadding(),
|
.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
|
@Composable
|
||||||
fun UserStatusInList(
|
private fun UserStatusInList(
|
||||||
userName: String,
|
userName: String,
|
||||||
userIsPrivateMember: Boolean,
|
userIsPrivateMember: Boolean,
|
||||||
userIsPublicMember: 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_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_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_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_copy_dialog_title">Copy/Clone Follow List</string>
|
||||||
<string name="follow_set_desc_modify_label">Modify description</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>
|
<string name="follow_set_empty_desc_label">This list doesn\'t have a description</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user