Merge branch 'main' into bugfix-dont-render-gallery-for-mixed-images-and-videos

This commit is contained in:
Vitor Pamplona
2025-10-14 09:48:58 -04:00
committed by GitHub
25 changed files with 738 additions and 316 deletions

View File

@@ -151,6 +151,7 @@ android {
signingConfig = signingConfigs.debug
}
}
// TODO: remove this when lightcompressor uses one MP4 parser only
packaging {
resources {

View File

@@ -31,9 +31,10 @@ data class FollowSet(
val identifierTag: String,
val title: String,
val description: String?,
val visibility: SetVisibility,
val profiles: Set<String>,
) : NostrSet(setVisibility = visibility, content = profiles) {
val visibility: SetVisibility = SetVisibility.Mixed,
val privateProfiles: Set<String> = emptySet(),
val publicProfiles: Set<String> = emptySet(),
) : NostrSet(setVisibility = visibility, privateContent = privateProfiles, publicContent = publicProfiles) {
companion object {
fun mapEventToSet(
event: PeopleListEvent,
@@ -53,26 +54,22 @@ data class FollowSet(
identifierTag = dTag,
title = listTitle,
description = listDescription,
visibility = SetVisibility.Private,
profiles = privateFollows.toSet(),
privateProfiles = privateFollows.toSet(),
)
} else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) {
FollowSet(
identifierTag = dTag,
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(),
)
}
}

View File

@@ -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,12 +59,12 @@ class FollowSetState(
emit(followSets)
delay(2000)
}
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
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 =
@@ -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 {}
}
}
}

View File

@@ -22,11 +22,13 @@ package com.vitorpamplona.amethyst.model.nip51Lists.followSets
sealed class NostrSet(
val setVisibility: SetVisibility,
val content: Collection<String>,
val privateContent: Collection<String>,
val publicContent: Collection<String>,
)
class CuratedBookmarkSet(
val name: String,
val visibility: SetVisibility,
val setItems: List<String>,
) : NostrSet(visibility, setItems)
val privateSetItems: List<String>,
val publicSetItems: List<String>,
) : NostrSet(visibility, privateSetItems, publicSetItems)

View File

@@ -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),
)
}
}

View File

