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.nip51Lists.followList.FollowListEvent
|
||||
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.personFirst
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.removePerson
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.title
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
|
||||
import com.vitorpamplona.quartz.utils.flattenToSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -110,6 +114,7 @@ class FollowListsState(
|
||||
identifierTag = this.dTag(),
|
||||
title = this.title() ?: this.dTag(),
|
||||
description = this.description(),
|
||||
image = this.image(),
|
||||
privateMembers = emptySet(),
|
||||
publicMembers = cache.load(this.followIdSet()),
|
||||
)
|
||||
@@ -123,13 +128,17 @@ class FollowListsState(
|
||||
.flowOn(Dispatchers.IO)
|
||||
.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
|
||||
.map { peopleLists ->
|
||||
peopleLists.firstOrNull { list -> list.identifierTag == selectedDTag }
|
||||
}.onStart {
|
||||
emit(uiListFlow.value.firstOrNull { it.identifierTag == selectedDTag })
|
||||
}
|
||||
.map { it.select(dTag) }
|
||||
.onStart { emit(selectList(dTag)) }
|
||||
|
||||
fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex)
|
||||
|
||||
@@ -175,51 +184,43 @@ class FollowListsState(
|
||||
suspend fun addFollowList(
|
||||
name: String,
|
||||
desc: String?,
|
||||
image: String?,
|
||||
member: User? = null,
|
||||
isPrivate: Boolean = false,
|
||||
account: Account,
|
||||
) {
|
||||
): String {
|
||||
val dTag = UUID.randomUUID().toString()
|
||||
|
||||
val newListTemplate =
|
||||
FollowListEvent.build(
|
||||
name = name,
|
||||
people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
dTag = UUID.randomUUID().toString(),
|
||||
dTag = dTag,
|
||||
) {
|
||||
if (desc != null) description(desc)
|
||||
if (image != null) image(image)
|
||||
}
|
||||
|
||||
val newList = signer.sign(newListTemplate)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
return dTag
|
||||
}
|
||||
|
||||
suspend fun renameFollowList(
|
||||
newName: String,
|
||||
followPack: PeopleList,
|
||||
suspend fun updateMetadata(
|
||||
name: String?,
|
||||
desc: String?,
|
||||
image: String?,
|
||||
peopleList: PeopleList,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(followPack.identifierTag)
|
||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||
|
||||
val template =
|
||||
listEvent.update {
|
||||
title(newName)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun modifyFollowSetDescription(
|
||||
newDescription: String,
|
||||
followPack: PeopleList,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(followPack.identifierTag)
|
||||
|
||||
val template =
|
||||
listEvent.update {
|
||||
description(newDescription)
|
||||
if (name != null) title(name)
|
||||
if (desc != null) description(desc)
|
||||
if (image != null) image(image)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ data class PeopleList(
|
||||
val identifierTag: String,
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val image: String?,
|
||||
val privateMembers: 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.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.update
|
||||
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.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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -116,6 +121,7 @@ class PeopleListsState(
|
||||
identifierTag = this.dTag(),
|
||||
title = this.nameOrTitle() ?: this.dTag(),
|
||||
description = this.description(),
|
||||
image = this.image(),
|
||||
privateMembers = cache.load(decryptionCache.privateUserIdSet(this)),
|
||||
publicMembers = cache.load(this.publicUsersIdSet()),
|
||||
)
|
||||
@@ -129,17 +135,17 @@ class PeopleListsState(
|
||||
.flowOn(Dispatchers.IO)
|
||||
.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
|
||||
.map { peopleLists ->
|
||||
peopleLists.firstOrNull { list ->
|
||||
list.identifierTag == selectedDTag
|
||||
}
|
||||
}.onStart {
|
||||
emit(
|
||||
uiListFlow.value.firstOrNull { it.identifierTag == selectedDTag },
|
||||
)
|
||||
}
|
||||
.map { it.select(dTag) }
|
||||
.onStart { emit(selectList(dTag)) }
|
||||
|
||||
fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
|
||||
|
||||
@@ -183,49 +189,48 @@ class PeopleListsState(
|
||||
suspend fun addFollowList(
|
||||
listName: String,
|
||||
listDescription: String?,
|
||||
listImage: String?,
|
||||
member: User? = null,
|
||||
isPrivate: Boolean = false,
|
||||
account: Account,
|
||||
) {
|
||||
val newList =
|
||||
PeopleListEvent.createListWithDescription(
|
||||
dTag = UUID.randomUUID().toString(),
|
||||
title = listName,
|
||||
description = listDescription,
|
||||
): String {
|
||||
val dTag = UUID.randomUUID().toString()
|
||||
val newListTemplate =
|
||||
PeopleListEvent.build(
|
||||
dTag = dTag,
|
||||
name = listName,
|
||||
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
privateMembers = if (isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
signer = account.signer,
|
||||
)
|
||||
) {
|
||||
if (listDescription != null) description(listDescription)
|
||||
if (listImage != null) image(listImage)
|
||||
}
|
||||
|
||||
val newList = signer.sign(newListTemplate)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
return dTag
|
||||
}
|
||||
|
||||
suspend fun renameFollowList(
|
||||
newName: String,
|
||||
suspend fun updateMetadata(
|
||||
listName: String?,
|
||||
listDescription: String?,
|
||||
listImage: String?,
|
||||
peopleList: PeopleList,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||
val newList =
|
||||
PeopleListEvent.modifyListName(
|
||||
earlierVersion = listEvent,
|
||||
newName = newName,
|
||||
signer = account.signer,
|
||||
)
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun modifyFollowSetDescription(
|
||||
newDescription: String?,
|
||||
peopleList: PeopleList,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||
val newList =
|
||||
PeopleListEvent.modifyDescription(
|
||||
earlierVersion = listEvent,
|
||||
newDescription = newDescription,
|
||||
signer = account.signer,
|
||||
)
|
||||
val template =
|
||||
listEvent.update {
|
||||
if (listName != null) name(listName)
|
||||
if (listDescription != null) description(listDescription)
|
||||
if (listImage != null) image(listImage)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
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.packs.FollowPackScreen
|
||||
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.notifications.NotificationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
||||
@@ -132,6 +134,9 @@ fun AppNavigation(
|
||||
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, 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) }
|
||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||
|
||||
|
||||
@@ -63,6 +63,14 @@ sealed class Route {
|
||||
val dTag: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class PeopleListMetadataEdit(
|
||||
val dTag: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class FollowPackMetadataEdit(
|
||||
val dTag: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class PeopleListManagement(
|
||||
val userToAdd: HexKey,
|
||||
) : Route()
|
||||
@@ -294,6 +302,15 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
|
||||
dest.hasRoute<Route.GenericCommentPost>() -> entry.toRoute<Route.GenericCommentPost>()
|
||||
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 -> {
|
||||
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
|
||||
|
||||
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.PaddingValues
|
||||
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.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults.cardElevation
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -55,29 +62,38 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
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.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
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.lists.display.DrawUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.ListActionsMenuButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
||||
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.ThemeComparisonRow
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -354,6 +370,10 @@ private fun ListActionsMenuButton(
|
||||
nav: INav,
|
||||
) {
|
||||
ListActionsMenuButton(
|
||||
note = viewModel::selectedNote,
|
||||
onEditList = {
|
||||
nav.nav { Route.PeopleListMetadataEdit(viewModel.selectedDTag.value) }
|
||||
},
|
||||
onBroadcastList = {
|
||||
accountViewModel.launchSigner {
|
||||
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.User
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -60,6 +61,10 @@ class PeopleListViewModel : ViewModel() {
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
fun selectedAddress() = PeopleListEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value)
|
||||
|
||||
fun selectedNote() = account.cache.getOrCreateAddressableNote(selectedAddress())
|
||||
|
||||
fun init(
|
||||
account: Account,
|
||||
selectedDTag: String,
|
||||
|
||||
+108
-1
@@ -20,6 +20,9 @@
|
||||
*/
|
||||
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.PaddingValues
|
||||
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.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults.cardElevation
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -48,29 +55,38 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
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.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
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.lists.display.DrawUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.ListActionsMenuButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -211,6 +227,10 @@ private fun ListActionsMenuButton(
|
||||
nav: INav,
|
||||
) {
|
||||
ListActionsMenuButton(
|
||||
note = viewModel::selectedNote,
|
||||
onEditList = {
|
||||
nav.nav { Route.FollowPackMetadataEdit(viewModel.selectedDTag.value) }
|
||||
},
|
||||
onBroadcastList = {
|
||||
accountViewModel.launchSigner {
|
||||
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
|
||||
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
||||
fun FollowPackViewPreview() {
|
||||
|
||||
+5
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -60,6 +61,10 @@ class FollowPackViewModel : ViewModel() {
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
fun selectedAddress() = FollowListEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value)
|
||||
|
||||
fun selectedNote() = account.cache.getOrCreateAddressableNote(selectedAddress())
|
||||
|
||||
fun init(
|
||||
account: Account,
|
||||
selectedDTag: String,
|
||||
|
||||
-42
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Stable
|
||||
@@ -36,47 +35,6 @@ class FollowPackViewModel : ViewModel() {
|
||||
|
||||
fun listFlow() = accountViewModel.account.followLists.uiListFlow
|
||||
|
||||
fun addItem(
|
||||
title: String,
|
||||
description: String?,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.addFollowList(
|
||||
name = title,
|
||||
desc = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openItem(dTag: String) = Route.MyFollowPackView(dTag)
|
||||
|
||||
fun renameItem(
|
||||
followSet: PeopleList,
|
||||
newValue: String,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.renameFollowList(
|
||||
newName = newValue,
|
||||
followPack = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeItemDescription(
|
||||
followSet: PeopleList,
|
||||
newDescription: String,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.modifyFollowSetDescription(
|
||||
newDescription = newDescription,
|
||||
followPack = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cloneItem(
|
||||
followSet: PeopleList,
|
||||
customName: String?,
|
||||
|
||||
+28
-16
@@ -41,6 +41,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
@@ -86,19 +87,22 @@ fun AllPeopleListFeedView(
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
PeopleListFabsAndMenu(
|
||||
title = R.string.follow_set_creation_dialog_title,
|
||||
onAddSet = peopleListModel::addItem,
|
||||
NewListButton(
|
||||
onClick = {
|
||||
nav.nav(Route.PeopleListMetadataEdit())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(peopleListFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||
PeopleListItem(
|
||||
modifier = Modifier.fillMaxSize().animateItem(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.animateItem(),
|
||||
peopleList = followSet,
|
||||
onClick = { nav.nav(peopleListModel.openItem(followSet.identifierTag)) },
|
||||
onRename = { peopleListModel.renameItem(followSet, it) },
|
||||
onDescriptionChange = { newDescription -> peopleListModel.changeItemDescription(followSet, newDescription) },
|
||||
onClick = { nav.nav(Route.MyPeopleListView(followSet.identifierTag)) },
|
||||
onEditMetadata = { nav.nav(Route.PeopleListMetadataEdit(followSet.identifierTag)) },
|
||||
onClone = { cloneName, cloneDescription -> peopleListModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||
onDelete = { peopleListModel.deleteItem(followSet) },
|
||||
)
|
||||
@@ -106,7 +110,10 @@ fun AllPeopleListFeedView(
|
||||
}
|
||||
stickyHeader {
|
||||
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,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
@@ -122,19 +129,22 @@ fun AllPeopleListFeedView(
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
PeopleListFabsAndMenu(
|
||||
title = R.string.follow_pack_creation_dialog_title,
|
||||
onAddSet = followPackModel::addItem,
|
||||
NewListButton(
|
||||
onClick = {
|
||||
nav.nav(Route.FollowPackMetadataEdit())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||
PeopleListItem(
|
||||
modifier = Modifier.fillMaxSize().animateItem(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.animateItem(),
|
||||
peopleList = followSet,
|
||||
onClick = { nav.nav(followPackModel.openItem(followSet.identifierTag)) },
|
||||
onRename = { followPackModel.renameItem(followSet, it) },
|
||||
onDescriptionChange = { newDescription -> followPackModel.changeItemDescription(followSet, newDescription) },
|
||||
onClick = { nav.nav(Route.MyFollowPackView(followSet.identifierTag)) },
|
||||
onEditMetadata = { nav.nav(Route.FollowPackMetadataEdit(followSet.identifierTag)) },
|
||||
onClone = { cloneName, cloneDescription -> followPackModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||
onDelete = { followPackModel.deleteItem(followSet) },
|
||||
)
|
||||
@@ -147,7 +157,9 @@ fun AllPeopleListFeedView(
|
||||
@Composable
|
||||
fun AllPeopleListFeedEmpty(message: String = stringRes(R.string.feed_is_empty)) {
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(horizontal = Size40dp),
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = Size40dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
|
||||
+2
-25
@@ -31,8 +31,6 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -81,17 +79,8 @@ fun ListOfPeopleListsScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PeopleListFabsAndMenu(
|
||||
title: Int = R.string.follow_set_creation_dialog_title,
|
||||
onAddSet: (name: String, description: String?) -> Unit,
|
||||
) {
|
||||
val isSetAdditionDialogOpen = remember { mutableStateOf(false) }
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
isSetAdditionDialogOpen.value = true
|
||||
},
|
||||
) {
|
||||
fun NewListButton(onClick: () -> Unit) {
|
||||
OutlinedButton(onClick = onClick) {
|
||||
Row(horizontalArrangement = SpacedBy5dp, verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
||||
@@ -100,16 +89,4 @@ fun PeopleListFabsAndMenu(
|
||||
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",
|
||||
title = "Sample List Title, Very long title, very very very long",
|
||||
description = "Sample List Description",
|
||||
image = "http://some.com/image.png",
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
)
|
||||
@@ -92,6 +93,7 @@ private fun PeopleListItemPreview() {
|
||||
identifierTag = "00001-2223",
|
||||
title = "Sample List Title",
|
||||
description = "Sample List Description",
|
||||
image = "http://some.com/image.png",
|
||||
setOf(user1, user3),
|
||||
emptySet(),
|
||||
)
|
||||
@@ -101,6 +103,7 @@ private fun PeopleListItemPreview() {
|
||||
identifierTag = "00001-2224",
|
||||
title = "Sample List Title",
|
||||
description = "Sample List Description",
|
||||
image = "http://some.com/image.png",
|
||||
emptySet(),
|
||||
setOf(user1, user3),
|
||||
)
|
||||
@@ -110,6 +113,7 @@ private fun PeopleListItemPreview() {
|
||||
identifierTag = "00001-2225",
|
||||
title = "Sample List Title",
|
||||
description = "Sample List Description",
|
||||
image = "http://some.com/image.png",
|
||||
setOf(user3),
|
||||
setOf(user1, user2, user3),
|
||||
)
|
||||
@@ -120,8 +124,7 @@ private fun PeopleListItemPreview() {
|
||||
modifier = Modifier,
|
||||
peopleList = samplePeopleList1,
|
||||
onClick = {},
|
||||
onRename = {},
|
||||
onDescriptionChange = { },
|
||||
onEditMetadata = {},
|
||||
onClone = { newName, newDesc -> },
|
||||
onDelete = {},
|
||||
)
|
||||
@@ -129,8 +132,7 @@ private fun PeopleListItemPreview() {
|
||||
modifier = Modifier,
|
||||
peopleList = samplePeopleList2,
|
||||
onClick = {},
|
||||
onRename = {},
|
||||
onDescriptionChange = { },
|
||||
onEditMetadata = {},
|
||||
onClone = { newName, newDesc -> },
|
||||
onDelete = {},
|
||||
)
|
||||
@@ -138,8 +140,7 @@ private fun PeopleListItemPreview() {
|
||||
modifier = Modifier,
|
||||
peopleList = samplePeopleList3,
|
||||
onClick = {},
|
||||
onRename = {},
|
||||
onDescriptionChange = { },
|
||||
onEditMetadata = {},
|
||||
onClone = { newName, newDesc -> },
|
||||
onDelete = {},
|
||||
)
|
||||
@@ -147,8 +148,7 @@ private fun PeopleListItemPreview() {
|
||||
modifier = Modifier,
|
||||
peopleList = samplePeopleList4,
|
||||
onClick = {},
|
||||
onRename = {},
|
||||
onDescriptionChange = { },
|
||||
onEditMetadata = {},
|
||||
onClone = { newName, newDesc -> },
|
||||
onDelete = {},
|
||||
)
|
||||
@@ -161,8 +161,7 @@ fun PeopleListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
peopleList: PeopleList,
|
||||
onClick: () -> Unit,
|
||||
onRename: (String) -> Unit,
|
||||
onDescriptionChange: (String) -> Unit,
|
||||
onEditMetadata: () -> Unit,
|
||||
onClone: (customName: String?, customDescription: String?) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
@@ -186,10 +185,7 @@ fun PeopleListItem(
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
PeopleListOptionsButton(
|
||||
peopleListName = peopleList.title,
|
||||
peopleListDescription = peopleList.description,
|
||||
onListRename = onRename,
|
||||
onListDescriptionChange = onDescriptionChange,
|
||||
onListEditMetadata = onEditMetadata,
|
||||
onListCloneCreate = onClone,
|
||||
onListDelete = onDelete,
|
||||
)
|
||||
@@ -276,10 +272,7 @@ fun DisplayParticipantNumberAndStatus(
|
||||
@Composable
|
||||
private fun PeopleListOptionsButton(
|
||||
modifier: Modifier = Modifier,
|
||||
peopleListName: String,
|
||||
peopleListDescription: String?,
|
||||
onListRename: (String) -> Unit,
|
||||
onListDescriptionChange: (String) -> Unit,
|
||||
onListEditMetadata: () -> Unit,
|
||||
onListCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
||||
onListDelete: () -> Unit,
|
||||
) {
|
||||
@@ -291,35 +284,23 @@ private fun PeopleListOptionsButton(
|
||||
VerticalDotsIcon()
|
||||
|
||||
ListOptionsMenu(
|
||||
setName = peopleListName,
|
||||
setDescription = peopleListDescription,
|
||||
isExpanded = isMenuOpen.value,
|
||||
onDismiss = { isMenuOpen.value = false },
|
||||
onListRename = onListRename,
|
||||
onListDescriptionChange = onListDescriptionChange,
|
||||
onListEditMetadata = onListEditMetadata,
|
||||
onListClone = onListCloneCreate,
|
||||
onDelete = onListDelete,
|
||||
onDismiss = { isMenuOpen.value = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListOptionsMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
isExpanded: Boolean,
|
||||
setName: String,
|
||||
setDescription: String?,
|
||||
onListRename: (String) -> Unit,
|
||||
onListDescriptionChange: (String) -> Unit,
|
||||
onListEditMetadata: () -> Unit,
|
||||
onListClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val isRenameDialogOpen = remember { mutableStateOf(false) }
|
||||
val renameString = remember { mutableStateOf("") }
|
||||
|
||||
val isDescriptionModDialogOpen = remember { mutableStateOf(false) }
|
||||
|
||||
val isCopyDialogOpen = remember { mutableStateOf(false) }
|
||||
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
||||
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
||||
@@ -330,19 +311,10 @@ private fun ListOptionsMenu(
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_rename_btn_label))
|
||||
Text(text = stringRes(R.string.follow_set_edit_list_metadata))
|
||||
},
|
||||
onClick = {
|
||||
isRenameDialogOpen.value = true
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_desc_modify_label))
|
||||
},
|
||||
onClick = {
|
||||
isDescriptionModDialogOpen.value = true
|
||||
onListEditMetadata()
|
||||
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) {
|
||||
ListCloneDialog(
|
||||
optionalNewName = optionalCloneName.value,
|
||||
|
||||
-42
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Stable
|
||||
@@ -36,47 +35,6 @@ class PeopleListViewModel : ViewModel() {
|
||||
|
||||
fun listFlow() = accountViewModel.account.peopleLists.uiListFlow
|
||||
|
||||
fun addItem(
|
||||
title: String,
|
||||
description: String?,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.addFollowList(
|
||||
listName = title,
|
||||
listDescription = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openItem(dTag: String) = Route.MyPeopleListView(dTag)
|
||||
|
||||
fun renameItem(
|
||||
followSet: PeopleList,
|
||||
newValue: String,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.renameFollowList(
|
||||
newName = newValue,
|
||||
peopleList = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeItemDescription(
|
||||
followSet: PeopleList,
|
||||
newDescription: String?,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.modifyFollowSetDescription(
|
||||
newDescription = newDescription,
|
||||
peopleList = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cloneItem(
|
||||
followSet: PeopleList,
|
||||
customName: String?,
|
||||
|
||||
+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.service.relayClient.reqCommand.user.observeUserName
|
||||
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.lists.list.PeopleListFabsAndMenu
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
@@ -95,17 +96,8 @@ fun FollowListAndPackAndUserView(
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
PeopleListFabsAndMenu(
|
||||
title = R.string.follow_set_creation_dialog_title,
|
||||
onAddSet = { title, description ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.peopleLists.addFollowList(
|
||||
listName = title,
|
||||
listDescription = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
NewListButton(
|
||||
onClick = { nav.nav(Route.PeopleListMetadataEdit()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -162,17 +154,8 @@ fun FollowListAndPackAndUserView(
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
PeopleListFabsAndMenu(
|
||||
title = R.string.follow_pack_creation_dialog_title,
|
||||
onAddSet = { title, description ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.followLists.addFollowList(
|
||||
name = title,
|
||||
desc = description,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
}
|
||||
},
|
||||
NewListButton(
|
||||
onClick = { nav.nav(Route.FollowPackMetadataEdit()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,6 +562,7 @@
|
||||
<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_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_rename_dialog_indicator_first_part">You are renaming from </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="follow_list_item_label">Follow Pack</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>
|
||||
|
||||
Reference in New Issue
Block a user