From 80906f2c5a67aa43909bd3a85dfcac8d76686b3a Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 29 Sep 2025 11:17:12 +0100 Subject: [PATCH 01/15] Refactor models to take into account mixed lists, due to misunderstanding on my part. --- .../model/nip51Lists/followSets/FollowSet.kt | 14 +++++++------- .../model/nip51Lists/followSets/FollowSetState.kt | 2 +- .../model/nip51Lists/followSets/NostrSet.kt | 8 +++++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt index 59f5ed34c..91cd973a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt @@ -32,8 +32,9 @@ data class FollowSet( val title: String, val description: String?, val visibility: SetVisibility, - val profiles: Set, -) : NostrSet(setVisibility = visibility, content = profiles) { + val privateProfiles: Set = emptySet(), + val publicProfiles: Set = emptySet(), +) : NostrSet(setVisibility = visibility, privateContent = privateProfiles, publicContent = publicProfiles) { companion object { fun mapEventToSet( event: PeopleListEvent, @@ -54,7 +55,7 @@ data class FollowSet( title = listTitle, description = listDescription, visibility = SetVisibility.Private, - profiles = privateFollows.toSet(), + privateProfiles = privateFollows.toSet(), ) } else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) { FollowSet( @@ -62,17 +63,16 @@ data class FollowSet( title = listTitle, description = listDescription, visibility = SetVisibility.Public, - profiles = publicFollows.toSet(), + publicProfiles = publicFollows.toSet(), ) } else { - // Follow set is empty, so assume public. Why? Nostr limitation. - // TODO: Could this be fixed at protocol level? FollowSet( identifierTag = dTag, title = listTitle, description = listDescription, visibility = SetVisibility.Public, - profiles = publicFollows.toSet(), + privateProfiles = privateFollows.toSet(), + publicProfiles = publicFollows.toSet(), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt index 120c651a1..d12f6a9d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt @@ -68,7 +68,7 @@ class FollowSetState( val profilesFlow = getFollowSetNotesFlow() .map { it -> - it.flatMapTo(mutableSetOf()) { it.profiles }.toSet() + it.flatMapTo(mutableSetOf()) { it.privateProfiles + it.publicProfiles }.toSet() }.stateIn(scope, SharingStarted.Eagerly, emptySet()) fun mapNoteToFollowSet(note: Note): FollowSet = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt index aadae3380..13295f7c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt @@ -22,11 +22,13 @@ package com.vitorpamplona.amethyst.model.nip51Lists.followSets sealed class NostrSet( val setVisibility: SetVisibility, - val content: Collection, + val privateContent: Collection, + val publicContent: Collection, ) class CuratedBookmarkSet( val name: String, val visibility: SetVisibility, - val setItems: List, -) : NostrSet(visibility, setItems) + val privateSetItems: List, + val publicSetItems: List, +) : NostrSet(visibility, privateSetItems, publicSetItems) From dfb683e00d8c8b3222500ec0058e2239c25e6dc8 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Tue, 30 Sep 2025 15:37:02 +0100 Subject: [PATCH 02/15] Continue refactoring of models to make things correct. --- .../model/nip51Lists/followSets/FollowSet.kt | 5 +- .../loggedIn/lists/FollowSetFeedViewModel.kt | 8 +-- .../nip51Lists/peopleList/PeopleListEvent.kt | 50 +++++++------------ 3 files changed, 24 insertions(+), 39 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt index 91cd973a3..0ec59bfd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt @@ -31,7 +31,7 @@ data class FollowSet( val identifierTag: String, val title: String, val description: String?, - val visibility: SetVisibility, + val visibility: SetVisibility = SetVisibility.Mixed, val privateProfiles: Set = emptySet(), val publicProfiles: Set = emptySet(), ) : NostrSet(setVisibility = visibility, privateContent = privateProfiles, publicContent = publicProfiles) { @@ -54,7 +54,6 @@ data class FollowSet( identifierTag = dTag, title = listTitle, description = listDescription, - visibility = SetVisibility.Private, privateProfiles = privateFollows.toSet(), ) } else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) { @@ -62,7 +61,6 @@ data class FollowSet( identifierTag = dTag, title = listTitle, description = listDescription, - visibility = SetVisibility.Public, publicProfiles = publicFollows.toSet(), ) } else { @@ -70,7 +68,6 @@ data class FollowSet( identifierTag = dTag, title = listTitle, description = listDescription, - visibility = SetVisibility.Public, privateProfiles = privateFollows.toSet(), publicProfiles = publicFollows.toSet(), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt index c921816cf..4cb41e16b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet -import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter @@ -117,8 +116,8 @@ class FollowSetFeedViewModel( fun addFollowSet( setName: String, setDescription: String?, - isListPrivate: Boolean, optionalFirstMemberHex: String? = null, + firstMemberShouldBePrivate: Boolean = false, account: Account, ) { if (!account.settings.isWriteable()) { @@ -129,7 +128,7 @@ class FollowSetFeedViewModel( dTag = UUID.randomUUID().toString(), title = setName, description = setDescription, - isPrivate = isListPrivate, + isPrivate = firstMemberShouldBePrivate, firstMemberHex = optionalFirstMemberHex, signer = account.signer, ) { @@ -179,6 +178,7 @@ class FollowSetFeedViewModel( fun addUserToSet( userProfileHex: String, followSet: FollowSet, + shouldBePrivateMember: Boolean, account: Account, ) { if (!account.settings.isWriteable()) { @@ -190,7 +190,7 @@ class FollowSetFeedViewModel( PeopleListEvent.addUser( earlierVersion = followSetEvent, pubKeyHex = userProfileHex, - isPrivate = followSet.visibility == SetVisibility.Private, + isPrivate = shouldBePrivateMember, signer = account.signer, ) { account.sendMyPublicAndPrivateOutbox(it) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt index 48ec42f8b..1d2c4c9f1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt @@ -228,45 +228,33 @@ class PeopleListEvent( createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, ) { + val isFirstMemberSpecified = firstMemberHex != null if (description == null) { - val newList = - create( + val newListTemplate = + build( name = title, - person = UserTag(pubKey = firstMemberHex ?: signer.pubKey), - isPrivate = isPrivate, + publicPeople = if (!isPrivate && isFirstMemberSpecified) listOf(UserTag(pubKey = firstMemberHex)) else emptyList(), + privatePeople = if (isPrivate && isFirstMemberSpecified) listOf(UserTag(pubKey = firstMemberHex)) else emptyList(), signer = signer, dTag = dTag, createdAt = createdAt, ) + val newList = signer.sign(newListTemplate) onReady(newList) } else { - if (isPrivate) { - val event = - build( - name = title, - privatePeople = listOf(UserTag(pubKey = firstMemberHex ?: signer.pubKey)), - signer = signer, - dTag = dTag, - createdAt = createdAt, - ) { - addUnique(arrayOf("description", description)) - } - val list = signer.sign(event) - onReady(list) - } else { - val event = - build( - name = title, - publicPeople = listOf(UserTag(pubKey = firstMemberHex ?: signer.pubKey)), - signer = signer, - dTag = dTag, - createdAt = createdAt, - ) { - addUnique(arrayOf("description", description)) - } - val list = signer.sign(event) - onReady(list) - } + val event = + build( + name = title, + publicPeople = if (!isPrivate && isFirstMemberSpecified) listOf(UserTag(pubKey = firstMemberHex)) else emptyList(), + privatePeople = if (isPrivate && isFirstMemberSpecified) listOf(UserTag(pubKey = firstMemberHex)) else emptyList(), + signer = signer, + dTag = dTag, + createdAt = createdAt, + ) { + addUnique(arrayOf("description", description)) + } + val list = signer.sign(event) + onReady(list) } } From f6b3fdcab489444dac73420a5ec44160a5efda5c Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Tue, 30 Sep 2025 15:38:16 +0100 Subject: [PATCH 03/15] Refactor UI to adapt to correct way of managing follow sets. --- .../loggedIn/lists/CustomListsScreen.kt | 107 ++++----------- .../ui/screen/loggedIn/lists/CustomSetItem.kt | 114 +++++++++------- .../lists/followsets/FollowSetScreen.kt | 86 +++++++++--- .../followsets/FollowSetsManagementDialog.kt | 128 ++++++++++++------ amethyst/src/main/res/values/strings.xml | 13 +- 5 files changed, 258 insertions(+), 190 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt index 9a600f9e7..5c7b1e47a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt @@ -30,9 +30,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.PlaylistAdd import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab @@ -48,8 +50,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -119,12 +119,11 @@ fun ListsAndSetsScreen( refresh = { followSetsViewModel.invalidateData() }, - addItem = { title: String, description: String?, listType: SetVisibility -> - val isSetPrivate = listType == SetVisibility.Private + addItem = { title: String, description: String? -> + followSetsViewModel.addFollowSet( setName = title, setDescription = description, - isListPrivate = isSetPrivate, account = accountViewModel.account, ) }, @@ -153,14 +152,14 @@ fun ListsAndSetsScreen( fun CustomListsScreen( followSetFeedState: FollowSetFeedState, refresh: () -> Unit, - addItem: (title: String, description: String?, listType: SetVisibility) -> Unit, + addItem: (title: String, description: String?) -> Unit, openItem: (identifier: String) -> Unit, renameItem: (followSet: FollowSet, newName: String) -> Unit, deleteItem: (followSet: FollowSet) -> Unit, accountViewModel: AccountViewModel, nav: INav, ) { - val pagerState = rememberPagerState { 3 } + val pagerState = rememberPagerState { 2 } val coroutineScope = rememberCoroutineScope() DisappearingScaffold( @@ -185,22 +184,14 @@ fun CustomListsScreen( onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, text = { Text(text = stringRes(R.string.labeled_bookmarks), overflow = TextOverflow.Visible) }, ) - Tab( - selected = pagerState.currentPage == 2, - onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } }, - text = { Text(text = stringRes(R.string.general_bookmarks), overflow = TextOverflow.Visible) }, - ) } } }, floatingButton = { // TODO: Show components based on current tab FollowSetFabsAndMenu( - onAddPrivateSet = { name: String, description: String? -> - addItem(name, description, SetVisibility.Private) - }, - onAddPublicSet = { name: String, description: String? -> - addItem(name, description, SetVisibility.Public) + onAddSet = { name: String, description: String? -> + addItem(name, description) }, ) }, @@ -226,7 +217,7 @@ fun CustomListsScreen( ) 1 -> LabeledBookmarksFeedView() - 2 -> GeneralBookmarksFeedView() +// 2 -> GeneralBookmarksFeedView() } } } @@ -236,57 +227,34 @@ fun CustomListsScreen( @Composable private fun FollowSetFabsAndMenu( modifier: Modifier = Modifier, - onAddPrivateSet: (name: String, description: String?) -> Unit, - onAddPublicSet: (name: String, description: String?) -> Unit, + onAddSet: (name: String, description: String?) -> Unit, ) { val isSetAdditionDialogOpen = remember { mutableStateOf(false) } - val isPrivateOptionTapped = remember { mutableStateOf(false) } - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - FloatingActionButton( - onClick = { - isPrivateOptionTapped.value = true - isSetAdditionDialogOpen.value = true - }, - shape = CircleShape, - containerColor = MaterialTheme.colorScheme.primary, - ) { + ExtendedFloatingActionButton( + text = { + Text(text = "New") + }, + icon = { Icon( - painter = painterResource(R.drawable.lock_plus), + imageVector = Icons.AutoMirrored.Filled.PlaylistAdd, contentDescription = null, - tint = Color.White, ) - } - FloatingActionButton( - onClick = { - isSetAdditionDialogOpen.value = true - }, - shape = CircleShape, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Icon( - painter = painterResource(R.drawable.earth_plus), - contentDescription = null, - tint = Color.White, - ) - } - } + }, + onClick = { + isSetAdditionDialogOpen.value = true + }, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) if (isSetAdditionDialogOpen.value) { NewSetCreationDialog( onDismiss = { isSetAdditionDialogOpen.value = false - isPrivateOptionTapped.value = false }, - shouldBePrivate = isPrivateOptionTapped.value, onCreateList = { name, description -> - if (isPrivateOptionTapped.value) { - onAddPrivateSet(name, description) - } else { - onAddPublicSet(name, description) - } + onAddSet(name, description) }, ) } @@ -296,27 +264,10 @@ private fun FollowSetFabsAndMenu( fun NewSetCreationDialog( modifier: Modifier = Modifier, onDismiss: () -> Unit, - shouldBePrivate: Boolean, onCreateList: (name: String, description: String?) -> Unit, ) { val newListName = remember { mutableStateOf("") } val newListDescription = remember { mutableStateOf(null) } - val context = LocalContext.current - - val listTypeText = - stringRes( - context, - when (shouldBePrivate) { - true -> R.string.follow_set_type_private - false -> R.string.follow_set_type_public - }, - ) - - val listTypeIcon = - when (shouldBePrivate) { - true -> R.drawable.lock - false -> R.drawable.ic_public - } AlertDialog( onDismissRequest = onDismiss, @@ -325,12 +276,8 @@ fun NewSetCreationDialog( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - painter = painterResource(listTypeIcon), - contentDescription = null, - ) Text( - text = stringRes(R.string.follow_set_creation_dialog_title, listTypeText), + text = stringRes(R.string.follow_set_creation_dialog_title), ) } }, @@ -347,7 +294,7 @@ fun NewSetCreationDialog( }, ) Spacer(modifier = DoubleVertSpacer) - // For the list description + // For the set description TextField( value = ( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt index 4d75c61db..0d81b60ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt @@ -37,13 +37,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle @@ -56,7 +53,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet -import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.stringRes @@ -64,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import kotlin.let @Composable fun CustomSetItem( @@ -96,20 +91,75 @@ fun CustomSetItem( ) { Text(followSet.title, fontWeight = FontWeight.Bold) Spacer(modifier = StdHorzSpacer) - FilterChip( - selected = true, - onClick = {}, - label = { - Text(text = "${followSet.profiles.size}") - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.People, - contentDescription = null, + if (followSet.publicProfiles.isEmpty() && followSet.privateProfiles.isEmpty()) { + FilterChip( + selected = true, + onClick = {}, + label = { + Text(text = "No members") + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.People, + contentDescription = null, + ) + }, + shape = ButtonBorder, + ) + } + if (followSet.publicProfiles.isNotEmpty()) { + val publicMemberSize = followSet.publicProfiles.size + val membersLabel = + stringRes( + context, + if (publicMemberSize == 1) { + R.string.follow_set_single_member_label + } else { + R.string.follow_set_multiple_member_label + }, ) - }, - shape = ButtonBorder, - ) + FilterChip( + selected = true, + onClick = {}, + label = { + Text(text = "$publicMemberSize $membersLabel") + }, + leadingIcon = { + Icon( + painterResource(R.drawable.ic_public), + contentDescription = null, + ) + }, + shape = ButtonBorder, + ) + Spacer(modifier = StdHorzSpacer) + } + if (followSet.privateProfiles.isNotEmpty()) { + val privateMemberSize = followSet.privateProfiles.size + val membersLabel = + stringRes( + context, + if (privateMemberSize == 1) { + R.string.follow_set_single_member_label + } else { + R.string.follow_set_multiple_member_label + }, + ) + FilterChip( + selected = true, + onClick = {}, + label = { + Text(text = "$privateMemberSize $membersLabel") + }, + leadingIcon = { + Icon( + painterResource(R.drawable.lock), + contentDescription = null, + ) + }, + shape = ButtonBorder, + ) + } } Spacer(modifier = StdVertSpacer) Text( @@ -119,34 +169,6 @@ fun CustomSetItem( maxLines = 2, ) } - - followSet.visibility.let { - val text by derivedStateOf { - when (it) { - SetVisibility.Public -> stringRes(context, R.string.follow_set_type_public) - SetVisibility.Private -> stringRes(context, R.string.follow_set_type_private) - SetVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed) - } - } - Column( - modifier = Modifier.padding(top = 15.dp), - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - painter = - painterResource( - when (it) { - SetVisibility.Public -> R.drawable.ic_public - SetVisibility.Private -> R.drawable.lock - SetVisibility.Mixed -> R.drawable.format_list_bulleted_type - }, - ), - contentDescription = stringRes(R.string.follow_set_type_description, text), - ) - Text(text, color = Color.Gray, fontWeight = FontWeight.Light) - } - } } Column( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt index d0b930e80..ffaa9367f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt @@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.Row 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.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -68,7 +70,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet -import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose @@ -81,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.VertPadding import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -144,7 +146,8 @@ fun FollowSetScreen( when { selectedSetState.value != null -> { val selectedSet = selectedSetState.value - val users = selectedSet!!.profiles.mapToUsers(accountViewModel).filterNotNull() + val publicMembers = selectedSet!!.publicProfiles.mapToUsers(accountViewModel).filterNotNull() + val privateMembers = selectedSet.privateProfiles.mapToUsers(accountViewModel).filterNotNull() Scaffold( topBar = { TopAppBar( @@ -171,6 +174,7 @@ fun FollowSetScreen( selectedSet, accountViewModel.account, ) + navigator.popBack() }, ) }, @@ -190,7 +194,8 @@ fun FollowSetScreen( bottom = padding.calculateBottomPadding(), ).consumeWindowInsets(padding) .imePadding(), - followSetList = users, + publicMemberList = publicMembers, + privateMemberList = privateMembers, onDeleteUser = { followSetViewModel.removeUserFromSet( it, @@ -233,14 +238,7 @@ fun TitleAndDescription( ) Spacer(modifier = StdHorzSpacer) Icon( - painter = - painterResource( - when (followSet.setVisibility) { - SetVisibility.Public -> R.drawable.ic_public - SetVisibility.Private -> R.drawable.lock - SetVisibility.Mixed -> R.drawable.format_list_bulleted_type - }, - ), + painter = painterResource(R.drawable.format_list_bulleted_type), contentDescription = null, ) } @@ -260,7 +258,8 @@ fun TitleAndDescription( @Composable private fun FollowSetListView( modifier: Modifier = Modifier, - followSetList: List, + publicMemberList: List, + privateMemberList: List, onDeleteUser: (String) -> Unit, accountViewModel: AccountViewModel, nav: INav, @@ -272,14 +271,61 @@ private fun FollowSetListView( contentPadding = FeedPadding, state = listState, ) { - itemsIndexed(followSetList, key = { _, item -> item.pubkeyHex }) { _, item -> - FollowSetListItem( - modifier = Modifier.animateItem(), - user = item, - accountViewModel = accountViewModel, - nav = nav, - onDeleteUser = onDeleteUser, - ) + if (publicMemberList.isNotEmpty()) { + stickyHeader { + Column( + modifier = VertPadding, + ) { + Text( + text = "Public Profiles", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ) + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 2.dp, + color = MaterialTheme.colorScheme.onBackground, + ) + } + } + itemsIndexed(publicMemberList, key = { _, item -> item.pubkeyHex }) { _, item -> + FollowSetListItem( + modifier = Modifier.animateItem(), + user = item, + accountViewModel = accountViewModel, + nav = nav, + onDeleteUser = onDeleteUser, + ) + } + item { + Spacer(modifier = Modifier.height(30.dp)) + } + } + if (privateMemberList.isNotEmpty()) { + stickyHeader { + Text( + text = "Private Profiles", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ) + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 2.dp, + color = MaterialTheme.colorScheme.onBackground, + ) + } + itemsIndexed(privateMemberList, key = { _, item -> item.pubkeyHex }) { _, item -> + FollowSetListItem( + modifier = Modifier.animateItem(), + user = item, + accountViewModel = accountViewModel, + nav = nav, + onDeleteUser = onDeleteUser, + ) + } + item { + Spacer(modifier = Modifier.height(30.dp)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt index 2cc2c35f8..bd7dc24d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt @@ -45,6 +45,8 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.HorizontalDivider @@ -69,6 +71,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -165,16 +168,18 @@ fun FollowSetsManagementDialog( ) { when (followSetsState) { is FollowSetFeedState.Loaded -> { - val lists = (followSetsState as FollowSetFeedState.Loaded).feed + val sets = (followSetsState as FollowSetFeedState.Loaded).feed + + sets.forEachIndexed { index, set -> - lists.forEachIndexed { index, list -> Spacer(StdVertSpacer) FollowSetItem( modifier = Modifier.fillMaxWidth(), - listHeader = list.title, - setVisibility = list.visibility, + listHeader = set.title, + setVisibility = set.visibility, userName = userInfo.toBestDisplayName(), - isUserInList = list.profiles.contains(userHex), + userIsPrivateMember = set.privateProfiles.contains(userHex), + userIsPublicMember = set.publicProfiles.contains(userHex), onRemoveUser = { Log.d( "Amethyst", @@ -182,23 +187,30 @@ fun FollowSetsManagementDialog( ) followSetsViewModel.removeUserFromSet( userHex, - list, + set, account, ) Log.d( "Amethyst", - "Updated List. New size: ${list.profiles.size}", + "Updated List. Private profiles size: ${set.privateProfiles.size}," + + "Public profiles size: ${set.publicProfiles.size}", ) }, - onAddUser = { + onAddUserToList = { userShouldBePrivate -> Log.d( "Amethyst", "ProfileActions: Adding item to list ...", ) - followSetsViewModel.addUserToSet(userHex, list, account) + followSetsViewModel.addUserToSet( + userHex, + set, + userShouldBePrivate, + account, + ) Log.d( "Amethyst", - "Updated List. New size: ${list.profiles.size}", + "Updated List. Private profiles size: ${set.privateProfiles.size}," + + "Public profiles size: ${set.publicProfiles.size}", ) }, ) @@ -222,11 +234,11 @@ fun FollowSetsManagementDialog( if (followSetsState != FollowSetFeedState.Loading) { FollowSetsCreationMenu( userName = userInfo.toBestDisplayName(), - onSetCreate = { setName, setIsPrivate, description -> + onSetCreate = { setName, memberShouldBePrivate, description -> followSetsViewModel.addFollowSet( setName = setName, setDescription = description, - isListPrivate = setIsPrivate, + firstMemberShouldBePrivate = memberShouldBePrivate, optionalFirstMemberHex = userHex, account = account, ) @@ -306,11 +318,13 @@ fun FollowSetItem( listHeader: String, setVisibility: SetVisibility, userName: String, - isUserInList: Boolean, - onAddUser: () -> Unit, + userIsPrivateMember: Boolean, + userIsPublicMember: Boolean, + onAddUserToList: (shouldBePrivateMember: Boolean) -> Unit, onRemoveUser: () -> Unit, ) { val context = LocalContext.current + val isUserInList = userIsPrivateMember || userIsPublicMember Row( modifier = modifier @@ -330,26 +344,10 @@ fun FollowSetItem( ) { Text(listHeader, fontWeight = FontWeight.Bold) Spacer(modifier = StdHorzSpacer) - setVisibility.let { - val text by derivedStateOf { - when (it) { - SetVisibility.Public -> stringRes(context, R.string.follow_set_type_public) - SetVisibility.Private -> stringRes(context, R.string.follow_set_type_private) - SetVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed) - } - } - Icon( - painter = - painterResource( - when (setVisibility) { - SetVisibility.Public -> R.drawable.ic_public - SetVisibility.Private -> R.drawable.lock - SetVisibility.Mixed -> R.drawable.format_list_bulleted_type - }, - ), - contentDescription = stringRes(R.string.follow_set_type_description, text), - ) - } + Icon( + painter = painterResource(R.drawable.format_list_bulleted_type), + contentDescription = stringRes(R.string.follow_set_type_description), + ) } Spacer(modifier = StdVertSpacer) @@ -362,7 +360,11 @@ fun FollowSetItem( Text( text = if (isUserInList) { - stringRes(R.string.follow_set_presence_indicator, userName) + if (userIsPublicMember) { + stringRes(R.string.follow_set_public_presence_indicator, userName) + } else { + stringRes(R.string.follow_set_private_presence_indicator, userName) + } } else { stringRes(R.string.follow_set_absence_indicator, userName) }, @@ -389,9 +391,14 @@ fun FollowSetItem( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { + val isUserAddTapped = remember { mutableStateOf(false) } IconButton( onClick = { - if (isUserInList) onRemoveUser() else onAddUser() + if (isUserInList) { + onRemoveUser() + } else { + isUserAddTapped.value = true + } }, modifier = Modifier @@ -423,15 +430,55 @@ fun FollowSetItem( text = stringRes(if (isUserInList) R.string.remove else R.string.add), color = MaterialTheme.colorScheme.onBackground, ) + + UserAdditionOptionsMenu( + isExpanded = isUserAddTapped.value, + onUserAdd = { shouldBePrivateMember -> + onAddUserToList(shouldBePrivateMember) + }, + onDismiss = { isUserAddTapped.value = false }, + ) } } } +@Composable +private fun UserAdditionOptionsMenu( + modifier: Modifier = Modifier, + isExpanded: Boolean, + onUserAdd: (asPrivateMember: Boolean) -> Unit, + onDismiss: () -> Unit, +) { + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismiss, + ) { + DropdownMenuItem( + text = { + Text(text = "Add as public member") + }, + onClick = { + onUserAdd(false) + onDismiss() + }, + ) + DropdownMenuItem( + text = { + Text(text = "Add as private member") + }, + onClick = { + onUserAdd(true) + onDismiss() + }, + ) + } +} + @Composable fun FollowSetsCreationMenu( modifier: Modifier = Modifier, userName: String, - onSetCreate: (setName: String, setIsPrivate: Boolean, description: String?) -> Unit, + onSetCreate: (setName: String, memberShouldBePrivate: Boolean, description: String?) -> Unit, ) { val isListAdditionDialogOpen = remember { mutableStateOf(false) } val isPrivateOptionTapped = remember { mutableStateOf(false) } @@ -474,7 +521,6 @@ fun FollowSetsCreationMenu( isListAdditionDialogOpen.value = false isPrivateOptionTapped.value = false }, - shouldBePrivate = isPrivateOptionTapped.value, onCreateList = { name, description -> onSetCreate(name, isPrivateOptionTapped.value, description) }, @@ -528,7 +574,11 @@ fun FollowSetCreationItem( } Spacer(modifier = StdVertSpacer) Text( - stringRes(R.string.follow_set_creation_item_description, setTypeLabel, userName), + stringRes( + R.string.follow_set_creation_item_description, + userName, + setTypeLabel.lowercase(Locale.current.platformLocale), + ), fontWeight = FontWeight.Light, overflow = TextOverflow.Ellipsis, maxLines = 2, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 9c6f4283c..5db7302c7 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -522,15 +522,18 @@ Add author to follow set Add or remove user from lists, or create a new list with this user. Icon for %1$s List - "%1$s is present in this list" - "%1$s is not in this list" + %1$s is a public member + %1$s is a private member + member + members + %1$s is not in this list Your Follow Sets No follow sets were found, or you don\'t have any follow sets. Tap below to refresh, or use the menu to create one. There was a problem while fetching: %1$s Make New List - "Create new %1$s list with user - Creates a %1$s follow set, and adds %2$s to it. - New %1$s List + New list with %1$s membership + Creates a new follow set, and adds %1$s as a %2$s member. + New Follow Set Set name Set description(optional) Create set From 2df575d4af1e944aa1b6d82f918ec507421e1f1d Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 3 Oct 2025 16:02:15 +0100 Subject: [PATCH 04/15] Modify the models to support creating a new list with several profiles. Use IO dispatcher in FollowSetState. Introduce follow set copying/cloning, with custom names/descriptions for the clones. --- .../nip51Lists/followSets/FollowSetState.kt | 17 ++-------- .../loggedIn/lists/FollowSetFeedViewModel.kt | 28 +++++++++++++++- .../nip51Lists/peopleList/PeopleListEvent.kt | 32 +++++++++++++++---- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt index d12f6a9d8..0d1326c81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt @@ -25,20 +25,16 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent -import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class FollowSetState( @@ -50,7 +46,7 @@ class FollowSetState( private val isActive = MutableStateFlow(false) suspend fun getFollowSetNotes() = - withContext(Dispatchers.Default) { + withContext(Dispatchers.IO) { val followSetNotes = LocalCache.getFollowSetNotesFor(user) return@withContext followSetNotes } @@ -63,7 +59,7 @@ class FollowSetState( emit(followSets) delay(2000) } - }.flowOn(Dispatchers.Default) + }.flowOn(Dispatchers.IO) val profilesFlow = getFollowSetNotesFlow() @@ -82,14 +78,5 @@ class FollowSetState( init { isActive.update { true } - scope.launch(Dispatchers.Default) { - getFollowSetNotesFlow() - .onCompletion { - isActive.update { false } - }.catch { - Log.e(this@FollowSetState.javaClass.simpleName, "Error on flow collection: ${it.message}") - isActive.update { false } - }.collect {} - } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt index 4cb41e16b..66ff75c3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt @@ -129,7 +129,8 @@ class FollowSetFeedViewModel( title = setName, description = setDescription, isPrivate = firstMemberShouldBePrivate, - firstMemberHex = optionalFirstMemberHex, + firstPublicMembers = if (optionalFirstMemberHex != null) listOf(optionalFirstMemberHex) else emptyList(), + firstPrivateMembers = if (optionalFirstMemberHex != null) listOf(optionalFirstMemberHex) else emptyList(), signer = account.signer, ) { account.sendMyPublicAndPrivateOutbox(it) @@ -159,6 +160,31 @@ class FollowSetFeedViewModel( } } + fun cloneFollowSet( + currentFollowSet: FollowSet, + customCloneName: String?, + customCloneDescription: String?, + account: Account, + ) { + if (!account.settings.isWriteable()) { + println("You are in read-only mode. Please login to make modifications.") + } else { + viewModelScope.launch(Dispatchers.IO) { + PeopleListEvent.createListWithDescription( + dTag = UUID.randomUUID().toString(), + title = customCloneName ?: currentFollowSet.title, + description = customCloneDescription ?: currentFollowSet.description, + isPrivate = false, + firstPublicMembers = currentFollowSet.publicProfiles.toList(), + firstPrivateMembers = currentFollowSet.privateProfiles.toList(), + signer = account.signer, + ) { + account.sendMyPublicAndPrivateOutbox(it) + } + } + } + } + fun deleteFollowSet( followSet: FollowSet, account: Account, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt index 1d2c4c9f1..cb8306e5d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt @@ -223,18 +223,28 @@ class PeopleListEvent( title: String, description: String? = null, isPrivate: Boolean, - firstMemberHex: String? = null, + firstPublicMembers: List = emptyList(), + firstPrivateMembers: List = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, ) { - val isFirstMemberSpecified = firstMemberHex != null if (description == null) { val newListTemplate = build( name = title, - publicPeople = if (!isPrivate && isFirstMemberSpecified) listOf(UserTag(pubKey = firstMemberHex)) else emptyList(), - privatePeople = if (isPrivate && isFirstMemberSpecified) listOf(UserTag(pubKey = firstMemberHex)) else emptyList(), + publicPeople = + if (!isPrivate && firstPublicMembers.isNotEmpty()) { + firstPublicMembers.map { UserTag(pubKey = it) } + } else { + emptyList() + }, + privatePeople = + if (isPrivate && firstPrivateMembers.isNotEmpty()) { + firstPrivateMembers.map { UserTag(pubKey = it) } + } else { + emptyList() + }, signer = signer, dTag = dTag, createdAt = createdAt, @@ -245,8 +255,18 @@ class PeopleListEvent( val event = build( name = title, - publicPeople = if (!isPrivate && isFirstMemberSpecified) listOf(UserTag(pubKey = firstMemberHex)) else emptyList(), - privatePeople = if (isPrivate && isFirstMemberSpecified) listOf(UserTag(pubKey = firstMemberHex)) else emptyList(), + publicPeople = + if (!isPrivate && firstPublicMembers.isNotEmpty()) { + firstPublicMembers.map { UserTag(pubKey = it) } + } else { + emptyList() + }, + privatePeople = + if (isPrivate && firstPrivateMembers.isNotEmpty()) { + firstPrivateMembers.map { UserTag(pubKey = it) } + } else { + emptyList() + }, signer = signer, dTag = dTag, createdAt = createdAt, From e418a26f414c0d4cf63e26ab3f7b702868c87c8e Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 3 Oct 2025 16:26:20 +0100 Subject: [PATCH 05/15] Add support for copying/cloning follow sets in the UI. --- .../loggedIn/lists/CustomListsScreen.kt | 13 ++ .../ui/screen/loggedIn/lists/CustomSetItem.kt | 113 +++++++++++++++++- .../loggedIn/lists/FollowSetFeedView.kt | 6 + amethyst/src/main/res/values/strings.xml | 5 + 4 files changed, 134 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt index 5c7b1e47a..df36448dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt @@ -137,6 +137,14 @@ fun ListsAndSetsScreen( account = accountViewModel.account, ) }, + cloneItem = { followSet, customName, customDescription -> + followSetsViewModel.cloneFollowSet( + currentFollowSet = followSet, + customCloneName = customName, + customCloneDescription = customDescription, + account = accountViewModel.account, + ) + }, deleteItem = { followSet -> followSetsViewModel.deleteFollowSet( followSet = followSet, @@ -155,6 +163,7 @@ fun CustomListsScreen( addItem: (title: String, description: String?) -> Unit, openItem: (identifier: String) -> Unit, renameItem: (followSet: FollowSet, newName: String) -> Unit, + cloneItem: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, deleteItem: (followSet: FollowSet) -> Unit, accountViewModel: AccountViewModel, nav: INav, @@ -213,6 +222,7 @@ fun CustomListsScreen( onRefresh = refresh, onOpenItem = openItem, onRenameItem = renameItem, + onItemClone = cloneItem, onDeleteItem = deleteItem, ) @@ -372,6 +382,9 @@ private fun SetItemPreview() { onFollowSetRename = { println("Follow set new name: $it") }, + onFollowSetClone = { newName, newDesc -> + println("The follow set has been cloned, and has custom name: $newName, Desc: $newDesc") + }, onFollowSetDelete = { println(" The follow set ${sampleFollowSet.title} has been deleted.") }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt index 0d81b60ac..8569624f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -67,6 +68,7 @@ fun CustomSetItem( followSet: FollowSet, onFollowSetClick: () -> Unit, onFollowSetRename: (String) -> Unit, + onFollowSetClone: (customName: String?, customDescription: String?) -> Unit, onFollowSetDelete: () -> Unit, ) { val context = LocalContext.current @@ -122,7 +124,7 @@ fun CustomSetItem( selected = true, onClick = {}, label = { - Text(text = "$publicMemberSize $membersLabel") + Text(text = "$publicMemberSize") }, leadingIcon = { Icon( @@ -149,7 +151,7 @@ fun CustomSetItem( selected = true, onClick = {}, label = { - Text(text = "$privateMemberSize $membersLabel") + Text(text = "$privateMemberSize") }, leadingIcon = { Icon( @@ -182,6 +184,7 @@ fun CustomSetItem( SetOptionsButton( followSetName = followSet.title, onListRename = onFollowSetRename, + onSetCloneCreate = onFollowSetClone, onListDelete = onFollowSetDelete, ) } @@ -189,10 +192,11 @@ fun CustomSetItem( } @Composable -fun SetOptionsButton( +private fun SetOptionsButton( modifier: Modifier = Modifier, followSetName: String, onListRename: (String) -> Unit, + onSetCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit, onListDelete: () -> Unit, ) { val isMenuOpen = remember { mutableStateOf(false) } @@ -207,6 +211,7 @@ fun SetOptionsButton( isExpanded = isMenuOpen.value, onDismiss = { isMenuOpen.value = false }, onListRename = onListRename, + onSetClone = onSetCloneCreate, onDelete = onListDelete, ) } @@ -218,12 +223,17 @@ private fun SetOptionsMenu( isExpanded: Boolean, listName: String, onListRename: (String) -> Unit, + onSetClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit, onDelete: () -> Unit, onDismiss: () -> Unit, ) { val isRenameDialogOpen = remember { mutableStateOf(false) } val renameString = remember { mutableStateOf("") } + val isCopyDialogOpen = remember { mutableStateOf(false) } + val optionalCloneName = remember { mutableStateOf(null) } + val optionalCloneDescription = remember { mutableStateOf(null) } + DropdownMenu( expanded = isExpanded, onDismissRequest = onDismiss, @@ -245,6 +255,15 @@ private fun SetOptionsMenu( onDismiss() }, ) + DropdownMenuItem( + text = { + Text(text = stringRes(R.string.follow_set_copy_action_btn_label)) + }, + onClick = { + isCopyDialogOpen.value = true + onDismiss() + }, + ) } if (isRenameDialogOpen.value) { @@ -260,6 +279,23 @@ private fun SetOptionsMenu( }, ) } + + if (isCopyDialogOpen.value) { + SetCloneDialog( + optionalNewName = optionalCloneName.value, + optionalNewDesc = optionalCloneDescription.value, + onCloneNameChange = { + optionalCloneName.value = it + }, + onCloneDescChange = { + optionalCloneDescription.value = it + }, + onCloneCreate = { name, description -> + onSetClone(optionalCloneName.value, optionalCloneDescription.value) + }, + onDismiss = { isCopyDialogOpen.value = false }, + ) + } } @Composable @@ -321,3 +357,74 @@ private fun RenameDialog( }, ) } + +@Composable +private fun SetCloneDialog( + modifier: Modifier = Modifier, + optionalNewName: String?, + optionalNewDesc: String?, + onCloneNameChange: (String?) -> Unit, + onCloneDescChange: (String?) -> Unit, + onCloneCreate: (customName: String?, customDescription: String?) -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringRes(R.string.follow_set_copy_dialog_title), + ) + } + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.follow_set_copy_indicator_description), + fontSize = 15.sp, + fontWeight = FontWeight.Light, + fontStyle = FontStyle.Italic, + ) + // For the set clone name + TextField( + value = optionalNewName ?: "", + onValueChange = onCloneNameChange, + label = { + Text(text = stringRes(R.string.follow_set_copy_name_label)) + }, + ) + Spacer(modifier = DoubleVertSpacer) + // For the set clone description + TextField( + value = optionalNewDesc ?: "", + onValueChange = onCloneDescChange, + label = { + Text(text = stringRes(R.string.follow_set_copy_desc_label)) + }, + ) + } + }, + confirmButton = { + Button( + onClick = { + onCloneCreate(optionalNewName, optionalNewDesc) + onDismiss() + }, + ) { + Text(stringRes(R.string.follow_set_copy_action_btn_label)) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + ) { + Text(stringRes(R.string.cancel)) + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt index 1ecaca898..a90011f4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt @@ -51,6 +51,7 @@ fun FollowSetFeedView( onRefresh: () -> Unit = {}, onOpenItem: (String) -> Unit = {}, onRenameItem: (targetSet: FollowSet, newName: String) -> Unit, + onItemClone: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, onDeleteItem: (followSet: FollowSet) -> Unit, ) { when (followSetFeedState) { @@ -63,6 +64,7 @@ fun FollowSetFeedView( onRefresh = onRefresh, onItemClick = onOpenItem, onItemRename = onRenameItem, + onItemClone = onItemClone, onItemDelete = onDeleteItem, ) } @@ -91,6 +93,7 @@ fun FollowSetLoaded( onRefresh: () -> Unit = {}, onItemClick: (itemIdentifier: String) -> Unit = {}, onItemRename: (followSet: FollowSet, newName: String) -> Unit, + onItemClone: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, onItemDelete: (followSet: FollowSet) -> Unit, ) { Log.d("FollowSetComposable", "FollowSetLoaded: Follow Set size: ${loadedFeedState.size}") @@ -113,6 +116,9 @@ fun FollowSetLoaded( onFollowSetRename = { onItemRename(set, it) }, + onFollowSetClone = { cloneName, cloneDescription -> + onItemClone(set, cloneName, cloneDescription) + }, onFollowSetDelete = { onItemDelete(set) }, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6ff31fe76..a4743acf1 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -534,9 +534,14 @@ New list with %1$s membership Creates a new follow set, and adds %1$s as a %2$s member. New Follow Set + Copy/Clone Follow Set + You can set a custom name/description for this clone set below. Set name + (Original Set name) Set description(optional) + (Original Set description) Create set + Copy/Clone set Rename set You are renaming from to.. From 55198b15468e9a0a2aa0ca8a8f796587c274deac Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 6 Oct 2025 11:39:43 +0100 Subject: [PATCH 06/15] Move one last string to string resources. --- .../amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt | 2 +- amethyst/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt index 8569624f7..f5612b861 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt @@ -98,7 +98,7 @@ fun CustomSetItem( selected = true, onClick = {}, label = { - Text(text = "No members") + Text(text = stringRes(R.string.follow_set_empty_label)) }, leadingIcon = { Icon( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a4743acf1..1d8fe7d0c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -526,6 +526,7 @@ %1$s is a private member member members + No members %1$s is not in this list Your Follow Sets No follow sets were found, or you don\'t have any follow sets. Tap below to refresh, or use the menu to create one. From 9aa5f1bd9f03af2f8ec2424cd1271ff7169537a8 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 9 Oct 2025 16:56:40 +0100 Subject: [PATCH 07/15] Add support for modifying follow set descriptions. --- .../loggedIn/lists/CustomListsScreen.kt | 12 ++ .../ui/screen/loggedIn/lists/CustomSetItem.kt | 127 +++++++++++++++--- .../loggedIn/lists/FollowSetFeedView.kt | 6 + .../loggedIn/lists/FollowSetFeedViewModel.kt | 21 +++ 4 files changed, 148 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt index df36448dc..0f8c31fbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt @@ -137,6 +137,13 @@ fun ListsAndSetsScreen( account = accountViewModel.account, ) }, + changeItemDescription = { followSet, newDescription -> + followSetsViewModel.modifyFollowSetDescription( + newDescription = newDescription, + followSet = followSet, + account = accountViewModel.account, + ) + }, cloneItem = { followSet, customName, customDescription -> followSetsViewModel.cloneFollowSet( currentFollowSet = followSet, @@ -163,6 +170,7 @@ fun CustomListsScreen( addItem: (title: String, description: String?) -> Unit, openItem: (identifier: String) -> Unit, renameItem: (followSet: FollowSet, newName: String) -> Unit, + changeItemDescription: (followSet: FollowSet, newDescription: String?) -> Unit, cloneItem: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, deleteItem: (followSet: FollowSet) -> Unit, accountViewModel: AccountViewModel, @@ -222,6 +230,7 @@ fun CustomListsScreen( onRefresh = refresh, onOpenItem = openItem, onRenameItem = renameItem, + onItemDescriptionChange = changeItemDescription, onItemClone = cloneItem, onDeleteItem = deleteItem, ) @@ -382,6 +391,9 @@ private fun SetItemPreview() { onFollowSetRename = { println("Follow set new name: $it") }, + onFollowSetDescriptionChange = { description -> + println("The follow set's description has been changed to $description") + }, onFollowSetClone = { newName, newDesc -> println("The follow set has been cloned, and has custom name: $newName, Desc: $newDesc") }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt index f5612b861..9283bf2d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt @@ -68,6 +68,7 @@ fun CustomSetItem( followSet: FollowSet, onFollowSetClick: () -> Unit, onFollowSetRename: (String) -> Unit, + onFollowSetDescriptionChange: (String?) -> Unit, onFollowSetClone: (customName: String?, customDescription: String?) -> Unit, onFollowSetDelete: () -> Unit, ) { @@ -183,7 +184,9 @@ fun CustomSetItem( ) { SetOptionsButton( followSetName = followSet.title, - onListRename = onFollowSetRename, + followSetDescription = followSet.description, + onSetRename = onFollowSetRename, + onSetDescriptionChange = onFollowSetDescriptionChange, onSetCloneCreate = onFollowSetClone, onListDelete = onFollowSetDelete, ) @@ -195,7 +198,9 @@ fun CustomSetItem( private fun SetOptionsButton( modifier: Modifier = Modifier, followSetName: String, - onListRename: (String) -> Unit, + followSetDescription: String?, + onSetRename: (String) -> Unit, + onSetDescriptionChange: (String?) -> Unit, onSetCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit, onListDelete: () -> Unit, ) { @@ -207,10 +212,12 @@ private fun SetOptionsButton( VerticalDotsIcon() SetOptionsMenu( - listName = followSetName, + setName = followSetName, + setDescription = followSetDescription, isExpanded = isMenuOpen.value, onDismiss = { isMenuOpen.value = false }, - onListRename = onListRename, + onSetRename = onSetRename, + onSetDescriptionChange = onSetDescriptionChange, onSetClone = onSetCloneCreate, onDelete = onListDelete, ) @@ -221,8 +228,10 @@ private fun SetOptionsButton( private fun SetOptionsMenu( modifier: Modifier = Modifier, isExpanded: Boolean, - listName: String, - onListRename: (String) -> Unit, + setName: String, + setDescription: String?, + onSetRename: (String) -> Unit, + onSetDescriptionChange: (String?) -> Unit, onSetClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit, onDelete: () -> Unit, onDismiss: () -> Unit, @@ -230,6 +239,8 @@ private fun SetOptionsMenu( val isRenameDialogOpen = remember { mutableStateOf(false) } val renameString = remember { mutableStateOf("") } + val isDescriptionModDialogOpen = remember { mutableStateOf(false) } + val isCopyDialogOpen = remember { mutableStateOf(false) } val optionalCloneName = remember { mutableStateOf(null) } val optionalCloneDescription = remember { mutableStateOf(null) } @@ -238,14 +249,6 @@ private fun SetOptionsMenu( expanded = isExpanded, onDismissRequest = onDismiss, ) { - DropdownMenuItem( - text = { - Text(text = stringRes(R.string.quick_action_delete)) - }, - onClick = { - onDelete() - }, - ) DropdownMenuItem( text = { Text(text = stringRes(R.string.follow_set_rename_btn_label)) @@ -255,6 +258,15 @@ private fun SetOptionsMenu( onDismiss() }, ) + DropdownMenuItem( + text = { + Text(text = "Modify description") + }, + onClick = { + isDescriptionModDialogOpen.value = true + onDismiss() + }, + ) DropdownMenuItem( text = { Text(text = stringRes(R.string.follow_set_copy_action_btn_label)) @@ -264,22 +276,38 @@ private fun SetOptionsMenu( onDismiss() }, ) + DropdownMenuItem( + text = { + Text(text = stringRes(R.string.quick_action_delete)) + }, + onClick = { + onDelete() + }, + ) } if (isRenameDialogOpen.value) { - RenameDialog( - currentName = listName, + SetRenameDialog( + currentName = setName, newName = renameString.value, onStringRenameChange = { renameString.value = it }, onDismissDialog = { isRenameDialogOpen.value = false }, onListRename = { - onListRename(renameString.value) + onSetRename(renameString.value) }, ) } + if (isDescriptionModDialogOpen.value) { + SetModifyDescriptionDialog( + currentDescription = setDescription, + onDismissDialog = { isDescriptionModDialogOpen.value = false }, + onModifyDescription = onSetDescriptionChange, + ) + } + if (isCopyDialogOpen.value) { SetCloneDialog( optionalNewName = optionalCloneName.value, @@ -299,7 +327,7 @@ private fun SetOptionsMenu( } @Composable -private fun RenameDialog( +private fun SetRenameDialog( modifier: Modifier = Modifier, currentName: String, newName: String, @@ -358,6 +386,69 @@ private fun RenameDialog( ) } +@Composable +private fun SetModifyDescriptionDialog( + modifier: Modifier = Modifier, + currentDescription: String?, + onDismissDialog: () -> Unit, + onModifyDescription: (String?) -> Unit, +) { + val updatedDescription = remember { mutableStateOf(null) } + + val modifyIndicatorLabel = + if (currentDescription == null) { + "This list doesn't have a description" + } else { + buildAnnotatedString { + append("Current description: ") + withStyle( + SpanStyle( + fontWeight = FontWeight.Bold, + fontStyle = FontStyle.Normal, + fontSize = 15.sp, + ), + ) { + append("\"" + currentDescription + "\"") + } + }.text + } + + AlertDialog( + onDismissRequest = onDismissDialog, + title = { + Text(text = "Modify description") + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = modifyIndicatorLabel, + fontSize = 15.sp, + fontWeight = FontWeight.Light, + fontStyle = FontStyle.Italic, + ) + TextField( + value = updatedDescription.value ?: "", + onValueChange = { updatedDescription.value = it }, + ) + } + }, + confirmButton = { + Button( + onClick = { + onModifyDescription(updatedDescription.value) + onDismissDialog() + }, + ) { Text(text = "Modify") } + }, + dismissButton = { + Button(onClick = onDismissDialog) { Text(text = stringRes(R.string.cancel)) } + }, + ) +} + @Composable private fun SetCloneDialog( modifier: Modifier = Modifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt index a90011f4c..d9bc4d2fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt @@ -51,6 +51,7 @@ fun FollowSetFeedView( onRefresh: () -> Unit = {}, onOpenItem: (String) -> Unit = {}, onRenameItem: (targetSet: FollowSet, newName: String) -> Unit, + onItemDescriptionChange: (followSet: FollowSet, newDescription: String?) -> Unit, onItemClone: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, onDeleteItem: (followSet: FollowSet) -> Unit, ) { @@ -64,6 +65,7 @@ fun FollowSetFeedView( onRefresh = onRefresh, onItemClick = onOpenItem, onItemRename = onRenameItem, + onItemDescriptionChange = onItemDescriptionChange, onItemClone = onItemClone, onItemDelete = onDeleteItem, ) @@ -93,6 +95,7 @@ fun FollowSetLoaded( onRefresh: () -> Unit = {}, onItemClick: (itemIdentifier: String) -> Unit = {}, onItemRename: (followSet: FollowSet, newName: String) -> Unit, + onItemDescriptionChange: (followSet: FollowSet, newDescription: String?) -> Unit, onItemClone: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, onItemDelete: (followSet: FollowSet) -> Unit, ) { @@ -116,6 +119,9 @@ fun FollowSetLoaded( onFollowSetRename = { onItemRename(set, it) }, + onFollowSetDescriptionChange = { newDescription -> + onItemDescriptionChange(set, newDescription) + }, onFollowSetClone = { cloneName, cloneDescription -> onItemClone(set, cloneName, cloneDescription) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt index 66ff75c3b..33c3f52a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt @@ -160,6 +160,27 @@ class FollowSetFeedViewModel( } } + fun modifyFollowSetDescription( + newDescription: String?, + followSet: FollowSet, + account: Account, + ) { + if (!account.settings.isWriteable()) { + println("You are in read-only mode. Please login to make modifications.") + } else { + viewModelScope.launch(Dispatchers.IO) { + val setEvent = getFollowSetNote(followSet.identifierTag, account)?.event as PeopleListEvent + PeopleListEvent.modifyDescription( + earlierVersion = setEvent, + newDescription = newDescription, + signer = account.signer, + ) { + account.sendMyPublicAndPrivateOutbox(it) + } + } + } + } + fun cloneFollowSet( currentFollowSet: FollowSet, customCloneName: String?, From a8d01e36679e5ad0a8fb9fc24a8aed31fe9888d9 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 9 Oct 2025 17:02:42 +0100 Subject: [PATCH 08/15] Put in the relevant code in PeopleListEvent. Use DescriptionTag instead of manual construction. --- .../nip51Lists/peopleList/PeopleListEvent.kt | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt index cb8306e5d..05215ef58 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt @@ -271,7 +271,7 @@ class PeopleListEvent( dTag = dTag, createdAt = createdAt, ) { - addUnique(arrayOf("description", description)) + addUnique(DescriptionTag.assemble(description)) } val list = signer.sign(event) onReady(list) @@ -359,5 +359,51 @@ class PeopleListEvent( ) onReady(modified) } + + suspend fun modifyDescription( + earlierVersion: PeopleListEvent, + newDescription: String?, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (PeopleListEvent) -> Unit = {}, + ) { + 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 + } else { + if (newDescription == null || newDescription.isEmpty()) { + val modified = + resign( + publicTags = earlierVersion.tags.remove { it[0] == DescriptionTag.TAG_NAME }, + privateTags = privateTags.remove { it[0] == DescriptionTag.TAG_NAME }, + signer = signer, + createdAt = createdAt, + ) + onReady(modified) + } else { + val newDescriptionTag = DescriptionTag.assemble(newDescription) + val modified = + 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, + ) + } + onReady(modified) + } + } + } } } From 3c622f544c5c25ef3ce17ab83b0040019008b548 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 9 Oct 2025 19:55:40 +0100 Subject: [PATCH 09/15] Make a method specially for cloning/copying. Refactor createListWithDescription(). --- .../loggedIn/lists/FollowSetFeedViewModel.kt | 3 +- .../nip51Lists/peopleList/PeopleListEvent.kt | 96 ++++++++++--------- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt index 33c3f52a4..b1cf1e8c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt @@ -191,11 +191,10 @@ class FollowSetFeedViewModel( println("You are in read-only mode. Please login to make modifications.") } else { viewModelScope.launch(Dispatchers.IO) { - PeopleListEvent.createListWithDescription( + PeopleListEvent.copy( dTag = UUID.randomUUID().toString(), title = customCloneName ?: currentFollowSet.title, description = customCloneDescription ?: currentFollowSet.description, - isPrivate = false, firstPublicMembers = currentFollowSet.publicProfiles.toList(), firstPrivateMembers = currentFollowSet.privateProfiles.toList(), signer = account.signer, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt index 05215ef58..13e71223c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt @@ -229,53 +229,55 @@ class PeopleListEvent( createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, ) { - if (description == null) { - val newListTemplate = - build( - name = title, - publicPeople = - if (!isPrivate && firstPublicMembers.isNotEmpty()) { - firstPublicMembers.map { UserTag(pubKey = it) } - } else { - emptyList() - }, - privatePeople = - if (isPrivate && firstPrivateMembers.isNotEmpty()) { - firstPrivateMembers.map { UserTag(pubKey = it) } - } else { - emptyList() - }, - signer = signer, - dTag = dTag, - createdAt = createdAt, - ) - val newList = signer.sign(newListTemplate) - onReady(newList) - } else { - val event = - build( - name = title, - publicPeople = - if (!isPrivate && firstPublicMembers.isNotEmpty()) { - firstPublicMembers.map { UserTag(pubKey = it) } - } else { - emptyList() - }, - privatePeople = - if (isPrivate && firstPrivateMembers.isNotEmpty()) { - firstPrivateMembers.map { UserTag(pubKey = it) } - } else { - emptyList() - }, - signer = signer, - dTag = dTag, - createdAt = createdAt, - ) { - addUnique(DescriptionTag.assemble(description)) - } - val list = signer.sign(event) - onReady(list) - } + val newListTemplate = + build( + name = title, + publicPeople = + if (!isPrivate && firstPublicMembers.isNotEmpty()) { + firstPublicMembers.map { UserTag(pubKey = it) } + } else { + emptyList() + }, + privatePeople = + if (isPrivate && firstPrivateMembers.isNotEmpty()) { + firstPrivateMembers.map { UserTag(pubKey = it) } + } else { + emptyList() + }, + signer = signer, + dTag = dTag, + createdAt = createdAt, + ) { + if (description != null) addUnique(DescriptionTag.assemble(description)) + } + val newList = signer.sign(newListTemplate) + onReady(newList) + } + + suspend fun copy( + dTag: String, + title: String, + description: String? = null, + firstPublicMembers: List = emptyList(), + firstPrivateMembers: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (PeopleListEvent) -> Unit, + ) { + val cloneTemplate = + build( + name = title, + publicPeople = firstPublicMembers.map { UserTag(pubKey = it) }, + privatePeople = firstPrivateMembers.map { UserTag(pubKey = it) }, + signer = signer, + dTag = dTag, + createdAt = createdAt, + ) { + if (description != null) addUnique(DescriptionTag.assemble(description)) + } + + val listClone = signer.sign(cloneTemplate) + onReady(listClone) } suspend fun createListWithUser( From ac60e3d221f224def002afcad7b466b73f2e75b1 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 9 Oct 2025 20:13:00 +0100 Subject: [PATCH 10/15] String resources. --- .../amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt | 10 +++++----- amethyst/src/main/res/values/strings.xml | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt index 9283bf2d7..89dec3b22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt @@ -260,7 +260,7 @@ private fun SetOptionsMenu( ) DropdownMenuItem( text = { - Text(text = "Modify description") + Text(text = stringRes(R.string.follow_set_desc_modify_label)) }, onClick = { isDescriptionModDialogOpen.value = true @@ -397,10 +397,10 @@ private fun SetModifyDescriptionDialog( val modifyIndicatorLabel = if (currentDescription == null) { - "This list doesn't have a description" + stringRes(R.string.follow_set_empty_desc_label) } else { buildAnnotatedString { - append("Current description: ") + append(stringRes(R.string.follow_set_current_desc_label) + " ") withStyle( SpanStyle( fontWeight = FontWeight.Bold, @@ -416,7 +416,7 @@ private fun SetModifyDescriptionDialog( AlertDialog( onDismissRequest = onDismissDialog, title = { - Text(text = "Modify description") + Text(text = stringRes(R.string.follow_set_desc_modify_label)) }, text = { Column( @@ -441,7 +441,7 @@ private fun SetModifyDescriptionDialog( onModifyDescription(updatedDescription.value) onDismissDialog() }, - ) { Text(text = "Modify") } + ) { Text(text = stringRes(R.string.follow_set_desc_modify_btn_label)) } }, dismissButton = { Button(onClick = onDismissDialog) { Text(text = stringRes(R.string.cancel)) } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 1d8fe7d0c..a0d1ef928 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -536,6 +536,9 @@ Creates a new follow set, and adds %1$s as a %2$s member. New Follow Set Copy/Clone Follow Set + Modify description + This list doesn\'t have a description + Current description: You can set a custom name/description for this clone set below. Set name (Original Set name) @@ -544,6 +547,7 @@ Create set Copy/Clone set Rename set + Modify You are renaming from to.. From 2a3c31cc5b785327bd213ce2292bde75963d08be Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 12 Oct 2025 21:13:12 +0200 Subject: [PATCH 11/15] Use latest LightCompressor-enhanced: Fix for Progressive download Replaced obsolete MP4 parser libraries with native MediaMuxer/MediaCodec --- amethyst/build.gradle | 6 ------ .../amethyst/service/uploads/VideoCompressionHelper.kt | 9 +-------- gradle/libs.versions.toml | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index aa3084380..b30502c32 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -151,12 +151,6 @@ android { signingConfig = signingConfigs.debug } } - // TODO: remove this when lightcompressor uses one MP4 parser only - packaging { - resources { - resources.pickFirsts.add('builddef.lst') - } - } flavorDimensions = ["channel"] diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index 2369431f7..73fd68267 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -257,16 +257,9 @@ object VideoCompressionHelper { "Compressed [$size] ($reductionPercent% reduction)", ) - // Attempt to correct the path: if it contains "_temp" then remove it - val correctedPath = - if (path.contains("_temp")) { - path.replace("_temp", "") - } else { - path - } if (continuation.isActive) { continuation.resume( - MediaCompressorResult(Uri.fromFile(File(correctedPath)), contentType, size), + MediaCompressorResult(Uri.fromFile(File(path)), contentType, size), ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1527b5dd..595a5d690 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ languageId = "17.0.6" lazysodiumAndroid = "5.2.0" lazysodiumJava = "5.2.0" lifecycleRuntimeKtx = "2.9.4" -lightcompressor = "1.5.0" +lightcompressor = "use-native-libraries-for-avc-and-hevc-SNAPSHOT" markdown = "f92ef49c9d" media3 = "1.8.0" mockk = "1.14.5" From 13402fa5d4b856842a86e2e65195a0137baf05ba Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 13 Oct 2025 09:25:11 +0200 Subject: [PATCH 12/15] Use lightcompressor-enhanced 1.6.0 release --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 595a5d690..69aa099e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ languageId = "17.0.6" lazysodiumAndroid = "5.2.0" lazysodiumJava = "5.2.0" lifecycleRuntimeKtx = "2.9.4" -lightcompressor = "use-native-libraries-for-avc-and-hevc-SNAPSHOT" +lightcompressor-enhanced = "1.6.0" markdown = "f92ef49c9d" media3 = "1.8.0" mockk = "1.14.5" @@ -63,7 +63,7 @@ core = "1.7.0" mavenPublish = "0.34.0" [libraries] -abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor" } +abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor-enhanced" } accompanist-adaptive = { group = "com.google.accompanist", name = "accompanist-adaptive", version.ref = "accompanistAdaptive" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistAdaptive" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } From ba756c9acd227a7ff118afaa5187d6e7254eae2a Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 13 Oct 2025 20:30:18 +0100 Subject: [PATCH 13/15] Take into account profile membership when removing profile. --- .../nip51Lists/peopleList/PeopleListEvent.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt index 13e71223c..1676c2bc5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt @@ -155,6 +155,31 @@ class PeopleListEvent( ) } + suspend fun remove( + earlierVersion: PeopleListEvent, + person: UserTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): PeopleListEvent { + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + return resign( + publicTags = earlierVersion.tags, + privateTags = privateTags.remove(person.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } else { + return resign( + content = earlierVersion.content, + tags = earlierVersion.tags.remove(person.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } + } + suspend fun resign( publicTags: TagArray, privateTags: TagArray, @@ -321,6 +346,7 @@ class PeopleListEvent( suspend fun removeUser( earlierVersion: PeopleListEvent, pubKeyHex: String, + isUserPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, @@ -329,6 +355,7 @@ class PeopleListEvent( remove( earlierVersion = earlierVersion, person = UserTag(pubKey = pubKeyHex), + isPrivate = isUserPrivate, signer = signer, createdAt = createdAt, ) From e6c841373b46f47901a2d610840c3e093536e520 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 13 Oct 2025 21:04:42 +0100 Subject: [PATCH 14/15] String resources. Remove unused string resource in other locale-specific files, to avoid much work. --- .../amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt | 2 +- .../ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt | 5 +++-- .../lists/followsets/FollowSetsManagementDialog.kt | 6 +++--- amethyst/src/main/res/values-cs-rCZ/strings.xml | 3 +-- amethyst/src/main/res/values-cs/strings.xml | 3 +-- amethyst/src/main/res/values-de-rDE/strings.xml | 3 +-- amethyst/src/main/res/values-de/strings.xml | 3 +-- amethyst/src/main/res/values-hi-rIN/strings.xml | 3 +-- amethyst/src/main/res/values-hu-rHU/strings.xml | 3 +-- amethyst/src/main/res/values-lv-rLV/strings.xml | 2 +- amethyst/src/main/res/values-pl-rPL/strings.xml | 3 +-- amethyst/src/main/res/values-pt-rBR/strings.xml | 3 +-- amethyst/src/main/res/values-sv-rSE/strings.xml | 3 +-- amethyst/src/main/res/values-zh-rCN/strings.xml | 3 +-- amethyst/src/main/res/values/strings.xml | 7 ++++++- 15 files changed, 24 insertions(+), 28 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt index 0f8c31fbf..b5739ef97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt @@ -252,7 +252,7 @@ private fun FollowSetFabsAndMenu( ExtendedFloatingActionButton( text = { - Text(text = "New") + Text(text = stringRes(R.string.follow_set_create_btn_label)) }, icon = { Icon( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt index ffaa9367f..58da75b8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt @@ -77,6 +77,7 @@ import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding @@ -277,7 +278,7 @@ private fun FollowSetListView( modifier = VertPadding, ) { Text( - text = "Public Profiles", + text = stringRes(R.string.follow_set_public_members_header_label), fontSize = 18.sp, fontWeight = FontWeight.Bold, ) @@ -304,7 +305,7 @@ private fun FollowSetListView( if (privateMemberList.isNotEmpty()) { stickyHeader { Text( - text = "Private Profiles", + text = stringRes(R.string.follow_set_private_members_header_label), fontSize = 18.sp, fontWeight = FontWeight.Bold, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt index bd7dc24d8..4ae55ff43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt @@ -346,7 +346,7 @@ fun FollowSetItem( Spacer(modifier = StdHorzSpacer) Icon( painter = painterResource(R.drawable.format_list_bulleted_type), - contentDescription = stringRes(R.string.follow_set_type_description), + contentDescription = stringRes(R.string.follow_set_icon_description), ) } @@ -455,7 +455,7 @@ private fun UserAdditionOptionsMenu( ) { DropdownMenuItem( text = { - Text(text = "Add as public member") + Text(text = stringRes(R.string.follow_set_public_member_add_label)) }, onClick = { onUserAdd(false) @@ -464,7 +464,7 @@ private fun UserAdditionOptionsMenu( ) DropdownMenuItem( text = { - Text(text = "Add as private member") + Text(text = stringRes(R.string.follow_set_private_member_add_label)) }, onClick = { onUserAdd(true) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index a8c63c16b..7f78eb705 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -453,8 +453,7 @@ Zdá se, že zatím nemáte žádné sady sledování.\nKlepněte níže pro obnovení nebo použijte tlačítko přidat k vytvoření nové. Přidat autora do sady sledování Přidat nebo odebrat uživatele ze seznamů, nebo vytvořit nový seznam s tímto uživatelem. - Ikona pro seznam %1$s - "%1$s je v tomto seznamu" + Ikona pro seznam %1$s "%1$s není v tomto seznamu" Vaše sady sledování Nebyly nalezeny žádné sady sledování, nebo žádné nemáte. Klepněte níže pro obnovení nebo použijte menu pro vytvoření nové. diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index 1d68921db..fc5948211 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -961,8 +961,7 @@ Zdá se, že zatím nemáte žádné sady sledování.\nKlepněte níže pro obnovení nebo použijte tlačítko přidat k vytvoření nové. Přidat autora do sady sledování Přidat nebo odebrat uživatele ze seznamů, nebo vytvořit nový seznam s tímto uživatelem. - Ikona pro seznam %1$s - %1$s je v tomto seznamu + Ikona pro seznam %1$s %1$s není v tomto seznamu Vaše sady sledování Nebyly nalezeny žádné sady sledování, nebo žádné nemáte. Klepněte níže pro obnovení nebo použijte menu pro vytvoření nové. diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 3aee42a42..08a8ba61b 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -459,8 +459,7 @@ anz der Bedingungen ist erforderlich Es scheint, dass du noch keine Folge-Sets hast.\nTippe unten zum Aktualisieren oder verwende die Plus-Taste, um ein neues zu erstellen. Autor zum Folge-Set hinzufügen Benutzer zu Listen hinzufügen oder entfernen, oder eine neue Liste mit diesem Benutzer erstellen. - Symbol für %1$s-Liste - "%1$s ist in dieser Liste" + Symbol für %1$s-Liste "%1$s ist nicht in dieser Liste" Deine Folge-Sets Keine Folge-Sets gefunden oder du hast keine. Tippe unten zum Aktualisieren oder verwende das Menü, um eines zu erstellen. diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index 4d90c98c9..fd7c5ffa8 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -1001,8 +1001,7 @@ anz der Bedingungen ist erforderlich Es scheint, dass du noch keine Folge-Sets hast.\nTippe unten zum Aktualisieren oder verwende die Plus-Taste, um ein neues zu erstellen. Autor zum Folge-Set hinzufügen Benutzer zu Listen hinzufügen oder entfernen, oder eine neue Liste mit diesem Benutzer erstellen. - Symbol für %1$s-Liste - %1$s ist in dieser Liste + Symbol für %1$s-Liste %1$s ist nicht in dieser Liste Deine Folge-Sets Keine Folge-Sets gefunden oder du hast keine. Tippe unten zum Aktualisieren oder verwende das Menü, um eines zu erstellen. diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index e1db6d5b0..cf5278fda 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -457,8 +457,7 @@ लेखक जोडें अनुगम्य सूची में प्रयोक्ता को सूचियों में जोडें अथवा हटाएँ अथवा इस प्रयोक्ता के साथ नई सूची बनाएँ। - सूची %1$s के लिए चिह्न - "%1$s इस सूची में है" + सूची %1$s के लिए चिह्न "%1$s इस सूची में नहीं है" आपके अनुगम्य सूचियाँ कोई अनुगम्य सूचियाँ प्राप्त नहीं। अथवा आपका कोई अनुगम्य सूचियाँ हैं नहीं। नवीकरण के लिए नीचे दबाएँ अथवा विकल्पसूची द्वारा एक नया बनाएँ। diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 790806e5d..81e96f8cd 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -457,8 +457,7 @@ Szerző hozzáadása a követési gyűjteményhez Felhasználó hozzáadása vagy eltávolítása a listákból, vagy új lista létrehozása ezzel a felhasználóval. - Ikon a(z) %1$s nevű listához - "A(z) %1$s már a létezik a listában" + Ikon a(z) %1$s nevű listához "A(z) %1$s nincs a listában" Saját követési gyüjtemények Nem találhatók követési gyüjtemények, vagy nincs követési gyüjteménye. Érintse meg az alábbi gombot a frissítéshez vagy használja a menüt egy gyüjtemény létrehozásához. diff --git a/amethyst/src/main/res/values-lv-rLV/strings.xml b/amethyst/src/main/res/values-lv-rLV/strings.xml index 9ecf8d4b7..6044600cf 100644 --- a/amethyst/src/main/res/values-lv-rLV/strings.xml +++ b/amethyst/src/main/res/values-lv-rLV/strings.xml @@ -114,7 +114,7 @@ Sekot saraksts - Ikona %1$s sarakstam + Ikona %1$s sarakstam Izveidot jaunu sarakstu Jaunais %1$s saraksts Kolekcijas nosaukums diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index ede2780e0..c11590812 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -454,8 +454,7 @@ Dodaj autora do zbioru obserwowanych Dodaj lub usuń użytkownika z list, lub utwórz nową listę z tym użytkownikiem. - Ikona dla listy %1$s - "%1$s jest obecny na liście" + Ikona dla listy %1$s "%1$s nie jest na liście" Twój zbiór obserwowanych Nie znaleziono zbiorów obserwowanych lub nie masz żadnych zbiorów obserwowanych. Dotknij poniżej, aby odświeżyć lub użyj menu, aby go utworzyć. diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 439c76e55..2c0879035 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -453,8 +453,7 @@ Parece que você ainda não tem conjuntos de seguimento.\nToque abaixo para atualizar ou use o botão de adicionar para criar um novo. Adicionar autor ao conjunto de seguimento Adicionar ou remover usuário de listas, ou criar uma nova lista com este usuário. - Ícone da lista %1$s - "%1$s está presente nesta lista" + Ícone da lista %1$s "%1$s não está nesta lista" Seus conjuntos de seguimento Nenhum conjunto de seguimento foi encontrado ou você não possui nenhum. Toque abaixo para atualizar ou use o menu para criar um. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 4eb00d879..32e93ab55 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -453,8 +453,7 @@ Det verkar som att du inte har några följ-set ännu.\nTryck nedan för att uppdatera eller använd plusknappen för att skapa ett nytt. Lägg till författare i följ-set Lägg till eller ta bort användare från listor, eller skapa en ny lista med denna användare. - Ikon för %1$s-lista - "%1$s finns i denna lista" + Ikon för %1$s-lista "%1$s finns inte i denna lista" Dina följ-set Inga följ-set hittades, eller så har du inga. Tryck nedan för att uppdatera eller använd menyn för att skapa ett. diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index c08294305..6cbb7f36c 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -457,8 +457,7 @@ 添加作者到关注集 从列表中添加或删除用户,或用此用户创建一个新列表。 - %1$s 列表的图标 - "此列表中有 %1$s" + %1$s 列表的图标 "此列表中没有 %1$s" 您的关注集 未找到关注集,或者你还没有任何关注集。轻按下方刷新,或使用按钮新建。 diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a0d1ef928..81fec61e1 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -519,11 +519,16 @@ It seems you do not have any follow sets yet. \nTap below to refresh, or tap the add buttons to create a new one. + New Add author to follow set Add or remove user from lists, or create a new list with this user. - Icon for %1$s List + Icon for follow set %1$s is a public member %1$s is a private member + Add as public member + Add as private member + Public Profiles + Private Profiles member members No members From 9e991f8a496386e88ee12034bbcc3d547b7672bf Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 13 Oct 2025 21:31:22 +0100 Subject: [PATCH 15/15] Fix user removal functions, by updating them to take into account backend modifications made earlier. --- .../amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt | 2 ++ .../ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt | 1 + .../loggedIn/lists/followsets/FollowSetsManagementDialog.kt | 1 + 3 files changed, 4 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt index b1cf1e8c9..f7aae1c45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt @@ -247,6 +247,7 @@ class FollowSetFeedViewModel( fun removeUserFromSet( userProfileHex: String, + userIsPrivate: Boolean, followSet: FollowSet, account: Account, ) { @@ -259,6 +260,7 @@ class FollowSetFeedViewModel( PeopleListEvent.removeUser( earlierVersion = followSetEvent, pubKeyHex = userProfileHex, + isUserPrivate = userIsPrivate, signer = account.signer, ) { account.sendMyPublicAndPrivateOutbox(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt index 58da75b8a..97144ed83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt @@ -200,6 +200,7 @@ fun FollowSetScreen( onDeleteUser = { followSetViewModel.removeUserFromSet( it, + userIsPrivate = selectedSet.privateProfiles.contains(it), selectedSet, accountViewModel.account, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt index 4ae55ff43..06986bee0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt @@ -187,6 +187,7 @@ fun FollowSetsManagementDialog( ) followSetsViewModel.removeUserFromSet( userHex, + userIsPrivate = set.privateProfiles.contains(userHex), set, account, )