@@ -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,
)
},
@@ -138,6 +137,21 @@ 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,
customCloneName = customName,
customCloneDescription = customDescription,
account = accountViewModel.account,
)
},
deleteItem = { followSet ->
followSetsViewModel.deleteFollowSet(
followSet = followSet,
@@ -153,14 +167,16 @@ 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,
changeItemDescription: (followSet: FollowSet, newDescription: String?) -> Unit,
cloneItem: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit,
deleteItem: (followSet: FollowSet) -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {
val pagerState = rememberPagerState { 3 }
val pagerState = rememberPagerState { 2 }
val coroutineScope = rememberCoroutineScope()
DisappearingScaffold(
@@ -185,22 +201,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)
},
)
},
@@ -222,11 +230,13 @@ fun CustomListsScreen(
onRefresh = refresh,
onOpenItem = openItem,
onRenameItem = renameItem,
onItemDescriptionChange = changeItemDescription,
onItemClone = cloneItem,
onDeleteItem = deleteItem,
)
1 -> LabeledBookmarksFeedView()
2 -> GeneralBookmarksFeedView()
// 2 -> GeneralBookmarksFeedView()
}
}
}
@@ -236,57 +246,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
ExtendedFloatingActionButton(
text = {
Text(text = stringRes(R.string.follow_set_create_btn_label))
},
shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primary,
) {
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,
)
}
}
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 +283,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<String?>(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 +295,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 +313,7 @@ fun NewSetCreationDialog(
},
)
Spacer(modifier = DoubleVertSpacer)
// For the list description
// For the set description
TextField(
value =
(
@@ -425,6 +391,12 @@ 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")
},
onFollowSetDelete = {
println(" The follow set ${sampleFollowSet.title} has been deleted.")
},

View File

@@ -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,15 +53,14 @@ 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
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
import kotlin.let
@Composable
fun CustomSetItem(
@@ -72,6 +68,8 @@ fun CustomSetItem(
followSet: FollowSet,
onFollowSetClick: () -> Unit,
onFollowSetRename: (String) -> Unit,
onFollowSetDescriptionChange: (String?) -> Unit,
onFollowSetClone: (customName: String?, customDescription: String?) -> Unit,
onFollowSetDelete: () -> Unit,
) {
val context = LocalContext.current
@@ -96,11 +94,12 @@ fun CustomSetItem(
) {
Text(followSet.title, fontWeight = FontWeight.Bold)
Spacer(modifier = StdHorzSpacer)
if (followSet.publicProfiles.isEmpty() && followSet.privateProfiles.isEmpty()) {
FilterChip(
selected = true,
onClick = {},
label = {
Text(text = "${followSet.profiles.size}")
Text(text = stringRes(R.string.follow_set_empty_label))
},
leadingIcon = {
Icon(
@@ -111,6 +110,60 @@ fun CustomSetItem(
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
},
)
FilterChip(
selected = true,
onClick = {},
label = {
Text(text = "$publicMemberSize")
},
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")
},
leadingIcon = {
Icon(
painterResource(R.drawable.lock),
contentDescription = null,
)
},
shape = ButtonBorder,
)
}
}
Spacer(modifier = StdVertSpacer)
Text(
followSet.description ?: "",
@@ -119,34 +172,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(
@@ -159,7 +184,10 @@ fun CustomSetItem(
) {
SetOptionsButton(
followSetName = followSet.title,
onListRename = onFollowSetRename,
followSetDescription = followSet.description,
onSetRename = onFollowSetRename,
onSetDescriptionChange = onFollowSetDescriptionChange,
onSetCloneCreate = onFollowSetClone,
onListDelete = onFollowSetDelete,
)
}
@@ -167,10 +195,13 @@ fun CustomSetItem(
}
@Composable
fun SetOptionsButton(
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,
) {
val isMenuOpen = remember { mutableStateOf(false) }
@@ -181,10 +212,13 @@ 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,
)
}
@@ -194,26 +228,27 @@ 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,
) {
val isRenameDialogOpen = remember { mutableStateOf(false) }
val renameString = remember { mutableStateOf("") }
val isDescriptionModDialogOpen = remember { mutableStateOf(false) }
val isCopyDialogOpen = remember { mutableStateOf(false) }
val optionalCloneName = remember { mutableStateOf<String?>(null) }
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
DropdownMenu(
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))
@@ -223,25 +258,76 @@ private fun SetOptionsMenu(
onDismiss()
},
)
DropdownMenuItem(
text = {
Text(text = stringRes(R.string.follow_set_desc_modify_label))
},
onClick = {
isDescriptionModDialogOpen.value = true
onDismiss()
},
)
DropdownMenuItem(
text = {
Text(text = stringRes(R.string.follow_set_copy_action_btn_label))
},
onClick = {
isCopyDialogOpen.value = true
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,
optionalNewDesc = optionalCloneDescription.value,
onCloneNameChange = {
optionalCloneName.value = it
},
onCloneDescChange = {
optionalCloneDescription.value = it
},
onCloneCreate = { name, description ->
onSetClone(optionalCloneName.value, optionalCloneDescription.value)
},
onDismiss = { isCopyDialogOpen.value = false },
)
}
}
@Composable
private fun RenameDialog(
private fun SetRenameDialog(
modifier: Modifier = Modifier,
currentName: String,
newName: String,
@@ -299,3 +385,137 @@ private fun RenameDialog(
},
)
}
@Composable
private fun SetModifyDescriptionDialog(
modifier: Modifier = Modifier,
currentDescription: String?,
onDismissDialog: () -> Unit,
onModifyDescription: (String?) -> Unit,
) {
val updatedDescription = remember { mutableStateOf<String?>(null) }
val modifyIndicatorLabel =
if (currentDescription == null) {
stringRes(R.string.follow_set_empty_desc_label)
} else {
buildAnnotatedString {
append(stringRes(R.string.follow_set_current_desc_label) + " ")
withStyle(
SpanStyle(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Normal,
fontSize = 15.sp,
),
) {
append("\"" + currentDescription + "\"")
}
}.text
}
AlertDialog(
onDismissRequest = onDismissDialog,
title = {
Text(text = stringRes(R.string.follow_set_desc_modify_label))
},
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 = stringRes(R.string.follow_set_desc_modify_btn_label)) }
},
dismissButton = {
Button(onClick = onDismissDialog) { Text(text = stringRes(R.string.cancel)) }
},
)
}
@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))
}
},
)
}

