From 80906f2c5a67aa43909bd3a85dfcac8d76686b3a Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 29 Sep 2025 11:17:12 +0100 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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 133c897731f9045dcf0af1338b183ae88dbb78f4 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 13 Oct 2025 18:37:28 +0200 Subject: [PATCH 13/22] Added junit and mockK for androidTest UI dependencies Added failing androidTest for mixed image/video parsing --- amethyst/build.gradle | 4 + .../amethyst/ParagraphParserTest.kt | 217 ++++++++++++++++++ gradle/libs.versions.toml | 2 + 3 files changed, 223 insertions(+) create mode 100644 amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ParagraphParserTest.kt diff --git a/amethyst/build.gradle b/amethyst/build.gradle index aa3084380..d1c269684 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -155,6 +155,8 @@ android { packaging { resources { resources.pickFirsts.add('builddef.lst') + resources.pickFirsts.add('META-INF/LICENSE.md') + resources.pickFirsts.add('META-INF/LICENSE-notice.md') } } @@ -361,6 +363,8 @@ dependencies { androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.junit.ktx androidTestImplementation libs.androidx.espresso.core + androidTestImplementation libs.androidx.ui.test.junit4 + androidTestImplementation libs.mockk.android debugImplementation platform(libs.androidx.compose.bom) debugImplementation libs.androidx.ui.tooling diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ParagraphParserTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ParagraphParserTest.kt new file mode 100644 index 000000000..8e33ba4f7 --- /dev/null +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ParagraphParserTest.kt @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.commons.richtext.ImageSegment +import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.richtext.Segment +import com.vitorpamplona.amethyst.ui.components.ParagraphParser +import com.vitorpamplona.amethyst.ui.components.RenderContext +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList +import io.mockk.mockk +import kotlinx.collections.immutable.toImmutableList +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ParagraphParserTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testMixedImageAndVideoRenderedIndividually() { + // Test the bug: when mixed image + video, both should be rendered individually + val text = + "Renfield (2023)\n" + + "https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg\n" + + "https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4" + + val state = RichTextParser().parseText(text, EmptyTagList, null) + + // Extract the image segments from the parsed paragraphs + val imageSegments = mutableListOf() + state.paragraphs.forEach { paragraph -> + paragraph.words.forEach { word -> + if (word is ImageSegment) { + imageSegments.add(word) + } + } + } + + // Should have 2 image segments (image + video URLs) + assertEquals(2, imageSegments.size) + + // Track what gets rendered + val singleWordRenders = mutableListOf() + val galleryRenders = mutableListOf>() + + // Set up the test with mocked dependencies + val context = + RenderContext( + state = state, + backgroundColor = mutableStateOf(Color.White), + quotesLeft = 3, + callbackUri = null, + accountViewModel = mockk(relaxed = true), + nav = mockk(relaxed = true), + ) + + // Execute the actual ParagraphParser method + composeTestRule.setContent { + ParagraphParser().ProcessWordsWithImageGrouping( + words = imageSegments.toImmutableList(), + context = context, + renderSingleWord = { segment, _ -> + singleWordRenders.add(segment) + }, + renderGallery = { images, _ -> + galleryRenders.add(images) + }, + ) + } + + composeTestRule.waitForIdle() + + assertTrue( + "Mixed image/video should be rendered individually (2 renders), not as gallery (0 renders). " + + "Found: $singleWordRenders individual, $galleryRenders gallery", + singleWordRenders.size == 2 && galleryRenders.isEmpty(), + ) + } + + @Test + fun testMultipleImagesRenderedAsGallery() { + // Test that multiple images (no videos) are correctly grouped as gallery + val text = + "Gallery:\n" + + "https://example.com/image1.jpg\n" + + "https://example.com/image2.png" + + val state = RichTextParser().parseText(text, EmptyTagList, null) + + // Extract the image segments + val imageSegments = mutableListOf() + state.paragraphs.forEach { paragraph -> + paragraph.words.forEach { word -> + if (word is ImageSegment) { + imageSegments.add(word) + } + } + } + + assertEquals(2, imageSegments.size) + + // Track renders + val singleWordRenders = mutableListOf() + val galleryRenders = mutableListOf>() + + val context = + RenderContext( + state = state, + backgroundColor = mutableStateOf(Color.White), + quotesLeft = 3, + callbackUri = null, + accountViewModel = mockk(relaxed = true), + nav = mockk(relaxed = true), + ) + + composeTestRule.setContent { + ParagraphParser().ProcessWordsWithImageGrouping( + words = imageSegments.toImmutableList(), + context = context, + renderSingleWord = { segment, _ -> + singleWordRenders.add(segment) + }, + renderGallery = { images, _ -> + galleryRenders.add(images) + }, + ) + } + + composeTestRule.waitForIdle() + + // Should render as gallery (1 gallery with 2 images) + assertEquals("Should render 1 gallery", 1, galleryRenders.size) + assertEquals("Gallery should contain 2 images", 2, galleryRenders[0].size) + assertEquals("Should not render individually", 0, singleWordRenders.size) + } + + @Test + fun testSingleImageRenderedIndividually() { + // Test that a single image is rendered individually, not as gallery + val text = "Single image:\nhttps://example.com/image.jpg" + + val state = RichTextParser().parseText(text, EmptyTagList, null) + + val imageSegments = mutableListOf() + state.paragraphs.forEach { paragraph -> + paragraph.words.forEach { word -> + if (word is ImageSegment) { + imageSegments.add(word) + } + } + } + + assertEquals(1, imageSegments.size) + + val singleWordRenders = mutableListOf() + val galleryRenders = mutableListOf>() + + val context = + RenderContext( + state = state, + backgroundColor = mutableStateOf(Color.White), + quotesLeft = 3, + callbackUri = null, + accountViewModel = mockk(relaxed = true), + nav = mockk(relaxed = true), + ) + + composeTestRule.setContent { + ParagraphParser().ProcessWordsWithImageGrouping( + words = imageSegments.toImmutableList(), + context = context, + renderSingleWord = { segment, _ -> + singleWordRenders.add(segment) + }, + renderGallery = { images, _ -> + galleryRenders.add(images) + }, + ) + } + + composeTestRule.waitForIdle() + + // Should render individually (not as gallery) + assertEquals("Should render 1 individual image", 1, singleWordRenders.size) + assertEquals("Should not render as gallery", 0, galleryRenders.size) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1527b5dd..9f8a20577 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -102,6 +102,7 @@ androidx-security-crypto-ktx = { group = "androidx.security", name = "security-c androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } audiowaveform = { group = "com.github.lincollincol", name = "compose-audiowaveform", version.ref = "audiowaveform" } @@ -128,6 +129,7 @@ markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", nam markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" } markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"} okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" } From 349156f3806ea67daf9a01a0ca19b9f8af702227 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 13 Oct 2025 18:40:57 +0200 Subject: [PATCH 14/22] fix: Only render as gallery if all segments are images --- .../amethyst/ui/components/ParagraphParser.kt | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ParagraphParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ParagraphParser.kt index a943b37af..e26cefc06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ParagraphParser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ParagraphParser.kt @@ -197,19 +197,26 @@ class ParagraphParser { j++ } - if (imageSegments.size > 1) { - val imageContents = - imageSegments - .mapNotNull { segment -> - val imageUrl = segment.segmentText - context.state.imagesForPager[imageUrl] as? MediaUrlImage - }.toImmutableList() - - if (imageContents.isNotEmpty()) { - renderGallery(imageContents, context.accountViewModel) - } - } else { + if (imageSegments.size <= 1) { renderSingleWord(imageSegments.firstOrNull() ?: word, context) + } else { + val resolvedImages = + imageSegments.mapNotNull { segment -> + val imageUrl = segment.segmentText + context.state.imagesForPager[imageUrl] as? MediaUrlImage + } + + // Render gallery only if all segments are images + if (resolvedImages.size == imageSegments.size) { + renderGallery( + resolvedImages.toImmutableList(), + context.accountViewModel, + ) + } else { + imageSegments.forEach { segment -> + renderSingleWord(segment, context) + } + } } i = j // jump past processed run From ba756c9acd227a7ff118afaa5187d6e7254eae2a Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 13 Oct 2025 20:30:18 +0100 Subject: [PATCH 15/22] 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 16/22] 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 17/22] 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, ) From 85bdf67ac3fcb427f13edf6d5ebcdfd4bfb6ba09 Mon Sep 17 00:00:00 2001 From: davotoula Date: Tue, 14 Oct 2025 13:21:46 +0200 Subject: [PATCH 18/22] added failing test: Public tags should be preserved when removing from private bookmarks --- .../bookmarkList/BookmarkListEventTest.kt | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEventTest.kt diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEventTest.kt new file mode 100644 index 000000000..549253cf2 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEventTest.kt @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.bookmarkList + +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark +import com.vitorpamplona.quartz.utils.nsecToKeyPair +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BookmarkListEventTest { + private val signer = NostrSignerInternal("nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair()) + + @Test + fun publicTagsPreservedWhenRemovingFromPrivateBookmarks() = + runBlocking { + // Create a test event bookmark + val testEventId = "a".repeat(64) + val testBookmark = EventBookmark(testEventId) + + // Create a bookmark list with event in public bookmarks + val initialEvent = + BookmarkListEvent.create( + publicBookmarks = listOf(testBookmark), + privateBookmarks = emptyList(), + signer = signer, + createdAt = 1740669816, + ) + + // Count the public tags (should be 1 for the bookmark) + val initialPublicTagCount = initialEvent.tags.count { tag -> tag.size >= 2 && tag[0] == "e" && tag[1] == testEventId } + assertTrue( + initialPublicTagCount == 1, + "Should have exactly 1 public bookmark tag initially", + ) + + // Try to remove the bookmark from private bookmarks (even though it's only in public) + // This simulates the scenario where a bookmark exists in both lists + val updatedEvent = + BookmarkListEvent.remove( + earlierVersion = initialEvent, + bookmarkIdTag = testBookmark, + isPrivate = true, + signer = signer, + createdAt = 1740669817, + ) + + // Count the public tags again + val finalPublicTagCount = updatedEvent.tags.count { tag -> tag.size >= 2 && tag[0] == "e" && tag[1] == testEventId } + + // CRITICAL: The public tag count should be unchanged + // Without the fix, this would be 0 because the bug also removed public tags + assertTrue( + finalPublicTagCount == initialPublicTagCount, + "Public tags should be preserved when removing from private bookmarks. Initial: $initialPublicTagCount, Final: $finalPublicTagCount", + ) + + // Also verify using publicBookmarks() method + assertTrue( + updatedEvent.publicBookmarks().any { it is EventBookmark && it.eventId == testEventId }, + "Bookmark should still be accessible via publicBookmarks() method", + ) + } + + @Test + fun publicTagsRemovedWhenRemovingFromPublicBookmarks() = + runBlocking { + // Create a test event bookmark + val testEventId = "b".repeat(64) + val testBookmark = EventBookmark(testEventId) + + // Create a bookmark list with event only in public bookmarks + val initialEvent = + BookmarkListEvent.create( + publicBookmarks = listOf(testBookmark), + privateBookmarks = emptyList(), + signer = signer, + createdAt = 1740669816, + ) + + // Remove from public bookmarks + val updatedEvent = + BookmarkListEvent.remove( + earlierVersion = initialEvent, + bookmarkIdTag = testBookmark, + isPrivate = false, + signer = signer, + createdAt = 1740669817, + ) + + // Verify the bookmark was removed from public bookmarks + val finalPublicTagCount = updatedEvent.tags.count { tag -> tag.size >= 2 && tag[0] == "e" && tag[1] == testEventId } + assertTrue( + finalPublicTagCount == 0, + "Public tags should be removed when isPrivate=false", + ) + + assertFalse( + updatedEvent.publicBookmarks().any { it is EventBookmark && it.eventId == testEventId }, + "Bookmark should be removed from public bookmarks", + ) + } +} From 3a3d1a6f25cfb8ef1a047a5905ac6d0f7132c7c2 Mon Sep 17 00:00:00 2001 From: davotoula Date: Tue, 14 Oct 2025 13:31:27 +0200 Subject: [PATCH 19/22] bugfix: don't remove from public bookmarks when removing from private --- .../quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt index 0459ad5f0..1118b2135 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt @@ -134,7 +134,7 @@ class BookmarkListEvent( val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() resign( privateTags = privateTags.remove(bookmarkIdTag.toTagIdOnly()), - tags = earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()), + tags = earlierVersion.tags, signer = signer, createdAt = createdAt, ) From 2ebdedafaa518a874e5311ddab7a4ae0258c7e15 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 14 Oct 2025 13:47:57 +0000 Subject: [PATCH 20/22] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 2 +- .../src/main/res/values-de-rDE/strings.xml | 2 +- .../src/main/res/values-es-rES/strings.xml | 28 +++++++++++++++++++ .../src/main/res/values-es-rMX/strings.xml | 28 +++++++++++++++++++ .../src/main/res/values-es-rUS/strings.xml | 28 +++++++++++++++++++ .../src/main/res/values-hi-rIN/strings.xml | 4 ++- .../src/main/res/values-hu-rHU/strings.xml | 2 +- .../src/main/res/values-pl-rPL/strings.xml | 2 +- .../src/main/res/values-pt-rBR/strings.xml | 2 +- .../src/main/res/values-sl-rSI/strings.xml | 8 ++++++ .../src/main/res/values-sv-rSE/strings.xml | 2 +- .../src/main/res/values-zh-rCN/strings.xml | 2 +- 12 files changed, 102 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 7f78eb705..1a10707a2 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -454,7 +454,7 @@ 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 není v tomto seznamu" + %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é. Došlo k problému při načítání: %1$s diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 08a8ba61b..eb017d185 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -460,7 +460,7 @@ anz der Bedingungen ist erforderlich 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 nicht in dieser 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. Beim Abrufen ist ein Problem aufgetreten: %1$s diff --git a/amethyst/src/main/res/values-es-rES/strings.xml b/amethyst/src/main/res/values-es-rES/strings.xml index 78a43c082..205e5287f 100644 --- a/amethyst/src/main/res/values-es-rES/strings.xml +++ b/amethyst/src/main/res/values-es-rES/strings.xml @@ -112,6 +112,7 @@ Publicar Guardar Crear + Cambiar nombre Cancelar No se pudo cargar la imagen Dirección del relé @@ -445,6 +446,28 @@ A mi alrededor Global Lista de silenciados + Conjuntos de seguimiento + Marcadores etiquetados + Marcadores generales + Público + Privado + Mixto + Al parecer, aún no tienes ningún conjunto de seguimiento. + \nPulsa abajo para actualizar o pulsa los botones de añadir para crear uno nuevo. + + Añadir autor a conjunto de seguimiento + Añadir o eliminar usuario de las listas, o crear una nueva lista con este usuario. + %1$s no se encuentra en esta lista + Tus conjuntos de seguimiento + No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Pulsa abajo para actualizar o utiliza el menú para crear uno. + Ha habido un problema al recuperar: %1$s + Crear nueva lista + Nombre del conjunto + Descripción del conjunto (opcional) + Crear conjunto + Cambiar nombre del conjunto + Estás cambiando el nombre de + a.. El puerto predeterminado es 9050 ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -832,6 +855,8 @@ Media Alta Sin comprimir + Usa H.265/HEVC Codec + Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265. Editar borrador Iniciar sesión con código QR Ruta @@ -1014,6 +1039,8 @@ Descargar Error al abrir el archivo No hay aplicaciones torrent instaladas para abrir y descargar el archivo. + El evento no tiene suficiente información para crear un enlace magnético + Mis listas/conjuntos Selecciona una lista para filtrar el tablón Cerrar sesión al bloquear dispositivo Mensaje privado @@ -1035,4 +1062,5 @@ ¿Te gustaría enviar el informe de error reciente a Amethyst en un mensaje directo? No se compartirá información personal Enviarlo Este mensaje desaparecerá en %1$d días + Seleccionar firmante diff --git a/amethyst/src/main/res/values-es-rMX/strings.xml b/amethyst/src/main/res/values-es-rMX/strings.xml index 7a07b168f..64f8bcab9 100644 --- a/amethyst/src/main/res/values-es-rMX/strings.xml +++ b/amethyst/src/main/res/values-es-rMX/strings.xml @@ -111,6 +111,7 @@ Publicar Guardar Crear + Cambiar nombre Cancelar Error al subir la imagen Dirección del relé @@ -440,6 +441,28 @@ A mi alrededor Global Lista de silenciados + Conjuntos de seguimiento + Marcadores etiquetados + Marcadores generales + Público + Privado + Mixto + Al parecer, aún no tienes ningún conjunto de seguimiento. + \nToca abajo para actualizar o toca los botones de agregar para crear uno nuevo. + + Agregar autor a conjunto de seguimiento + Agregar o quitar usuario de las listas, o crear una nueva lista con este usuario. + %1$s no se encuentra en esta lista + Tus conjuntos de seguimiento + No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Toca abajo para actualizar o utiliza el menú para crear uno. + Hubo un problema al recuperar: %1$s + Crear nueva lista + Nombre del conjunto + Descripción del conjunto (opcional) + Crear conjunto + Cambiar nombre del conjunto + Estás cambiando el nombre de + a.. El puerto predeterminado es 9050 ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -822,6 +845,8 @@ Media Alta Sin comprimir + Usa H.265/HEVC Codec + Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265. Editar borrador Iniciar sesión con código QR Ruta @@ -1001,6 +1026,8 @@ Descargar Error al abrir el archivo No hay aplicaciones torrent instaladas para abrir y descargar el archivo. + El evento no tiene suficiente información para crear un enlace magnético + Mis listas/conjuntos Selecciona una lista para filtrar el feed Cerrar sesión al bloquear dispositivo Mensaje privado @@ -1011,4 +1038,5 @@ Buscar hashtag: #%1$s No traducir del Los idiomas que se muestran aquí no se traducirán. Selecciona un idioma para eliminarlo y traducirlo de nuevo. + Seleccionar firmante diff --git a/amethyst/src/main/res/values-es-rUS/strings.xml b/amethyst/src/main/res/values-es-rUS/strings.xml index d9be7f50f..bb790d8d7 100644 --- a/amethyst/src/main/res/values-es-rUS/strings.xml +++ b/amethyst/src/main/res/values-es-rUS/strings.xml @@ -111,6 +111,7 @@ Publicar Guardar Crear + Cambiar nombre Cancelar Error al subir la imagen Dirección del relé @@ -440,6 +441,28 @@ A mi alrededor Global Lista de silenciados + Conjuntos de seguimiento + Marcadores etiquetados + Marcadores generales + Público + Privado + Mixto + Al parecer, aún no tienes ningún conjunto de seguimiento. + \nToca abajo para actualizar o toca los botones de agregar para crear uno nuevo. + + Agregar autor a conjunto de seguimiento + Agregar o quitar usuario de las listas, o crear una nueva lista con este usuario. + %1$s no se encuentra en esta lista + Tus conjuntos de seguimiento + No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Toca abajo para actualizar o utiliza el menú para crear uno. + Hubo un problema al recuperar: %1$s + Crear nueva lista + Nombre del conjunto + Descripción del conjunto (opcional) + Crear conjunto + Cambiar nombre del conjunto + Estás cambiando el nombre de + a.. El puerto predeterminado es 9050 ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -822,6 +845,8 @@ Media Alta Sin comprimir + Usa H.265/HEVC Codec + Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265. Editar borrador Iniciar sesión con código QR Ruta @@ -1001,6 +1026,8 @@ Descargar Error al abrir el archivo No hay aplicaciones torrent instaladas para abrir y descargar el archivo. + El evento no tiene suficiente información para crear un enlace magnético + Mis listas/conjuntos Selecciona una lista para filtrar el feed Cerrar sesión al bloquear dispositivo Mensaje privado @@ -1011,4 +1038,5 @@ Buscar hashtag: #%1$s No traducir del Los idiomas que se muestran aquí no se traducirán. Selecciona un idioma para eliminarlo y traducirlo de nuevo. + Seleccionar firmante diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index cf5278fda..9e54ce9e4 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -458,7 +458,7 @@ लेखक जोडें अनुगम्य सूची में प्रयोक्ता को सूचियों में जोडें अथवा हटाएँ अथवा इस प्रयोक्ता के साथ नई सूची बनाएँ। सूची %1$s के लिए चिह्न - "%1$s इस सूची में नहीं है" + %1$s इस सूची में नहीं है आपके अनुगम्य सूचियाँ कोई अनुगम्य सूचियाँ प्राप्त नहीं। अथवा आपका कोई अनुगम्य सूचियाँ हैं नहीं। नवीकरण के लिए नीचे दबाएँ अथवा विकल्पसूची द्वारा एक नया बनाएँ। लाने में अपक्रम : %1$s @@ -859,6 +859,8 @@ मध्यम उच्च असंकुचित + उच्च कार्यक्षमता चलचित्र संकुचनविस्तारण अथवा एच॰२६५ क्रमलेख का प्रयोग करें + अधिक गुणवत्ता के साथ सूक्ष्मतर अभिलेख आकार परन्तु एच॰२६५ चालन सभी यन्त्रों द्वारा अवलम्बित नहीं। पाण्डुलिपि का सम्पादन करें क्यूआर॰ क्रमचित्र के साथ प्रवेशांकन करें मार्ग diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 81e96f8cd..4fb56b773 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -458,7 +458,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 nincs a listában" + 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. Probléma történt a következő lekérdezésekor: %1$s diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index c11590812..408a5ec41 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -455,7 +455,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 nie jest na liście" + %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ć. Podczas pobierania wystąpił błąd: %1$s diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 2c0879035..58293151a 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -454,7 +454,7 @@ 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 não está nesta lista" + %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. Houve um problema ao buscar: %1$s diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml index a5ad70e03..fc189ad67 100644 --- a/amethyst/src/main/res/values-sl-rSI/strings.xml +++ b/amethyst/src/main/res/values-sl-rSI/strings.xml @@ -123,6 +123,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Pošlji Shrani Ustvari + Preimenuj Prekliči Neuspelo nalaganje slike Naslov releja @@ -456,8 +457,13 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem V moji okolici Globalno Spisek utišanih + Označeni zaznamki + Splošni zaznamki + Javno Zasebno Mešano + Dodaj ali odstrani uprabnika iz seznama, ali pa ustvari nov seznam s tem uporabnikom. + Ustvari nov seznam Prevzeta vrata so 9050 ## Poveži se preko Tor omrežja z Orbot aplikacijo \n\n1. Namesti [Orbot aplikacijo](https://play.google.com/store/apps/details?id=org.torproject.android) @@ -845,6 +851,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Srednja Visoka Nestisnjeno + Uporabi H.265/HEVC Codec Uredi osnutek Vpiši se z QR kodo Pot @@ -1049,4 +1056,5 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Želite poslati poročilo o napaki Amethyst-u preko zasebnega sporočila? Vaši osebni podatki NE bojo posredovani. Pošlji To sporočilo bo izginilo čez %1$d dni + Izberi podpisnika diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 32e93ab55..f327d08bb 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -454,7 +454,7 @@ 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 inte i denna 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. Ett problem uppstod vid hämtning: %1$s diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 6cbb7f36c..434f9503b 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -458,7 +458,7 @@ 添加作者到关注集 从列表中添加或删除用户,或用此用户创建一个新列表。 %1$s 列表的图标 - "此列表中没有 %1$s" + 此列表中没有 %1$s 您的关注集 未找到关注集,或者你还没有任何关注集。轻按下方刷新,或使用按钮新建。 获取时出了问题: %1$s From deee1a1c851798c265defeca37814857440c263f Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 14 Oct 2025 13:50:15 +0000 Subject: [PATCH 21/22] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 2 +- .../src/main/res/values-de-rDE/strings.xml | 2 +- .../src/main/res/values-es-rES/strings.xml | 28 +++++++++++++++++++ .../src/main/res/values-es-rMX/strings.xml | 28 +++++++++++++++++++ .../src/main/res/values-es-rUS/strings.xml | 28 +++++++++++++++++++ .../src/main/res/values-hi-rIN/strings.xml | 4 ++- .../src/main/res/values-hu-rHU/strings.xml | 2 +- .../src/main/res/values-pl-rPL/strings.xml | 2 +- .../src/main/res/values-pt-rBR/strings.xml | 2 +- .../src/main/res/values-sl-rSI/strings.xml | 8 ++++++ .../src/main/res/values-sv-rSE/strings.xml | 2 +- .../src/main/res/values-zh-rCN/strings.xml | 2 +- 12 files changed, 102 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 7f78eb705..1a10707a2 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -454,7 +454,7 @@ 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 není v tomto seznamu" + %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é. Došlo k problému při načítání: %1$s diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 08a8ba61b..eb017d185 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -460,7 +460,7 @@ anz der Bedingungen ist erforderlich 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 nicht in dieser 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. Beim Abrufen ist ein Problem aufgetreten: %1$s diff --git a/amethyst/src/main/res/values-es-rES/strings.xml b/amethyst/src/main/res/values-es-rES/strings.xml index 78a43c082..205e5287f 100644 --- a/amethyst/src/main/res/values-es-rES/strings.xml +++ b/amethyst/src/main/res/values-es-rES/strings.xml @@ -112,6 +112,7 @@ Publicar Guardar Crear + Cambiar nombre Cancelar No se pudo cargar la imagen Dirección del relé @@ -445,6 +446,28 @@ A mi alrededor Global Lista de silenciados + Conjuntos de seguimiento + Marcadores etiquetados + Marcadores generales + Público + Privado + Mixto + Al parecer, aún no tienes ningún conjunto de seguimiento. + \nPulsa abajo para actualizar o pulsa los botones de añadir para crear uno nuevo. + + Añadir autor a conjunto de seguimiento + Añadir o eliminar usuario de las listas, o crear una nueva lista con este usuario. + %1$s no se encuentra en esta lista + Tus conjuntos de seguimiento + No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Pulsa abajo para actualizar o utiliza el menú para crear uno. + Ha habido un problema al recuperar: %1$s + Crear nueva lista + Nombre del conjunto + Descripción del conjunto (opcional) + Crear conjunto + Cambiar nombre del conjunto + Estás cambiando el nombre de + a.. El puerto predeterminado es 9050 ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -832,6 +855,8 @@ Media Alta Sin comprimir + Usa H.265/HEVC Codec + Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265. Editar borrador Iniciar sesión con código QR Ruta @@ -1014,6 +1039,8 @@ Descargar Error al abrir el archivo No hay aplicaciones torrent instaladas para abrir y descargar el archivo. + El evento no tiene suficiente información para crear un enlace magnético + Mis listas/conjuntos Selecciona una lista para filtrar el tablón Cerrar sesión al bloquear dispositivo Mensaje privado @@ -1035,4 +1062,5 @@ ¿Te gustaría enviar el informe de error reciente a Amethyst en un mensaje directo? No se compartirá información personal Enviarlo Este mensaje desaparecerá en %1$d días + Seleccionar firmante diff --git a/amethyst/src/main/res/values-es-rMX/strings.xml b/amethyst/src/main/res/values-es-rMX/strings.xml index 7a07b168f..64f8bcab9 100644 --- a/amethyst/src/main/res/values-es-rMX/strings.xml +++ b/amethyst/src/main/res/values-es-rMX/strings.xml @@ -111,6 +111,7 @@ Publicar Guardar Crear + Cambiar nombre Cancelar Error al subir la imagen Dirección del relé @@ -440,6 +441,28 @@ A mi alrededor Global Lista de silenciados + Conjuntos de seguimiento + Marcadores etiquetados + Marcadores generales + Público + Privado + Mixto + Al parecer, aún no tienes ningún conjunto de seguimiento. + \nToca abajo para actualizar o toca los botones de agregar para crear uno nuevo. + + Agregar autor a conjunto de seguimiento + Agregar o quitar usuario de las listas, o crear una nueva lista con este usuario. + %1$s no se encuentra en esta lista + Tus conjuntos de seguimiento + No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Toca abajo para actualizar o utiliza el menú para crear uno. + Hubo un problema al recuperar: %1$s + Crear nueva lista + Nombre del conjunto + Descripción del conjunto (opcional) + Crear conjunto + Cambiar nombre del conjunto + Estás cambiando el nombre de + a.. El puerto predeterminado es 9050 ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -822,6 +845,8 @@ Media Alta Sin comprimir + Usa H.265/HEVC Codec + Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265. Editar borrador Iniciar sesión con código QR Ruta @@ -1001,6 +1026,8 @@ Descargar Error al abrir el archivo No hay aplicaciones torrent instaladas para abrir y descargar el archivo. + El evento no tiene suficiente información para crear un enlace magnético + Mis listas/conjuntos Selecciona una lista para filtrar el feed Cerrar sesión al bloquear dispositivo Mensaje privado @@ -1011,4 +1038,5 @@ Buscar hashtag: #%1$s No traducir del Los idiomas que se muestran aquí no se traducirán. Selecciona un idioma para eliminarlo y traducirlo de nuevo. + Seleccionar firmante diff --git a/amethyst/src/main/res/values-es-rUS/strings.xml b/amethyst/src/main/res/values-es-rUS/strings.xml index d9be7f50f..bb790d8d7 100644 --- a/amethyst/src/main/res/values-es-rUS/strings.xml +++ b/amethyst/src/main/res/values-es-rUS/strings.xml @@ -111,6 +111,7 @@ Publicar Guardar Crear + Cambiar nombre Cancelar Error al subir la imagen Dirección del relé @@ -440,6 +441,28 @@ A mi alrededor Global Lista de silenciados + Conjuntos de seguimiento + Marcadores etiquetados + Marcadores generales + Público + Privado + Mixto + Al parecer, aún no tienes ningún conjunto de seguimiento. + \nToca abajo para actualizar o toca los botones de agregar para crear uno nuevo. + + Agregar autor a conjunto de seguimiento + Agregar o quitar usuario de las listas, o crear una nueva lista con este usuario. + %1$s no se encuentra en esta lista + Tus conjuntos de seguimiento + No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Toca abajo para actualizar o utiliza el menú para crear uno. + Hubo un problema al recuperar: %1$s + Crear nueva lista + Nombre del conjunto + Descripción del conjunto (opcional) + Crear conjunto + Cambiar nombre del conjunto + Estás cambiando el nombre de + a.. El puerto predeterminado es 9050 ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -822,6 +845,8 @@ Media Alta Sin comprimir + Usa H.265/HEVC Codec + Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265. Editar borrador Iniciar sesión con código QR Ruta @@ -1001,6 +1026,8 @@ Descargar Error al abrir el archivo No hay aplicaciones torrent instaladas para abrir y descargar el archivo. + El evento no tiene suficiente información para crear un enlace magnético + Mis listas/conjuntos Selecciona una lista para filtrar el feed Cerrar sesión al bloquear dispositivo Mensaje privado @@ -1011,4 +1038,5 @@ Buscar hashtag: #%1$s No traducir del Los idiomas que se muestran aquí no se traducirán. Selecciona un idioma para eliminarlo y traducirlo de nuevo. + Seleccionar firmante diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index cf5278fda..9e54ce9e4 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -458,7 +458,7 @@ लेखक जोडें अनुगम्य सूची में प्रयोक्ता को सूचियों में जोडें अथवा हटाएँ अथवा इस प्रयोक्ता के साथ नई सूची बनाएँ। सूची %1$s के लिए चिह्न - "%1$s इस सूची में नहीं है" + %1$s इस सूची में नहीं है आपके अनुगम्य सूचियाँ कोई अनुगम्य सूचियाँ प्राप्त नहीं। अथवा आपका कोई अनुगम्य सूचियाँ हैं नहीं। नवीकरण के लिए नीचे दबाएँ अथवा विकल्पसूची द्वारा एक नया बनाएँ। लाने में अपक्रम : %1$s @@ -859,6 +859,8 @@ मध्यम उच्च असंकुचित + उच्च कार्यक्षमता चलचित्र संकुचनविस्तारण अथवा एच॰२६५ क्रमलेख का प्रयोग करें + अधिक गुणवत्ता के साथ सूक्ष्मतर अभिलेख आकार परन्तु एच॰२६५ चालन सभी यन्त्रों द्वारा अवलम्बित नहीं। पाण्डुलिपि का सम्पादन करें क्यूआर॰ क्रमचित्र के साथ प्रवेशांकन करें मार्ग diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 81e96f8cd..4fb56b773 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -458,7 +458,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 nincs a listában" + 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. Probléma történt a következő lekérdezésekor: %1$s diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index c11590812..408a5ec41 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -455,7 +455,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 nie jest na liście" + %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ć. Podczas pobierania wystąpił błąd: %1$s diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 2c0879035..58293151a 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -454,7 +454,7 @@ 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 não está nesta lista" + %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. Houve um problema ao buscar: %1$s diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml index a5ad70e03..fc189ad67 100644 --- a/amethyst/src/main/res/values-sl-rSI/strings.xml +++ b/amethyst/src/main/res/values-sl-rSI/strings.xml @@ -123,6 +123,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Pošlji Shrani Ustvari + Preimenuj Prekliči Neuspelo nalaganje slike Naslov releja @@ -456,8 +457,13 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem V moji okolici Globalno Spisek utišanih + Označeni zaznamki + Splošni zaznamki + Javno Zasebno Mešano + Dodaj ali odstrani uprabnika iz seznama, ali pa ustvari nov seznam s tem uporabnikom. + Ustvari nov seznam Prevzeta vrata so 9050 ## Poveži se preko Tor omrežja z Orbot aplikacijo \n\n1. Namesti [Orbot aplikacijo](https://play.google.com/store/apps/details?id=org.torproject.android) @@ -845,6 +851,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Srednja Visoka Nestisnjeno + Uporabi H.265/HEVC Codec Uredi osnutek Vpiši se z QR kodo Pot @@ -1049,4 +1056,5 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Želite poslati poročilo o napaki Amethyst-u preko zasebnega sporočila? Vaši osebni podatki NE bojo posredovani. Pošlji To sporočilo bo izginilo čez %1$d dni + Izberi podpisnika diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 32e93ab55..f327d08bb 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -454,7 +454,7 @@ 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 inte i denna 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. Ett problem uppstod vid hämtning: %1$s diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 6cbb7f36c..434f9503b 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -458,7 +458,7 @@ 添加作者到关注集 从列表中添加或删除用户,或用此用户创建一个新列表。 %1$s 列表的图标 - "此列表中没有 %1$s" + 此列表中没有 %1$s 您的关注集 未找到关注集,或者你还没有任何关注集。轻按下方刷新,或使用按钮新建。 获取时出了问题: %1$s From 49f77af3e8ddbe170aabb936f8912afd3cee5e3e Mon Sep 17 00:00:00 2001 From: davotoula Date: Tue, 14 Oct 2025 18:59:54 +0200 Subject: [PATCH 22/22] Updated cs, se, de, pt --- .../src/main/res/values-cs-rCZ/strings.xml | 20 +++++++++++++++++++ .../src/main/res/values-de-rDE/strings.xml | 20 +++++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 20 +++++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 20 +++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 1a10707a2..472c4e505 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1063,4 +1063,24 @@ Odeslat Tato zpráva zmizí za %1$d dní Vybrat podepisovatele + + Nový + %1$s je veřejný člen + %1$s je soukromý člen + Přidat jako veřejného člena + Přidat jako soukromého člena + Veřejné profily + Soukromé profily + člen + členové + Žádní členové + Kopírovat/Klonovat sadu sledování + Upravit popis + Tento seznam nemá žádný popis + Aktuální popis: + Níže můžete nastavit vlastní název/popisek pro tuto klonovanou sadu. + (Původní název sady) + (Původní popis sady) + Kopírovat/Klonovat sadu + Upravit diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index eb017d185..1f0aad6a7 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1068,4 +1068,24 @@ anz der Bedingungen ist erforderlich Senden Diese Nachricht verschwindet in %1$d Tagen Signierer auswählen + + Neu + %1$s ist ein öffentliches Mitglied + %1$s ist ein privates Mitglied + Als öffentliches Mitglied hinzufügen + Als privates Mitglied hinzufügen + Öffentliche Profile + Private Profile + Mitglied + Mitglieder + Keine Mitglieder + Follow-Set kopieren/klonen + Beschreibung bearbeiten + Diese Liste hat keine Beschreibung + Aktuelle Beschreibung: + Unten kannst du einen eigenen Namen/Beschreibung für diesen Klon festlegen. + (Original-Set-Name) + (Original-Set-Beschreibung) + Set kopieren/klonen + Bearbeiten diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 58293151a..2c716eb7a 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1063,4 +1063,24 @@ Enviar Esta mensagem desaparecerá em %1$d dias Selecionar assinador + + Novo + %1$s é um membro público + %1$s é um membro privado + Adicionar como membro público + Adicionar como membro privado + Perfis públicos + Perfis privados + membro + membros + Nenhum membro + Copiar/Clonar conjunto de seguidores + Modificar descrição + Esta lista não tem descrição + Descrição atual: + Você pode definir um nome/descrição personalizada para este conjunto clonado abaixo. + (Nome original do conjunto) + (Descrição original do conjunto) + Copiar/Clonar conjunto + Modificar diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index f327d08bb..c79d279f2 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1062,4 +1062,24 @@ Skicka Detta meddelande försvinner om %1$d dagar Välj signatör + Ny + %1$s är en offentlig medlem + %1$s är en privat medlem + Lägg till som offentlig medlem + Lägg till som privat medlem + Offentliga profiler + Privata profiler + medlem + medlemmar + Inga medlemmar + Kopiera/Klona följlista + Ändra beskrivning + Den här listan har ingen beskrivning + Nuvarande beskrivning: + Du kan ange ett anpassat namn/beskrivning för denna klonlista nedan. + (Ursprungligt listnamn) + (Ursprunglig beskrivning) + Kopiera/Klona lista + Ändra +