Moves the List/Pack creation Dialog to a screen and adds an Image upload option
This commit is contained in:
+31
-30
@@ -37,11 +37,15 @@ 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.description
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.image
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.person
|
import com.vitorpamplona.quartz.nip51Lists.followList.person
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.personFirst
|
import com.vitorpamplona.quartz.nip51Lists.followList.personFirst
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.removePerson
|
import com.vitorpamplona.quartz.nip51Lists.followList.removePerson
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.title
|
import com.vitorpamplona.quartz.nip51Lists.followList.title
|
||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
|
||||||
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
|
||||||
@@ -110,6 +114,7 @@ class FollowListsState(
|
|||||||
identifierTag = this.dTag(),
|
identifierTag = this.dTag(),
|
||||||
title = this.title() ?: this.dTag(),
|
title = this.title() ?: this.dTag(),
|
||||||
description = this.description(),
|
description = this.description(),
|
||||||
|
image = this.image(),
|
||||||
privateMembers = emptySet(),
|
privateMembers = emptySet(),
|
||||||
publicMembers = cache.load(this.followIdSet()),
|
publicMembers = cache.load(this.followIdSet()),
|
||||||
)
|
)
|
||||||
@@ -123,13 +128,17 @@ class FollowListsState(
|
|||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
||||||
|
|
||||||
fun selectListFlow(selectedDTag: String) =
|
fun List<PeopleList>.select(dTag: String) =
|
||||||
|
this.firstOrNull {
|
||||||
|
it.identifierTag == dTag
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectList(dTag: String) = uiListFlow.value.select(dTag)
|
||||||
|
|
||||||
|
fun selectListFlow(dTag: String) =
|
||||||
uiListFlow
|
uiListFlow
|
||||||
.map { peopleLists ->
|
.map { it.select(dTag) }
|
||||||
peopleLists.firstOrNull { list -> list.identifierTag == selectedDTag }
|
.onStart { emit(selectList(dTag)) }
|
||||||
}.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)
|
||||||
|
|
||||||
@@ -175,51 +184,43 @@ class FollowListsState(
|
|||||||
suspend fun addFollowList(
|
suspend fun addFollowList(
|
||||||
name: String,
|
name: String,
|
||||||
desc: String?,
|
desc: String?,
|
||||||
|
image: String?,
|
||||||
member: User? = null,
|
member: User? = null,
|
||||||
isPrivate: Boolean = false,
|
isPrivate: Boolean = false,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
): String {
|
||||||
|
val dTag = UUID.randomUUID().toString()
|
||||||
|
|
||||||
val newListTemplate =
|
val newListTemplate =
|
||||||
FollowListEvent.build(
|
FollowListEvent.build(
|
||||||
name = name,
|
name = name,
|
||||||
people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||||
dTag = UUID.randomUUID().toString(),
|
dTag = dTag,
|
||||||
) {
|
) {
|
||||||
if (desc != null) description(desc)
|
if (desc != null) description(desc)
|
||||||
|
if (image != null) image(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
val newList = signer.sign(newListTemplate)
|
val newList = signer.sign(newListTemplate)
|
||||||
|
|
||||||
account.sendMyPublicAndPrivateOutbox(newList)
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
return dTag
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun renameFollowList(
|
suspend fun updateMetadata(
|
||||||
newName: String,
|
name: String?,
|
||||||
followPack: PeopleList,
|
desc: String?,
|
||||||
|
image: String?,
|
||||||
|
peopleList: PeopleList,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
) {
|
||||||
val listEvent = getPeopleList(followPack.identifierTag)
|
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||||
|
|
||||||
val template =
|
val template =
|
||||||
listEvent.update {
|
listEvent.update {
|
||||||
title(newName)
|
if (name != null) title(name)
|
||||||
}
|
if (desc != null) description(desc)
|
||||||
|
if (image != null) image(image)
|
||||||
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)
|
val newList = signer.sign(template)
|
||||||
|
|||||||
+1
@@ -30,6 +30,7 @@ data class PeopleList(
|
|||||||
val identifierTag: String,
|
val identifierTag: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
|
val image: String?,
|
||||||
val privateMembers: Set<User> = emptySet(),
|
val privateMembers: Set<User> = emptySet(),
|
||||||
val publicMembers: Set<User> = emptySet(),
|
val publicMembers: Set<User> = emptySet(),
|
||||||
) {
|
) {
|
||||||
|
|||||||
+44
-39
@@ -32,9 +32,14 @@ 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.nip09Deletions.DeletionEvent
|
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.description
|
||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
|
||||||
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
|
||||||
@@ -116,6 +121,7 @@ class PeopleListsState(
|
|||||||
identifierTag = this.dTag(),
|
identifierTag = this.dTag(),
|
||||||
title = this.nameOrTitle() ?: this.dTag(),
|
title = this.nameOrTitle() ?: this.dTag(),
|
||||||
description = this.description(),
|
description = this.description(),
|
||||||
|
image = this.image(),
|
||||||
privateMembers = cache.load(decryptionCache.privateUserIdSet(this)),
|
privateMembers = cache.load(decryptionCache.privateUserIdSet(this)),
|
||||||
publicMembers = cache.load(this.publicUsersIdSet()),
|
publicMembers = cache.load(this.publicUsersIdSet()),
|
||||||
)
|
)
|
||||||
@@ -129,17 +135,17 @@ class PeopleListsState(
|
|||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
fun selectListFlow(selectedDTag: String) =
|
fun List<PeopleList>.select(dTag: String) =
|
||||||
|
this.firstOrNull {
|
||||||
|
it.identifierTag == dTag
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectList(dTag: String) = uiListFlow.value.select(dTag)
|
||||||
|
|
||||||
|
fun selectListFlow(dTag: String) =
|
||||||
uiListFlow
|
uiListFlow
|
||||||
.map { peopleLists ->
|
.map { it.select(dTag) }
|
||||||
peopleLists.firstOrNull { list ->
|
.onStart { emit(selectList(dTag)) }
|
||||||
list.identifierTag == selectedDTag
|
|
||||||
}
|
|
||||||
}.onStart {
|
|
||||||
emit(
|
|
||||||
uiListFlow.value.firstOrNull { it.identifierTag == selectedDTag },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
|
fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
|
||||||
|
|
||||||
@@ -183,49 +189,48 @@ class PeopleListsState(
|
|||||||
suspend fun addFollowList(
|
suspend fun addFollowList(
|
||||||
listName: String,
|
listName: String,
|
||||||
listDescription: String?,
|
listDescription: String?,
|
||||||
|
listImage: String?,
|
||||||
member: User? = null,
|
member: User? = null,
|
||||||
isPrivate: Boolean = false,
|
isPrivate: Boolean = false,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
): String {
|
||||||
val newList =
|
val dTag = UUID.randomUUID().toString()
|
||||||
PeopleListEvent.createListWithDescription(
|
val newListTemplate =
|
||||||
dTag = UUID.randomUUID().toString(),
|
PeopleListEvent.build(
|
||||||
title = listName,
|
dTag = dTag,
|
||||||
description = listDescription,
|
name = listName,
|
||||||
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||||
privateMembers = if (isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
privateMembers = if (isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||||
signer = account.signer,
|
signer = account.signer,
|
||||||
)
|
) {
|
||||||
|
if (listDescription != null) description(listDescription)
|
||||||
|
if (listImage != null) image(listImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newList = signer.sign(newListTemplate)
|
||||||
|
|
||||||
account.sendMyPublicAndPrivateOutbox(newList)
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
return dTag
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun renameFollowList(
|
suspend fun updateMetadata(
|
||||||
newName: String,
|
listName: String?,
|
||||||
|
listDescription: String?,
|
||||||
|
listImage: String?,
|
||||||
peopleList: PeopleList,
|
peopleList: PeopleList,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
) {
|
||||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||||
val newList =
|
|
||||||
PeopleListEvent.modifyListName(
|
|
||||||
earlierVersion = listEvent,
|
|
||||||
newName = newName,
|
|
||||||
signer = account.signer,
|
|
||||||
)
|
|
||||||
account.sendMyPublicAndPrivateOutbox(newList)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun modifyFollowSetDescription(
|
val template =
|
||||||
newDescription: String?,
|
listEvent.update {
|
||||||
peopleList: PeopleList,
|
if (listName != null) name(listName)
|
||||||
account: Account,
|
if (listDescription != null) description(listDescription)
|
||||||
) {
|
if (listImage != null) image(listImage)
|
||||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
}
|
||||||
val newList =
|
|
||||||
PeopleListEvent.modifyDescription(
|
val newList = signer.sign(template)
|
||||||
earlierVersion = listEvent,
|
|
||||||
newDescription = newDescription,
|
|
||||||
signer = account.signer,
|
|
||||||
)
|
|
||||||
account.sendMyPublicAndPrivateOutbox(newList)
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists.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.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.list.metadata.FollowPackMetadataScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen
|
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
|
||||||
@@ -132,6 +134,9 @@ fun AppNavigation(
|
|||||||
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
|
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
|
||||||
composableFromBottomArgs<Route.PeopleListManagement> { FollowListAndPackAndUserScreen(it.userToAdd, accountViewModel, nav) }
|
composableFromBottomArgs<Route.PeopleListManagement> { FollowListAndPackAndUserScreen(it.userToAdd, accountViewModel, nav) }
|
||||||
|
|
||||||
|
composableFromBottomArgs<Route.PeopleListMetadataEdit> { PeopleListMetadataScreen(it.dTag, accountViewModel, nav) }
|
||||||
|
composableFromBottomArgs<Route.FollowPackMetadataEdit> { FollowPackMetadataScreen(it.dTag, 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) }
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ sealed class Route {
|
|||||||
val dTag: String,
|
val dTag: String,
|
||||||
) : Route()
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class PeopleListMetadataEdit(
|
||||||
|
val dTag: String? = null,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class FollowPackMetadataEdit(
|
||||||
|
val dTag: String? = null,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
@Serializable data class PeopleListManagement(
|
@Serializable data class PeopleListManagement(
|
||||||
val userToAdd: HexKey,
|
val userToAdd: HexKey,
|
||||||
) : Route()
|
) : Route()
|
||||||
@@ -294,6 +302,15 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
|
|||||||
dest.hasRoute<Route.GenericCommentPost>() -> entry.toRoute<Route.GenericCommentPost>()
|
dest.hasRoute<Route.GenericCommentPost>() -> entry.toRoute<Route.GenericCommentPost>()
|
||||||
dest.hasRoute<Route.NewPublicMessage>() -> entry.toRoute<Route.NewPublicMessage>()
|
dest.hasRoute<Route.NewPublicMessage>() -> entry.toRoute<Route.NewPublicMessage>()
|
||||||
|
|
||||||
|
dest.hasRoute<Route.Lists>() -> entry.toRoute<Route.Lists>()
|
||||||
|
dest.hasRoute<Route.MyPeopleListView>() -> entry.toRoute<Route.MyPeopleListView>()
|
||||||
|
dest.hasRoute<Route.MyFollowPackView>() -> entry.toRoute<Route.MyFollowPackView>()
|
||||||
|
dest.hasRoute<Route.PeopleListMetadataEdit>() -> entry.toRoute<Route.PeopleListMetadataEdit>()
|
||||||
|
dest.hasRoute<Route.FollowPackMetadataEdit>() -> entry.toRoute<Route.FollowPackMetadataEdit>()
|
||||||
|
dest.hasRoute<Route.PeopleListManagement>() -> entry.toRoute<Route.PeopleListManagement>()
|
||||||
|
dest.hasRoute<Route.NewGroupDM>() -> entry.toRoute<Route.NewGroupDM>()
|
||||||
|
dest.hasRoute<Route.UserSettings>() -> entry.toRoute<Route.UserSettings>()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
-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.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()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+108
-1
@@ -20,6 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
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.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -29,13 +32,17 @@ 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.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.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.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -55,29 +62,38 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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.core.content.ContextCompat
|
||||||
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.AddressableNote
|
||||||
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.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.navigation.routes.Route
|
||||||
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.VerticalDotsIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||||
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.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.PeopleListView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions
|
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.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.StdPadding
|
||||||
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
|
||||||
@@ -354,6 +370,10 @@ private fun ListActionsMenuButton(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
ListActionsMenuButton(
|
ListActionsMenuButton(
|
||||||
|
note = viewModel::selectedNote,
|
||||||
|
onEditList = {
|
||||||
|
nav.nav { Route.PeopleListMetadataEdit(viewModel.selectedDTag.value) }
|
||||||
|
},
|
||||||
onBroadcastList = {
|
onBroadcastList = {
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
viewModel.loadNote()?.let { updatedSetNote ->
|
viewModel.loadNote()?.let { updatedSetNote ->
|
||||||
@@ -369,3 +389,90 @@ private fun ListActionsMenuButton(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListActionsMenuButton(
|
||||||
|
note: () -> AddressableNote,
|
||||||
|
onEditList: () -> Unit,
|
||||||
|
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()
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = isActionListOpen.value,
|
||||||
|
onDismissRequest = { isActionListOpen.value = false },
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||||
|
onClick = {
|
||||||
|
val sendIntent =
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
externalLinkForNote(note()),
|
||||||
|
)
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TITLE,
|
||||||
|
stringRes(context, R.string.quick_action_share_browser_link),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareIntent =
|
||||||
|
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||||
|
ContextCompat.startActivity(context, shareIntent, null)
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_set_edit_list_metadata))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onEditList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_set_broadcast))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onBroadcastList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_set_delete))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onDeleteList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+5
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -60,6 +61,10 @@ class PeopleListViewModel : ViewModel() {
|
|||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
|
|
||||||
|
fun selectedAddress() = PeopleListEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value)
|
||||||
|
|
||||||
|
fun selectedNote() = account.cache.getOrCreateAddressableNote(selectedAddress())
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
account: Account,
|
account: Account,
|
||||||
selectedDTag: String,
|
selectedDTag: String,
|
||||||
|
|||||||
+108
-1
@@ -20,6 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.packs
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.packs
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
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.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -29,10 +32,14 @@ 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.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.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -48,29 +55,38 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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.core.content.ContextCompat
|
||||||
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.AddressableNote
|
||||||
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.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.navigation.routes.Route
|
||||||
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.VerticalDotsIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||||
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.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.PeopleListView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions
|
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.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.StdPadding
|
||||||
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.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -211,6 +227,10 @@ private fun ListActionsMenuButton(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
ListActionsMenuButton(
|
ListActionsMenuButton(
|
||||||
|
note = viewModel::selectedNote,
|
||||||
|
onEditList = {
|
||||||
|
nav.nav { Route.FollowPackMetadataEdit(viewModel.selectedDTag.value) }
|
||||||
|
},
|
||||||
onBroadcastList = {
|
onBroadcastList = {
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
viewModel.loadNote()?.let { updatedSetNote ->
|
viewModel.loadNote()?.let { updatedSetNote ->
|
||||||
@@ -227,6 +247,93 @@ private fun ListActionsMenuButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListActionsMenuButton(
|
||||||
|
note: () -> AddressableNote,
|
||||||
|
onEditList: () -> Unit,
|
||||||
|
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()
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = isActionListOpen.value,
|
||||||
|
onDismissRequest = { isActionListOpen.value = false },
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||||
|
onClick = {
|
||||||
|
val sendIntent =
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
externalLinkForNote(note()),
|
||||||
|
)
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TITLE,
|
||||||
|
stringRes(context, R.string.quick_action_share_browser_link),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareIntent =
|
||||||
|
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||||
|
ContextCompat.startActivity(context, shareIntent, null)
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_pack_edit_list_metadata))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onEditList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_pack_broadcast))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onBroadcastList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_pack_delete))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onDeleteList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
||||||
fun FollowPackViewPreview() {
|
fun FollowPackViewPreview() {
|
||||||
|
|||||||
+5
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -60,6 +61,10 @@ class FollowPackViewModel : ViewModel() {
|
|||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
|
|
||||||
|
fun selectedAddress() = FollowListEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value)
|
||||||
|
|
||||||
|
fun selectedNote() = account.cache.getOrCreateAddressableNote(selectedAddress())
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
account: Account,
|
account: Account,
|
||||||
selectedDTag: String,
|
selectedDTag: String,
|
||||||
|
|||||||
-42
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@@ -36,47 +35,6 @@ class FollowPackViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun listFlow() = accountViewModel.account.followLists.uiListFlow
|
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(
|
fun cloneItem(
|
||||||
followSet: PeopleList,
|
followSet: PeopleList,
|
||||||
customName: String?,
|
customName: String?,
|
||||||
|
|||||||
+28
-16
@@ -41,6 +41,7 @@ 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.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.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
|
||||||
@@ -86,19 +87,22 @@ fun AllPeopleListFeedView(
|
|||||||
color = MaterialTheme.colorScheme.grayText,
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PeopleListFabsAndMenu(
|
NewListButton(
|
||||||
title = R.string.follow_set_creation_dialog_title,
|
onClick = {
|
||||||
onAddSet = peopleListModel::addItem,
|
nav.nav(Route.PeopleListMetadataEdit())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemsIndexed(peopleListFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
itemsIndexed(peopleListFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||||
PeopleListItem(
|
PeopleListItem(
|
||||||
modifier = Modifier.fillMaxSize().animateItem(),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.animateItem(),
|
||||||
peopleList = followSet,
|
peopleList = followSet,
|
||||||
onClick = { nav.nav(peopleListModel.openItem(followSet.identifierTag)) },
|
onClick = { nav.nav(Route.MyPeopleListView(followSet.identifierTag)) },
|
||||||
onRename = { peopleListModel.renameItem(followSet, it) },
|
onEditMetadata = { nav.nav(Route.PeopleListMetadataEdit(followSet.identifierTag)) },
|
||||||
onDescriptionChange = { newDescription -> peopleListModel.changeItemDescription(followSet, newDescription) },
|
|
||||||
onClone = { cloneName, cloneDescription -> peopleListModel.cloneItem(followSet, cloneName, cloneDescription) },
|
onClone = { cloneName, cloneDescription -> peopleListModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||||
onDelete = { peopleListModel.deleteItem(followSet) },
|
onDelete = { peopleListModel.deleteItem(followSet) },
|
||||||
)
|
)
|
||||||
@@ -106,7 +110,10 @@ fun AllPeopleListFeedView(
|
|||||||
}
|
}
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = SpacedBy5dp,
|
horizontalArrangement = SpacedBy5dp,
|
||||||
) {
|
) {
|
||||||
@@ -122,19 +129,22 @@ fun AllPeopleListFeedView(
|
|||||||
color = MaterialTheme.colorScheme.grayText,
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PeopleListFabsAndMenu(
|
NewListButton(
|
||||||
title = R.string.follow_pack_creation_dialog_title,
|
onClick = {
|
||||||
onAddSet = followPackModel::addItem,
|
nav.nav(Route.FollowPackMetadataEdit())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||||
PeopleListItem(
|
PeopleListItem(
|
||||||
modifier = Modifier.fillMaxSize().animateItem(),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.animateItem(),
|
||||||
peopleList = followSet,
|
peopleList = followSet,
|
||||||
onClick = { nav.nav(followPackModel.openItem(followSet.identifierTag)) },
|
onClick = { nav.nav(Route.MyFollowPackView(followSet.identifierTag)) },
|
||||||
onRename = { followPackModel.renameItem(followSet, it) },
|
onEditMetadata = { nav.nav(Route.FollowPackMetadataEdit(followSet.identifierTag)) },
|
||||||
onDescriptionChange = { newDescription -> followPackModel.changeItemDescription(followSet, newDescription) },
|
|
||||||
onClone = { cloneName, cloneDescription -> followPackModel.cloneItem(followSet, cloneName, cloneDescription) },
|
onClone = { cloneName, cloneDescription -> followPackModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||||
onDelete = { followPackModel.deleteItem(followSet) },
|
onDelete = { followPackModel.deleteItem(followSet) },
|
||||||
)
|
)
|
||||||
@@ -147,7 +157,9 @@ fun AllPeopleListFeedView(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AllPeopleListFeedEmpty(message: String = stringRes(R.string.feed_is_empty)) {
|
fun AllPeopleListFeedEmpty(message: String = stringRes(R.string.feed_is_empty)) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxSize().padding(horizontal = Size40dp),
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = Size40dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
-25
@@ -31,8 +31,6 @@ 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.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -81,17 +79,8 @@ fun ListOfPeopleListsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PeopleListFabsAndMenu(
|
fun NewListButton(onClick: () -> Unit) {
|
||||||
title: Int = R.string.follow_set_creation_dialog_title,
|
OutlinedButton(onClick = onClick) {
|
||||||
onAddSet: (name: String, description: String?) -> Unit,
|
|
||||||
) {
|
|
||||||
val isSetAdditionDialogOpen = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = {
|
|
||||||
isSetAdditionDialogOpen.value = true
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Row(horizontalArrangement = SpacedBy5dp, verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalArrangement = SpacedBy5dp, verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
||||||
@@ -100,16 +89,4 @@ fun PeopleListFabsAndMenu(
|
|||||||
Text(stringRes(R.string.follow_set_create_btn_label))
|
Text(stringRes(R.string.follow_set_create_btn_label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSetAdditionDialogOpen.value) {
|
|
||||||
NewPeopleListCreationDialog(
|
|
||||||
title = title,
|
|
||||||
onDismiss = {
|
|
||||||
isSetAdditionDialogOpen.value = false
|
|
||||||
},
|
|
||||||
onCreateList = { name, description ->
|
|
||||||
onAddSet(name, description)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-66
@@ -83,6 +83,7 @@ private fun PeopleListItemPreview() {
|
|||||||
identifierTag = "00001-2222",
|
identifierTag = "00001-2222",
|
||||||
title = "Sample List Title, Very long title, very very very long",
|
title = "Sample List Title, Very long title, very very very long",
|
||||||
description = "Sample List Description",
|
description = "Sample List Description",
|
||||||
|
image = "http://some.com/image.png",
|
||||||
emptySet(),
|
emptySet(),
|
||||||
emptySet(),
|
emptySet(),
|
||||||
)
|
)
|
||||||
@@ -92,6 +93,7 @@ private fun PeopleListItemPreview() {
|
|||||||
identifierTag = "00001-2223",
|
identifierTag = "00001-2223",
|
||||||
title = "Sample List Title",
|
title = "Sample List Title",
|
||||||
description = "Sample List Description",
|
description = "Sample List Description",
|
||||||
|
image = "http://some.com/image.png",
|
||||||
setOf(user1, user3),
|
setOf(user1, user3),
|
||||||
emptySet(),
|
emptySet(),
|
||||||
)
|
)
|
||||||
@@ -101,6 +103,7 @@ private fun PeopleListItemPreview() {
|
|||||||
identifierTag = "00001-2224",
|
identifierTag = "00001-2224",
|
||||||
title = "Sample List Title",
|
title = "Sample List Title",
|
||||||
description = "Sample List Description",
|
description = "Sample List Description",
|
||||||
|
image = "http://some.com/image.png",
|
||||||
emptySet(),
|
emptySet(),
|
||||||
setOf(user1, user3),
|
setOf(user1, user3),
|
||||||
)
|
)
|
||||||
@@ -110,6 +113,7 @@ private fun PeopleListItemPreview() {
|
|||||||
identifierTag = "00001-2225",
|
identifierTag = "00001-2225",
|
||||||
title = "Sample List Title",
|
title = "Sample List Title",
|
||||||
description = "Sample List Description",
|
description = "Sample List Description",
|
||||||
|
image = "http://some.com/image.png",
|
||||||
setOf(user3),
|
setOf(user3),
|
||||||
setOf(user1, user2, user3),
|
setOf(user1, user2, user3),
|
||||||
)
|
)
|
||||||
@@ -120,8 +124,7 @@ private fun PeopleListItemPreview() {
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
peopleList = samplePeopleList1,
|
peopleList = samplePeopleList1,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onRename = {},
|
onEditMetadata = {},
|
||||||
onDescriptionChange = { },
|
|
||||||
onClone = { newName, newDesc -> },
|
onClone = { newName, newDesc -> },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@@ -129,8 +132,7 @@ private fun PeopleListItemPreview() {
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
peopleList = samplePeopleList2,
|
peopleList = samplePeopleList2,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onRename = {},
|
onEditMetadata = {},
|
||||||
onDescriptionChange = { },
|
|
||||||
onClone = { newName, newDesc -> },
|
onClone = { newName, newDesc -> },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@@ -138,8 +140,7 @@ private fun PeopleListItemPreview() {
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
peopleList = samplePeopleList3,
|
peopleList = samplePeopleList3,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onRename = {},
|
onEditMetadata = {},
|
||||||
onDescriptionChange = { },
|
|
||||||
onClone = { newName, newDesc -> },
|
onClone = { newName, newDesc -> },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@@ -147,8 +148,7 @@ private fun PeopleListItemPreview() {
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
peopleList = samplePeopleList4,
|
peopleList = samplePeopleList4,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onRename = {},
|
onEditMetadata = {},
|
||||||
onDescriptionChange = { },
|
|
||||||
onClone = { newName, newDesc -> },
|
onClone = { newName, newDesc -> },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@@ -161,8 +161,7 @@ fun PeopleListItem(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
peopleList: PeopleList,
|
peopleList: PeopleList,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onRename: (String) -> Unit,
|
onEditMetadata: () -> Unit,
|
||||||
onDescriptionChange: (String) -> Unit,
|
|
||||||
onClone: (customName: String?, customDescription: String?) -> Unit,
|
onClone: (customName: String?, customDescription: String?) -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -186,10 +185,7 @@ fun PeopleListItem(
|
|||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
) {
|
) {
|
||||||
PeopleListOptionsButton(
|
PeopleListOptionsButton(
|
||||||
peopleListName = peopleList.title,
|
onListEditMetadata = onEditMetadata,
|
||||||
peopleListDescription = peopleList.description,
|
|
||||||
onListRename = onRename,
|
|
||||||
onListDescriptionChange = onDescriptionChange,
|
|
||||||
onListCloneCreate = onClone,
|
onListCloneCreate = onClone,
|
||||||
onListDelete = onDelete,
|
onListDelete = onDelete,
|
||||||
)
|
)
|
||||||
@@ -276,10 +272,7 @@ fun DisplayParticipantNumberAndStatus(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PeopleListOptionsButton(
|
private fun PeopleListOptionsButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
peopleListName: String,
|
onListEditMetadata: () -> Unit,
|
||||||
peopleListDescription: String?,
|
|
||||||
onListRename: (String) -> Unit,
|
|
||||||
onListDescriptionChange: (String) -> Unit,
|
|
||||||
onListCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
onListCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
||||||
onListDelete: () -> Unit,
|
onListDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -291,35 +284,23 @@ private fun PeopleListOptionsButton(
|
|||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
|
|
||||||
ListOptionsMenu(
|
ListOptionsMenu(
|
||||||
setName = peopleListName,
|
|
||||||
setDescription = peopleListDescription,
|
|
||||||
isExpanded = isMenuOpen.value,
|
isExpanded = isMenuOpen.value,
|
||||||
onDismiss = { isMenuOpen.value = false },
|
onListEditMetadata = onListEditMetadata,
|
||||||
onListRename = onListRename,
|
|
||||||
onListDescriptionChange = onListDescriptionChange,
|
|
||||||
onListClone = onListCloneCreate,
|
onListClone = onListCloneCreate,
|
||||||
onDelete = onListDelete,
|
onDelete = onListDelete,
|
||||||
|
onDismiss = { isMenuOpen.value = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ListOptionsMenu(
|
private fun ListOptionsMenu(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
setName: String,
|
onListEditMetadata: () -> Unit,
|
||||||
setDescription: String?,
|
|
||||||
onListRename: (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,
|
||||||
) {
|
) {
|
||||||
val isRenameDialogOpen = remember { mutableStateOf(false) }
|
|
||||||
val renameString = remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
val isDescriptionModDialogOpen = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val isCopyDialogOpen = remember { mutableStateOf(false) }
|
val isCopyDialogOpen = remember { mutableStateOf(false) }
|
||||||
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
||||||
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
||||||
@@ -330,19 +311,10 @@ private fun ListOptionsMenu(
|
|||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(text = stringRes(R.string.follow_set_rename_btn_label))
|
Text(text = stringRes(R.string.follow_set_edit_list_metadata))
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
isRenameDialogOpen.value = true
|
onListEditMetadata()
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(text = stringRes(R.string.follow_set_desc_modify_label))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isDescriptionModDialogOpen.value = true
|
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -365,28 +337,6 @@ private fun ListOptionsMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRenameDialogOpen.value) {
|
|
||||||
ListRenameDialog(
|
|
||||||
currentName = setName,
|
|
||||||
newName = renameString.value,
|
|
||||||
onStringRenameChange = {
|
|
||||||
renameString.value = it
|
|
||||||
},
|
|
||||||
onDismissDialog = { isRenameDialogOpen.value = false },
|
|
||||||
onListRename = {
|
|
||||||
onListRename(renameString.value)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDescriptionModDialogOpen.value) {
|
|
||||||
ListModifyDescriptionDialog(
|
|
||||||
currentDescription = setDescription,
|
|
||||||
onDismissDialog = { isDescriptionModDialogOpen.value = false },
|
|
||||||
onModifyDescription = onListDescriptionChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCopyDialogOpen.value) {
|
if (isCopyDialogOpen.value) {
|
||||||
ListCloneDialog(
|
ListCloneDialog(
|
||||||
optionalNewName = optionalCloneName.value,
|
optionalNewName = optionalCloneName.value,
|
||||||
|
|||||||
-42
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@@ -36,47 +35,6 @@ class PeopleListViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun listFlow() = accountViewModel.account.peopleLists.uiListFlow
|
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(
|
fun cloneItem(
|
||||||
followSet: PeopleList,
|
followSet: PeopleList,
|
||||||
customName: String?,
|
customName: String?,
|
||||||
|
|||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* 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.metadata
|
||||||
|
|
||||||
|
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.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPackMetadataScreen(
|
||||||
|
selectedDTag: String?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val postViewModel: FollowPackMetadataViewModel = viewModel()
|
||||||
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
if (selectedDTag != null) {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.load(selectedDTag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FollowPackMetadataScaffold(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||||
|
@Composable
|
||||||
|
private fun DialogContentPreview() {
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
val postViewModel: FollowPackMetadataViewModel = viewModel()
|
||||||
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
ThemeComparisonRow {
|
||||||
|
FollowPackMetadataScaffold(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun FollowPackMetadataScaffold(
|
||||||
|
postViewModel: FollowPackMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
FollowPackMetadataTopBar(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { pad ->
|
||||||
|
LazyColumn(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(
|
||||||
|
start = 10.dp,
|
||||||
|
end = 10.dp,
|
||||||
|
top = pad.calculateTopPadding(),
|
||||||
|
bottom = pad.calculateBottomPadding(),
|
||||||
|
).consumeWindowInsets(pad)
|
||||||
|
.imePadding(),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
R.string.follow_pack_title,
|
||||||
|
R.string.follow_pack_explainer,
|
||||||
|
SettingsCategoryFirstModifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
ListName(postViewModel)
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
Picture(postViewModel, accountViewModel)
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
Description(postViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPackMetadataTopBar(
|
||||||
|
postViewModel: FollowPackMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
if (postViewModel.isNewPack) {
|
||||||
|
CreatingTopBar(
|
||||||
|
titleRes = R.string.follow_pack_creation_dialog_title,
|
||||||
|
isActive = postViewModel::canPost,
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
onPost = {
|
||||||
|
try {
|
||||||
|
postViewModel.createOrUpdate()
|
||||||
|
nav.popBack()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SavingTopBar(
|
||||||
|
titleRes = R.string.follow_pack_edit_list_metadata,
|
||||||
|
isActive = postViewModel::canPost,
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
onPost = {
|
||||||
|
try {
|
||||||
|
postViewModel.createOrUpdate()
|
||||||
|
nav.popBack()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Description(postViewModel: FollowPackMetadataViewModel) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.follow_pack_creation_desc_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.description.value,
|
||||||
|
onValueChange = { postViewModel.description.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.about_us),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
minLines = 3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Picture(
|
||||||
|
postViewModel: FollowPackMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.picture_url)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.picture.value,
|
||||||
|
onValueChange = { postViewModel.picture.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "http://mygroup.com/logo.jpg",
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
val context = LocalContext.current
|
||||||
|
SelectSingleFromGallery(
|
||||||
|
isUploading = postViewModel.isUploadingImageForPicture,
|
||||||
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
modifier = Modifier.padding(start = 2.dp),
|
||||||
|
) {
|
||||||
|
postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListName(postViewModel: FollowPackMetadataViewModel) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.follow_pack_creation_name_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.name.value,
|
||||||
|
onValueChange = { postViewModel.name.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.follow_pack_copy_name_label),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
)
|
||||||
|
}
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* 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.metadata
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class FollowPackMetadataViewModel : ViewModel() {
|
||||||
|
private lateinit var accountViewModel: AccountViewModel
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
|
var peopleList by mutableStateOf<PeopleList?>(null)
|
||||||
|
val isNewPack by derivedStateOf { peopleList == null }
|
||||||
|
|
||||||
|
val name = mutableStateOf(TextFieldValue())
|
||||||
|
val picture = mutableStateOf(TextFieldValue())
|
||||||
|
val description = mutableStateOf(TextFieldValue())
|
||||||
|
|
||||||
|
var isUploadingImageForPicture by mutableStateOf(false)
|
||||||
|
|
||||||
|
val canPost by derivedStateOf {
|
||||||
|
name.value.text.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
|
this.accountViewModel = accountViewModel
|
||||||
|
this.account = accountViewModel.account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun new() {
|
||||||
|
peopleList = null
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(dTag: String) {
|
||||||
|
peopleList = account.followLists.selectList(dTag)
|
||||||
|
name.value = TextFieldValue(peopleList?.title ?: "")
|
||||||
|
picture.value = TextFieldValue(peopleList?.image ?: "")
|
||||||
|
description.value = TextFieldValue(peopleList?.description ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createOrUpdate() {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
val peopleList = peopleList
|
||||||
|
if (peopleList == null) {
|
||||||
|
val newListIdentifier =
|
||||||
|
accountViewModel.account.followLists.addFollowList(
|
||||||
|
name = name.value.text,
|
||||||
|
desc = description.value.text,
|
||||||
|
image = picture.value.text,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
accountViewModel.account.followLists.updateMetadata(
|
||||||
|
name = name.value.text,
|
||||||
|
desc = description.value.text,
|
||||||
|
image = picture.value.text,
|
||||||
|
peopleList = peopleList,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
name.value = TextFieldValue()
|
||||||
|
picture.value = TextFieldValue()
|
||||||
|
description.value = TextFieldValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadForPicture(
|
||||||
|
uri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
upload(
|
||||||
|
uri,
|
||||||
|
context,
|
||||||
|
onUploading = { isUploadingImageForPicture = it },
|
||||||
|
onUploaded = { picture.value = TextFieldValue(it) },
|
||||||
|
onError = onError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun upload(
|
||||||
|
galleryUri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onUploading: (Boolean) -> Unit,
|
||||||
|
onUploaded: (String) -> Unit,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
onUploading(true)
|
||||||
|
|
||||||
|
val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result =
|
||||||
|
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
|
||||||
|
Nip96Uploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
onProgress = {},
|
||||||
|
httpAuth = account::createHTTPAuthorization,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BlossomUploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.url != null) {
|
||||||
|
onUploading(false)
|
||||||
|
onUploaded(result.url)
|
||||||
|
} else {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
||||||
|
}
|
||||||
|
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* 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.metadata
|
||||||
|
|
||||||
|
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.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeopleListMetadataScreen(
|
||||||
|
selectedDTag: String?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val postViewModel: PeopleListMetadataViewModel = viewModel()
|
||||||
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
if (selectedDTag != null) {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.load(selectedDTag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PeopleListMetadataScaffold(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||||
|
@Composable
|
||||||
|
private fun DialogContentPreview() {
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
val postViewModel: PeopleListMetadataViewModel = viewModel()
|
||||||
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
ThemeComparisonRow {
|
||||||
|
PeopleListMetadataScaffold(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PeopleListMetadataScaffold(
|
||||||
|
postViewModel: PeopleListMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
PeopleListMetadataTopBar(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { pad ->
|
||||||
|
LazyColumn(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(
|
||||||
|
start = 10.dp,
|
||||||
|
end = 10.dp,
|
||||||
|
top = pad.calculateTopPadding(),
|
||||||
|
bottom = pad.calculateBottomPadding(),
|
||||||
|
).consumeWindowInsets(pad)
|
||||||
|
.imePadding(),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
R.string.people_list_title,
|
||||||
|
R.string.people_list_explainer,
|
||||||
|
SettingsCategoryFirstModifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
ListName(postViewModel)
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
Picture(postViewModel, accountViewModel)
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
Description(postViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeopleListMetadataTopBar(
|
||||||
|
postViewModel: PeopleListMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
if (postViewModel.isNewList) {
|
||||||
|
CreatingTopBar(
|
||||||
|
titleRes = R.string.follow_set_creation_dialog_title,
|
||||||
|
isActive = postViewModel::canPost,
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
onPost = {
|
||||||
|
try {
|
||||||
|
postViewModel.createOrUpdate()
|
||||||
|
nav.popBack()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SavingTopBar(
|
||||||
|
titleRes = R.string.follow_set_edit_list_metadata,
|
||||||
|
isActive = postViewModel::canPost,
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
onPost = {
|
||||||
|
try {
|
||||||
|
postViewModel.createOrUpdate()
|
||||||
|
nav.popBack()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Description(postViewModel: PeopleListMetadataViewModel) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.follow_set_creation_desc_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.description.value,
|
||||||
|
onValueChange = { postViewModel.description.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.about_us),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
minLines = 3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Picture(
|
||||||
|
postViewModel: PeopleListMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.picture_url)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.picture.value,
|
||||||
|
onValueChange = { postViewModel.picture.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "http://mygroup.com/logo.jpg",
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
val context = LocalContext.current
|
||||||
|
SelectSingleFromGallery(
|
||||||
|
isUploading = postViewModel.isUploadingImageForPicture,
|
||||||
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
modifier = Modifier.padding(start = 2.dp),
|
||||||
|
) {
|
||||||
|
postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListName(postViewModel: PeopleListMetadataViewModel) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.follow_set_creation_name_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.name.value,
|
||||||
|
onValueChange = { postViewModel.name.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.follow_set_copy_name_label),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
)
|
||||||
|
}
|
||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* 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.metadata
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class PeopleListMetadataViewModel : ViewModel() {
|
||||||
|
private lateinit var accountViewModel: AccountViewModel
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
|
var peopleList by mutableStateOf<PeopleList?>(null)
|
||||||
|
val isNewList by derivedStateOf { peopleList == null }
|
||||||
|
|
||||||
|
val name = mutableStateOf(TextFieldValue())
|
||||||
|
val picture = mutableStateOf(TextFieldValue())
|
||||||
|
val description = mutableStateOf(TextFieldValue())
|
||||||
|
|
||||||
|
var isUploadingImageForPicture by mutableStateOf(false)
|
||||||
|
|
||||||
|
val canPost by derivedStateOf {
|
||||||
|
name.value.text.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
|
this.accountViewModel = accountViewModel
|
||||||
|
this.account = accountViewModel.account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun new() {
|
||||||
|
peopleList = null
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(dTag: String) {
|
||||||
|
peopleList = account.peopleLists.selectList(dTag)
|
||||||
|
name.value = TextFieldValue(peopleList?.title ?: "")
|
||||||
|
picture.value = TextFieldValue(peopleList?.image ?: "")
|
||||||
|
description.value = TextFieldValue(peopleList?.description ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNewChannel() = peopleList == null
|
||||||
|
|
||||||
|
fun createOrUpdate() {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
val peopleList = peopleList
|
||||||
|
if (peopleList == null) {
|
||||||
|
accountViewModel.account.peopleLists.addFollowList(
|
||||||
|
listName = name.value.text,
|
||||||
|
listDescription = description.value.text,
|
||||||
|
listImage = picture.value.text,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
accountViewModel.account.peopleLists.updateMetadata(
|
||||||
|
listName = name.value.text,
|
||||||
|
listDescription = description.value.text,
|
||||||
|
listImage = picture.value.text,
|
||||||
|
peopleList = peopleList,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
name.value = TextFieldValue()
|
||||||
|
picture.value = TextFieldValue()
|
||||||
|
description.value = TextFieldValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadForPicture(
|
||||||
|
uri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
upload(
|
||||||
|
uri,
|
||||||
|
context,
|
||||||
|
onUploading = { isUploadingImageForPicture = it },
|
||||||
|
onUploaded = { picture.value = TextFieldValue(it) },
|
||||||
|
onError = onError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun upload(
|
||||||
|
galleryUri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onUploading: (Boolean) -> Unit,
|
||||||
|
onUploaded: (String) -> Unit,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
onUploading(true)
|
||||||
|
|
||||||
|
val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result =
|
||||||
|
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
|
||||||
|
Nip96Uploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
onProgress = {},
|
||||||
|
httpAuth = account::createHTTPAuthorization,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BlossomUploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.url != null) {
|
||||||
|
onUploading(false)
|
||||||
|
onUploaded(result.url)
|
||||||
|
} else {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
||||||
|
}
|
||||||
|
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-23
@@ -42,8 +42,9 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||||
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.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.PeopleListFabsAndMenu
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton
|
||||||
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.MaxWidthWithHorzPadding
|
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||||
@@ -95,17 +96,8 @@ fun FollowListAndPackAndUserView(
|
|||||||
color = MaterialTheme.colorScheme.grayText,
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PeopleListFabsAndMenu(
|
NewListButton(
|
||||||
title = R.string.follow_set_creation_dialog_title,
|
onClick = { nav.nav(Route.PeopleListMetadataEdit()) },
|
||||||
onAddSet = { title, description ->
|
|
||||||
accountViewModel.launchSigner {
|
|
||||||
accountViewModel.account.peopleLists.addFollowList(
|
|
||||||
listName = title,
|
|
||||||
listDescription = description,
|
|
||||||
account = accountViewModel.account,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,17 +154,8 @@ fun FollowListAndPackAndUserView(
|
|||||||
color = MaterialTheme.colorScheme.grayText,
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PeopleListFabsAndMenu(
|
NewListButton(
|
||||||
title = R.string.follow_pack_creation_dialog_title,
|
onClick = { nav.nav(Route.FollowPackMetadataEdit()) },
|
||||||
onAddSet = { title, description ->
|
|
||||||
accountViewModel.launchSigner {
|
|
||||||
accountViewModel.account.followLists.addFollowList(
|
|
||||||
name = title,
|
|
||||||
desc = description,
|
|
||||||
account = accountViewModel.account,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,6 +562,7 @@
|
|||||||
<string name="follow_set_creation_action_btn_label">Create list</string>
|
<string name="follow_set_creation_action_btn_label">Create list</string>
|
||||||
<string name="follow_set_copy_action_btn_label">Copy/Clone list</string>
|
<string name="follow_set_copy_action_btn_label">Copy/Clone list</string>
|
||||||
<string name="follow_set_rename_btn_label">Rename list</string>
|
<string name="follow_set_rename_btn_label">Rename list</string>
|
||||||
|
<string name="follow_set_edit_list_metadata">Edit List</string>
|
||||||
<string name="follow_set_desc_modify_btn_label">Modify</string>
|
<string name="follow_set_desc_modify_btn_label">Modify</string>
|
||||||
<string name="follow_set_rename_dialog_indicator_first_part">You are renaming from </string>
|
<string name="follow_set_rename_dialog_indicator_first_part">You are renaming from </string>
|
||||||
<string name="follow_set_rename_dialog_indicator_second_part"> to..</string>
|
<string name="follow_set_rename_dialog_indicator_second_part"> to..</string>
|
||||||
@@ -1356,4 +1357,22 @@
|
|||||||
<string name="remove_user_from_the_list">Remove user from the list</string>
|
<string name="remove_user_from_the_list">Remove user from the list</string>
|
||||||
<string name="follow_list_item_label">Follow Pack</string>
|
<string name="follow_list_item_label">Follow Pack</string>
|
||||||
<string name="members">Members</string>
|
<string name="members">Members</string>
|
||||||
|
|
||||||
|
<string name="people_list_title">Follow List Metadata</string>
|
||||||
|
<string name="people_list_explainer">Follow lists metadata can be seen by anyone on Nostr. Only your private members are encrypted.</string>
|
||||||
|
|
||||||
|
<string name="follow_pack_title">Follow Pack Metadata</string>
|
||||||
|
<string name="follow_pack_explainer">Follow pack metadata can be seen by anyone on Nostr and is frequently published in many websites as startup kits for new users.</string>
|
||||||
|
|
||||||
|
<string name="follow_pack_edit_list_metadata">Edit Follow Pack</string>
|
||||||
|
<string name="follow_pack_creation_name_label">Pack name</string>
|
||||||
|
<string name="follow_pack_copy_name_label">New Pack name</string>
|
||||||
|
<string name="follow_pack_creation_desc_label">Pack description</string>
|
||||||
|
<string name="follow_pack_copy_desc_label">New Pack description</string>
|
||||||
|
|
||||||
|
<string name="follow_set_broadcast">Broadcast List</string>
|
||||||
|
<string name="follow_pack_broadcast">Broadcast Pack</string>
|
||||||
|
|
||||||
|
<string name="follow_set_delete">Delete List</string>
|
||||||
|
<string name="follow_pack_delete">Delete Pack</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+11
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.quartz.nip51Lists.followList
|
package com.vitorpamplona.quartz.nip51Lists.followList
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
@@ -70,6 +71,16 @@ class FollowListEvent(
|
|||||||
const val KIND = 39089
|
const val KIND = 39089
|
||||||
const val ALT = "List of people to follow"
|
const val ALT = "List of people to follow"
|
||||||
|
|
||||||
|
fun createAddress(
|
||||||
|
pubKey: HexKey,
|
||||||
|
dTag: String,
|
||||||
|
) = Address(KIND, pubKey, dTag)
|
||||||
|
|
||||||
|
fun listFor(
|
||||||
|
pubKey: HexKey,
|
||||||
|
dTag: String,
|
||||||
|
): String = Address.assemble(KIND, pubKey, dTag)
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
suspend fun create(
|
suspend fun create(
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
+14
-66
@@ -40,8 +40,8 @@ import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
|||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||||
import com.vitorpamplona.quartz.nip51Lists.replaceAll
|
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
@@ -70,6 +70,8 @@ class PeopleListEvent(
|
|||||||
|
|
||||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||||
|
|
||||||
|
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||||
|
|
||||||
fun users() = tags.users()
|
fun users() = tags.users()
|
||||||
|
|
||||||
fun countMutes() = tags.count(MuteTag::isTagged)
|
fun countMutes() = tags.count(MuteTag::isTagged)
|
||||||
@@ -87,7 +89,17 @@ class PeopleListEvent(
|
|||||||
|
|
||||||
fun createBlockAddress(pubKey: HexKey) = Address(KIND, pubKey, BLOCK_LIST_D_TAG)
|
fun createBlockAddress(pubKey: HexKey) = Address(KIND, pubKey, BLOCK_LIST_D_TAG)
|
||||||
|
|
||||||
fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
|
fun blockListFor(pubKeyHex: HexKey): String = Address.assemble(KIND, pubKeyHex, BLOCK_LIST_D_TAG)
|
||||||
|
|
||||||
|
fun createAddress(
|
||||||
|
pubKey: HexKey,
|
||||||
|
dTag: String,
|
||||||
|
) = Address(KIND, pubKey, dTag)
|
||||||
|
|
||||||
|
fun listFor(
|
||||||
|
pubKey: HexKey,
|
||||||
|
dTag: String,
|
||||||
|
): String = Address.assemble(KIND, pubKey, dTag)
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
suspend fun create(
|
suspend fun create(
|
||||||
@@ -378,69 +390,5 @@ class PeopleListEvent(
|
|||||||
signer = signer,
|
signer = signer,
|
||||||
createdAt = createdAt,
|
createdAt = createdAt,
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun modifyListName(
|
|
||||||
earlierVersion: PeopleListEvent,
|
|
||||||
newName: String,
|
|
||||||
signer: NostrSigner,
|
|
||||||
createdAt: Long = TimeUtils.now(),
|
|
||||||
): PeopleListEvent {
|
|
||||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
|
||||||
val currentTitle = earlierVersion.tags.first { it[0] == NameTag.TAG_NAME || it[0] == TitleTag.TAG_NAME }
|
|
||||||
val newTitleTag =
|
|
||||||
if (currentTitle[0] == NameTag.TAG_NAME) {
|
|
||||||
NameTag.assemble(newName)
|
|
||||||
} else {
|
|
||||||
TitleTag.assemble(newName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resign(
|
|
||||||
publicTags = earlierVersion.tags.replaceAll(currentTitle, newTitleTag),
|
|
||||||
privateTags = privateTags.replaceAll(currentTitle, newTitleTag),
|
|
||||||
signer = signer,
|
|
||||||
createdAt = createdAt,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun modifyDescription(
|
|
||||||
earlierVersion: PeopleListEvent,
|
|
||||||
newDescription: String?,
|
|
||||||
signer: NostrSigner,
|
|
||||||
createdAt: Long = TimeUtils.now(),
|
|
||||||
): PeopleListEvent? {
|
|
||||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
|
||||||
val currentDescriptionTag = earlierVersion.tags.firstOrNull { it[0] == DescriptionTag.TAG_NAME }
|
|
||||||
val currentDescription = currentDescriptionTag?.get(1)
|
|
||||||
if (currentDescription.equals(newDescription)) {
|
|
||||||
// Do nothing
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
if (newDescription == null || newDescription.isEmpty()) {
|
|
||||||
return resign(
|
|
||||||
publicTags = earlierVersion.tags.remove { it[0] == DescriptionTag.TAG_NAME },
|
|
||||||
privateTags = privateTags.remove { it[0] == DescriptionTag.TAG_NAME },
|
|
||||||
signer = signer,
|
|
||||||
createdAt = createdAt,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val newDescriptionTag = DescriptionTag.assemble(newDescription)
|
|
||||||
return if (currentDescriptionTag == null) {
|
|
||||||
resign(
|
|
||||||
publicTags = earlierVersion.tags.plusElement(newDescriptionTag),
|
|
||||||
privateTags = privateTags,
|
|
||||||
signer = signer,
|
|
||||||
createdAt = createdAt,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
resign(
|
|
||||||
publicTags = earlierVersion.tags.replaceAll(currentDescriptionTag, newDescriptionTag),
|
|
||||||
privateTags = privateTags.replaceAll(currentDescriptionTag, newDescriptionTag),
|
|
||||||
signer = signer,
|
|
||||||
createdAt = createdAt,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -22,8 +22,14 @@ package com.vitorpamplona.quartz.nip51Lists.peopleList
|
|||||||
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
||||||
|
|
||||||
fun TagArrayBuilder<PeopleListEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
fun TagArrayBuilder<PeopleListEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<PeopleListEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<PeopleListEvent>.image(url: String) = addUnique(ImageTag.assemble(url))
|
||||||
|
|
||||||
fun TagArrayBuilder<PeopleListEvent>.peoples(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() })
|
fun TagArrayBuilder<PeopleListEvent>.peoples(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() })
|
||||||
|
|||||||
Reference in New Issue
Block a user