View File

@@ -51,6 +51,8 @@ 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,
) {
when (followSetFeedState) {
@@ -63,6 +65,8 @@ fun FollowSetFeedView(
onRefresh = onRefresh,
onItemClick = onOpenItem,
onItemRename = onRenameItem,
onItemDescriptionChange = onItemDescriptionChange,
onItemClone = onItemClone,
onItemDelete = onDeleteItem,
)
}
@@ -91,6 +95,8 @@ 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,
) {
Log.d("FollowSetComposable", "FollowSetLoaded: Follow Set size: ${loadedFeedState.size}")
@@ -113,6 +119,12 @@ fun FollowSetLoaded(
onFollowSetRename = {
onItemRename(set, it)
},
onFollowSetDescriptionChange = { newDescription ->
onItemDescriptionChange(set, newDescription)
},
onFollowSetClone = { cloneName, cloneDescription ->
onItemClone(set, cloneName, cloneDescription)
},
onFollowSetDelete = {
onItemDelete(set)
},

View File

@@ -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,8 +128,9 @@ class FollowSetFeedViewModel(
dTag = UUID.randomUUID().toString(),
title = setName,
description = setDescription,
isPrivate = isListPrivate,
firstMemberHex = optionalFirstMemberHex,
isPrivate = firstMemberShouldBePrivate,
firstPublicMembers = if (optionalFirstMemberHex != null) listOf(optionalFirstMemberHex) else emptyList(),
firstPrivateMembers = if (optionalFirstMemberHex != null) listOf(optionalFirstMemberHex) else emptyList(),
signer = account.signer,
) {
account.sendMyPublicAndPrivateOutbox(it)
@@ -160,6 +160,51 @@ 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?,
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.copy(
dTag = UUID.randomUUID().toString(),
title = customCloneName ?: currentFollowSet.title,
description = customCloneDescription ?: currentFollowSet.description,
firstPublicMembers = currentFollowSet.publicProfiles.toList(),
firstPrivateMembers = currentFollowSet.privateProfiles.toList(),
signer = account.signer,
) {
account.sendMyPublicAndPrivateOutbox(it)
}
}
}
}
fun deleteFollowSet(
followSet: FollowSet,
account: Account,
@@ -179,6 +224,7 @@ class FollowSetFeedViewModel(
fun addUserToSet(
userProfileHex: String,
followSet: FollowSet,
shouldBePrivateMember: Boolean,
account: Account,
) {
if (!account.settings.isWriteable()) {
@@ -190,7 +236,7 @@ class FollowSetFeedViewModel(
PeopleListEvent.addUser(
earlierVersion = followSetEvent,
pubKeyHex = userProfileHex,
isPrivate = followSet.visibility == SetVisibility.Private,
isPrivate = shouldBePrivateMember,
signer = account.signer,
) {
account.sendMyPublicAndPrivateOutbox(it)
@@ -201,6 +247,7 @@ class FollowSetFeedViewModel(
fun removeUserFromSet(
userProfileHex: String,
userIsPrivate: Boolean,
followSet: FollowSet,
account: Account,
) {
@@ -213,6 +260,7 @@ class FollowSetFeedViewModel(
PeopleListEvent.removeUser(
earlierVersion = followSetEvent,
pubKeyHex = userProfileHex,
isUserPrivate = userIsPrivate,
signer = account.signer,
) {
account.sendMyPublicAndPrivateOutbox(it)

View File

@@ -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
@@ -76,11 +77,13 @@ 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
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 +147,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 +175,7 @@ fun FollowSetScreen(
selectedSet,
accountViewModel.account,
)
navigator.popBack()
},
)
},
@@ -190,10 +195,12 @@ fun FollowSetScreen(
bottom = padding.calculateBottomPadding(),
).consumeWindowInsets(padding)
.imePadding(),
followSetList = users,
publicMemberList = publicMembers,
privateMemberList = privateMembers,
onDeleteUser = {
followSetViewModel.removeUserFromSet(
it,
userIsPrivate = selectedSet.privateProfiles.contains(it),
selectedSet,
accountViewModel.account,
)
@@ -233,14 +240,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 +260,8 @@ fun TitleAndDescription(
@Composable
private fun FollowSetListView(
modifier: Modifier = Modifier,
followSetList: List<User>,
publicMemberList: List<User>,
privateMemberList: List<User>,
onDeleteUser: (String) -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
@@ -272,7 +273,24 @@ private fun FollowSetListView(
contentPadding = FeedPadding,
state = listState,
) {
itemsIndexed(followSetList, key = { _, item -> item.pubkeyHex }) { _, item ->
if (publicMemberList.isNotEmpty()) {
stickyHeader {
Column(
modifier = VertPadding,
) {
Text(
text = stringRes(R.string.follow_set_public_members_header_label),
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,
@@ -281,6 +299,36 @@ private fun FollowSetListView(
onDeleteUser = onDeleteUser,
)
}
item {
Spacer(modifier = Modifier.height(30.dp))
}
}
if (privateMemberList.isNotEmpty()) {
stickyHeader {
Text(
text = stringRes(R.string.follow_set_private_members_header_label),
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))
}
}
}
}

View File

@@ -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,31 @@ fun FollowSetsManagementDialog(
)
followSetsViewModel.removeUserFromSet(
userHex,
list,
userIsPrivate = set.privateProfiles.contains(userHex),
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 +235,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 +319,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,27 +345,11 @@ 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),
painter = painterResource(R.drawable.format_list_bulleted_type),
contentDescription = stringRes(R.string.follow_set_icon_description),
)
}
}
Spacer(modifier = StdVertSpacer)
Row {
@@ -362,7 +361,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 +392,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 +431,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 = stringRes(R.string.follow_set_public_member_add_label))
},
onClick = {
onUserAdd(false)
onDismiss()
},
)
DropdownMenuItem(
text = {
Text(text = stringRes(R.string.follow_set_private_member_add_label))
},
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 +522,6 @@ fun FollowSetsCreationMenu(
isListAdditionDialogOpen.value = false
isPrivateOptionTapped.value = false
},
shouldBePrivate = isPrivateOptionTapped.value,
onCreateList = { name, description ->
onSetCreate(name, isPrivateOptionTapped.value, description)
},
@@ -528,7 +575,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,

View File

@@ -453,8 +453,7 @@
<string name="follow_set_empty_feed_msg">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é.</string>
<string name="follow_set_add_author_from_note_action">Přidat autora do sady sledování</string>
<string name="follow_set_profile_actions_menu_description">Přidat nebo odebrat uživatele ze seznamů, nebo vytvořit nový seznam s tímto uživatelem.</string>
<string name="follow_set_type_description">Ikona pro seznam %1$s</string>
<string name="follow_set_presence_indicator">"%1$s je v tomto seznamu"</string>
<string name="follow_set_icon_description">Ikona pro seznam %1$s</string>
<string name="follow_set_absence_indicator">"%1$s není v tomto seznamu"</string>
<string name="follow_set_man_dialog_title">Vaše sady sledování</string>
<string name="follow_set_empty_dialog_msg">Nebyly nalezeny žádné sady sledování, nebo žádné nemáte. Klepněte níže pro obnovení nebo použijte menu pro vytvoření nové.</string>

View File

@@ -961,8 +961,7 @@
<string name="follow_set_empty_feed_msg">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é.</string>
<string name="follow_set_add_author_from_note_action">Přidat autora do sady sledování</string>
<string name="follow_set_profile_actions_menu_description">Přidat nebo odebrat uživatele ze seznamů, nebo vytvořit nový seznam s tímto uživatelem.</string>
<string name="follow_set_type_description">Ikona pro seznam %1$s</string>
<string name="follow_set_presence_indicator">%1$s je v tomto seznamu</string>
<string name="follow_set_icon_description">Ikona pro seznam %1$s</string>
<string name="follow_set_absence_indicator">%1$s není v tomto seznamu</string>
<string name="follow_set_man_dialog_title">Vaše sady sledování</string>
<string name="follow_set_empty_dialog_msg">Nebyly nalezeny žádné sady sledování, nebo žádné nemáte. Klepněte níže pro obnovení nebo použijte menu pro vytvoření nové.</string>

View File

@@ -459,8 +459,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="follow_set_empty_feed_msg">Es scheint, dass du noch keine Folge-Sets hast.\nTippe unten zum Aktualisieren oder verwende die Plus-Taste, um ein neues zu erstellen.</string>
<string name="follow_set_add_author_from_note_action">Autor zum Folge-Set hinzufügen</string>
<string name="follow_set_profile_actions_menu_description">Benutzer zu Listen hinzufügen oder entfernen, oder eine neue Liste mit diesem Benutzer erstellen.</string>
<string name="follow_set_type_description">Symbol für %1$s-Liste</string>
<string name="follow_set_presence_indicator">"%1$s ist in dieser Liste"</string>
<string name="follow_set_icon_description">Symbol für %1$s-Liste</string>
<string name="follow_set_absence_indicator">"%1$s ist nicht in dieser Liste"</string>
<string name="follow_set_man_dialog_title">Deine Folge-Sets</string>
<string name="follow_set_empty_dialog_msg">Keine Folge-Sets gefunden oder du hast keine. Tippe unten zum Aktualisieren oder verwende das Menü, um eines zu erstellen.</string>

View File

@@ -1001,8 +1001,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="follow_set_empty_feed_msg">Es scheint, dass du noch keine Folge-Sets hast.\nTippe unten zum Aktualisieren oder verwende die Plus-Taste, um ein neues zu erstellen.</string>
<string name="follow_set_add_author_from_note_action">Autor zum Folge-Set hinzufügen</string>
<string name="follow_set_profile_actions_menu_description">Benutzer zu Listen hinzufügen oder entfernen, oder eine neue Liste mit diesem Benutzer erstellen.</string>
<string name="follow_set_type_description">Symbol für %1$s-Liste</string>
<string name="follow_set_presence_indicator">%1$s ist in dieser Liste</string>
<string name="follow_set_icon_description">Symbol für %1$s-Liste</string>
<string name="follow_set_absence_indicator">%1$s ist nicht in dieser Liste</string>
<string name="follow_set_man_dialog_title">Deine Folge-Sets</string>
<string name="follow_set_empty_dialog_msg">Keine Folge-Sets gefunden oder du hast keine. Tippe unten zum Aktualisieren oder verwende das Menü, um eines zu erstellen.</string>

View File

@@ -457,8 +457,7 @@
</string>
<string name="follow_set_add_author_from_note_action">लेखक जोडें अनुगम्य सूची में</string>
<string name="follow_set_profile_actions_menu_description">प्रयोक्ता को सूचियों में जोडें अथवा हटाएँ अथवा इस प्रयोक्ता के साथ नई सूची बनाएँ।</string>
<string name="follow_set_type_description">सूची %1$s के लिए चिह्न</string>
<string name="follow_set_presence_indicator">"%1$s इस सूची में है"</string>
<string name="follow_set_icon_description">सूची %1$s के लिए चिह्न</string>
<string name="follow_set_absence_indicator">"%1$s इस सूची में नहीं है"</string>
<string name="follow_set_man_dialog_title">आपके अनुगम्य सूचियाँ</string>
<string name="follow_set_empty_dialog_msg">कोई अनुगम्य सूचियाँ प्राप्त नहीं। अथवा आपका कोई अनुगम्य सूचियाँ हैं नहीं। नवीकरण के लिए नीचे दबाएँ अथवा विकल्पसूची द्वारा एक नया बनाएँ।</string>

View File

@@ -457,8 +457,7 @@
</string>
<string name="follow_set_add_author_from_note_action">Szerző hozzáadása a követési gyűjteményhez</string>
<string name="follow_set_profile_actions_menu_description">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.</string>
<string name="follow_set_type_description">Ikon a(z) %1$s nevű listához</string>
<string name="follow_set_presence_indicator">"A(z) %1$s már a létezik a listában"</string>
<string name="follow_set_icon_description">Ikon a(z) %1$s nevű listához</string>
<string name="follow_set_absence_indicator">"A(z) %1$s nincs a listában"</string>
<string name="follow_set_man_dialog_title">Saját követési gyüjtemények</string>
<string name="follow_set_empty_dialog_msg">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.</string>

View File

@@ -114,7 +114,7 @@
<string name="yes"></string>
<string name="no"></string>
<string name="follow_list_selection">Sekot saraksts</string>
<string name="follow_set_type_description">Ikona %1$s sarakstam</string>
<string name="follow_set_icon_description">Ikona %1$s sarakstam</string>
<string name="follow_set_creation_menu_title">Izveidot jaunu sarakstu</string>
<string name="follow_set_creation_dialog_title">Jaunais %1$s saraksts</string>
<string name="follow_set_creation_name_label">Kolekcijas nosaukums</string>

View File

@@ -454,8 +454,7 @@
</string>
<string name="follow_set_add_author_from_note_action">Dodaj autora do zbioru obserwowanych</string>
<string name="follow_set_profile_actions_menu_description">Dodaj lub usuń użytkownika z list, lub utwórz nową listę z tym użytkownikiem.</string>
<string name="follow_set_type_description">Ikona dla listy %1$s</string>
<string name="follow_set_presence_indicator">"%1$s jest obecny na liście"</string>
<string name="follow_set_icon_description">Ikona dla listy %1$s</string>
<string name="follow_set_absence_indicator">"%1$s nie jest na liście"</string>
<string name="follow_set_man_dialog_title">Twój zbiór obserwowanych</string>
<string name="follow_set_empty_dialog_msg">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ć.</string>

View File

@@ -453,8 +453,7 @@
<string name="follow_set_empty_feed_msg">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.</string>
<string name="follow_set_add_author_from_note_action">Adicionar autor ao conjunto de seguimento</string>
<string name="follow_set_profile_actions_menu_description">Adicionar ou remover usuário de listas, ou criar uma nova lista com este usuário.</string>
<string name="follow_set_type_description">Ícone da lista %1$s</string>
<string name="follow_set_presence_indicator">"%1$s está presente nesta lista"</string>
<string name="follow_set_icon_description">Ícone da lista %1$s</string>
<string name="follow_set_absence_indicator">"%1$s não está nesta lista"</string>
<string name="follow_set_man_dialog_title">Seus conjuntos de seguimento</string>
<string name="follow_set_empty_dialog_msg">Nenhum conjunto de seguimento foi encontrado ou você não possui nenhum. Toque abaixo para atualizar ou use o menu para criar um.</string>

View File

@@ -453,8 +453,7 @@
<string name="follow_set_empty_feed_msg">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.</string>
<string name="follow_set_add_author_from_note_action">Lägg till författare i följ-set</string>
<string name="follow_set_profile_actions_menu_description">Lägg till eller ta bort användare från listor, eller skapa en ny lista med denna användare.</string>
<string name="follow_set_type_description">Ikon för %1$s-lista</string>
<string name="follow_set_presence_indicator">"%1$s finns i denna lista"</string>
<string name="follow_set_icon_description">Ikon för %1$s-lista</string>
<string name="follow_set_absence_indicator">"%1$s finns inte i denna lista"</string>
<string name="follow_set_man_dialog_title">Dina följ-set</string>
<string name="follow_set_empty_dialog_msg">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.</string>

View File

@@ -457,8 +457,7 @@
</string>
<string name="follow_set_add_author_from_note_action">添加作者到关注集</string>
<string name="follow_set_profile_actions_menu_description">从列表中添加或删除用户,或用此用户创建一个新列表。</string>
<string name="follow_set_type_description">%1$s 列表的图标</string>
<string name="follow_set_presence_indicator">"此列表中有 %1$s"</string>
<string name="follow_set_icon_description">%1$s 列表的图标</string>
<string name="follow_set_absence_indicator">"此列表中没有 %1$s"</string>
<string name="follow_set_man_dialog_title">您的关注集</string>
<string name="follow_set_empty_dialog_msg">未找到关注集,或者你还没有任何关注集。轻按下方刷新,或使用按钮新建。</string>

View File

@@ -519,22 +519,40 @@
It seems you do not have any follow sets yet.
\nTap below to refresh, or tap the add buttons to create a new one.
</string>
<string name="follow_set_create_btn_label">New</string>
<string name="follow_set_add_author_from_note_action">Add author to follow set</string>
<string name="follow_set_profile_actions_menu_description">Add or remove user from lists, or create a new list with this user.</string>
<string name="follow_set_type_description">Icon for %1$s List</string>
<string name="follow_set_presence_indicator">"%1$s is present in this list"</string>
<string name="follow_set_absence_indicator">"%1$s is not in this list"</string>
<string name="follow_set_icon_description">Icon for follow set</string>
<string name="follow_set_public_presence_indicator">%1$s is a public member</string>
<string name="follow_set_private_presence_indicator">%1$s is a private member</string>
<string name="follow_set_public_member_add_label">Add as public member</string>
<string name="follow_set_private_member_add_label">Add as private member</string>
<string name="follow_set_public_members_header_label">Public Profiles</string>
<string name="follow_set_private_members_header_label">Private Profiles</string>
<string name="follow_set_single_member_label">member</string>
<string name="follow_set_multiple_member_label">members</string>
<string name="follow_set_empty_label">No members</string>
<string name="follow_set_absence_indicator">%1$s is not in this list</string>
<string name="follow_set_man_dialog_title">Your Follow Sets</string>
<string name="follow_set_empty_dialog_msg">No follow sets were found, or you don\'t have any follow sets. Tap below to refresh, or use the menu to create one.</string>
<string name="follow_set_error_dialog_msg">There was a problem while fetching: %1$s</string>
<string name="follow_set_creation_menu_title">Make New List</string>
<string name="follow_set_creation_item_label">"Create new %1$s list with user</string>
<string name="follow_set_creation_item_description">Creates a %1$s follow set, and adds %2$s to it.</string>
<string name="follow_set_creation_dialog_title">New %1$s List</string>
<string name="follow_set_creation_item_label">New list with %1$s membership</string>
<string name="follow_set_creation_item_description">Creates a new follow set, and adds %1$s as a %2$s member.</string>
<string name="follow_set_creation_dialog_title">New Follow Set</string>
<string name="follow_set_copy_dialog_title">Copy/Clone Follow Set</string>
<string name="follow_set_desc_modify_label">Modify description</string>
<string name="follow_set_empty_desc_label">This list doesn\'t have a description</string>
<string name="follow_set_current_desc_label">Current description:</string>
<string name="follow_set_copy_indicator_description">You can set a custom name/description for this clone set below.</string>
<string name="follow_set_creation_name_label">Set name</string>
<string name="follow_set_copy_name_label">(Original Set name)</string>
<string name="follow_set_creation_desc_label">Set description(optional)</string>
<string name="follow_set_copy_desc_label">(Original Set description)</string>
<string name="follow_set_creation_action_btn_label">Create set</string>
<string name="follow_set_copy_action_btn_label">Copy/Clone set</string>
<string name="follow_set_rename_btn_label">Rename set</string>
<string name="follow_set_desc_modify_btn_label">Modify</string>
<string name="follow_set_rename_dialog_indicator_first_part">You are renaming from </string>
<string name="follow_set_rename_dialog_indicator_second_part"> to..</string>

View File

@@ -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-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" }

View File

@@ -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,
@@ -223,51 +248,61 @@ class PeopleListEvent(
title: String,
description: String? = null,
isPrivate: Boolean,
firstMemberHex: String? = null,
firstPublicMembers: List<String> = emptyList(),
firstPrivateMembers: List<String> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
if (description == null) {
val newList =
create(
val newListTemplate =
build(
name = title,
person = UserTag(pubKey = firstMemberHex ?: signer.pubKey),
isPrivate = isPrivate,
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)
} else {
if (isPrivate) {
val event =
}
suspend fun copy(
dTag: String,
title: String,
description: String? = null,
firstPublicMembers: List<String> = emptyList(),
firstPrivateMembers: List<String> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
val cloneTemplate =
build(
name = title,
privatePeople = listOf(UserTag(pubKey = firstMemberHex ?: signer.pubKey)),
publicPeople = firstPublicMembers.map { UserTag(pubKey = it) },
privatePeople = firstPrivateMembers.map { UserTag(pubKey = it) },
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)
}
if (description != null) addUnique(DescriptionTag.assemble(description))
}
val listClone = signer.sign(cloneTemplate)
onReady(listClone)
}
suspend fun createListWithUser(
@@ -311,6 +346,7 @@ class PeopleListEvent(
suspend fun removeUser(
earlierVersion: PeopleListEvent,
pubKeyHex: String,
isUserPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
@@ -319,6 +355,7 @@ class PeopleListEvent(
remove(
earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex),
isPrivate = isUserPrivate,
signer = signer,
createdAt = createdAt,
)
@@ -351,5 +388,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)
}
}
}
}
}