Merge pull request #1507 from KotlinGeekDev/followset-improvements

More follow set improvements.
This commit is contained in:
Vitor Pamplona
2025-10-14 09:46:07 -04:00
committed by GitHub
22 changed files with 734 additions and 306 deletions
@@ -31,9 +31,10 @@ data class FollowSet(
val identifierTag: String, val identifierTag: String,
val title: String, val title: String,
val description: String?, val description: String?,
val visibility: SetVisibility, val visibility: SetVisibility = SetVisibility.Mixed,
val profiles: Set<String>, val privateProfiles: Set<String> = emptySet(),
) : NostrSet(setVisibility = visibility, content = profiles) { val publicProfiles: Set<String> = emptySet(),
) : NostrSet(setVisibility = visibility, privateContent = privateProfiles, publicContent = publicProfiles) {
companion object { companion object {
fun mapEventToSet( fun mapEventToSet(
event: PeopleListEvent, event: PeopleListEvent,
@@ -53,26 +54,22 @@ data class FollowSet(
identifierTag = dTag, identifierTag = dTag,
title = listTitle, title = listTitle,
description = listDescription, description = listDescription,
visibility = SetVisibility.Private, privateProfiles = privateFollows.toSet(),
profiles = privateFollows.toSet(),
) )
} else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) { } else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) {
FollowSet( FollowSet(
identifierTag = dTag, identifierTag = dTag,
title = listTitle, title = listTitle,
description = listDescription, description = listDescription,
visibility = SetVisibility.Public, publicProfiles = publicFollows.toSet(),
profiles = publicFollows.toSet(),
) )
} else { } else {
// Follow set is empty, so assume public. Why? Nostr limitation.
// TODO: Could this be fixed at protocol level?
FollowSet( FollowSet(
identifierTag = dTag, identifierTag = dTag,
title = listTitle, title = listTitle,
description = listDescription, description = listDescription,
visibility = SetVisibility.Public, privateProfiles = privateFollows.toSet(),
profiles = publicFollows.toSet(), publicProfiles = publicFollows.toSet(),
) )
} }
} }
@@ -25,20 +25,16 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class FollowSetState( class FollowSetState(
@@ -50,7 +46,7 @@ class FollowSetState(
private val isActive = MutableStateFlow(false) private val isActive = MutableStateFlow(false)
suspend fun getFollowSetNotes() = suspend fun getFollowSetNotes() =
withContext(Dispatchers.Default) { withContext(Dispatchers.IO) {
val followSetNotes = LocalCache.getFollowSetNotesFor(user) val followSetNotes = LocalCache.getFollowSetNotesFor(user)
return@withContext followSetNotes return@withContext followSetNotes
} }
@@ -63,12 +59,12 @@ class FollowSetState(
emit(followSets) emit(followSets)
delay(2000) delay(2000)
} }
}.flowOn(Dispatchers.Default) }.flowOn(Dispatchers.IO)
val profilesFlow = val profilesFlow =
getFollowSetNotesFlow() getFollowSetNotesFlow()
.map { it -> .map { it ->
it.flatMapTo(mutableSetOf()) { it.profiles }.toSet() it.flatMapTo(mutableSetOf()) { it.privateProfiles + it.publicProfiles }.toSet()
}.stateIn(scope, SharingStarted.Eagerly, emptySet()) }.stateIn(scope, SharingStarted.Eagerly, emptySet())
fun mapNoteToFollowSet(note: Note): FollowSet = fun mapNoteToFollowSet(note: Note): FollowSet =
@@ -82,14 +78,5 @@ class FollowSetState(
init { init {
isActive.update { true } 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 {}
}
} }
} }
@@ -22,11 +22,13 @@ package com.vitorpamplona.amethyst.model.nip51Lists.followSets
sealed class NostrSet( sealed class NostrSet(
val setVisibility: SetVisibility, val setVisibility: SetVisibility,
val content: Collection<String>, val privateContent: Collection<String>,
val publicContent: Collection<String>,
) )
class CuratedBookmarkSet( class CuratedBookmarkSet(
val name: String, val name: String,
val visibility: SetVisibility, val visibility: SetVisibility,
val setItems: List<String>, val privateSetItems: List<String>,
) : NostrSet(visibility, setItems) val publicSetItems: List<String>,
) : NostrSet(visibility, privateSetItems, publicSetItems)
@@ -30,9 +30,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
@@ -48,8 +50,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -119,12 +119,11 @@ fun ListsAndSetsScreen(
refresh = { refresh = {
followSetsViewModel.invalidateData() followSetsViewModel.invalidateData()
}, },
addItem = { title: String, description: String?, listType: SetVisibility -> addItem = { title: String, description: String? ->
val isSetPrivate = listType == SetVisibility.Private
followSetsViewModel.addFollowSet( followSetsViewModel.addFollowSet(
setName = title, setName = title,
setDescription = description, setDescription = description,
isListPrivate = isSetPrivate,
account = accountViewModel.account, account = accountViewModel.account,
) )
}, },
@@ -138,6 +137,21 @@ fun ListsAndSetsScreen(
account = accountViewModel.account, 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 -> deleteItem = { followSet ->
followSetsViewModel.deleteFollowSet( followSetsViewModel.deleteFollowSet(
followSet = followSet, followSet = followSet,
@@ -153,14 +167,16 @@ fun ListsAndSetsScreen(
fun CustomListsScreen( fun CustomListsScreen(
followSetFeedState: FollowSetFeedState, followSetFeedState: FollowSetFeedState,
refresh: () -> Unit, refresh: () -> Unit,
addItem: (title: String, description: String?, listType: SetVisibility) -> Unit, addItem: (title: String, description: String?) -> Unit,
openItem: (identifier: String) -> Unit, openItem: (identifier: String) -> Unit,
renameItem: (followSet: FollowSet, newName: 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, deleteItem: (followSet: FollowSet) -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val pagerState = rememberPagerState { 3 } val pagerState = rememberPagerState { 2 }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
DisappearingScaffold( DisappearingScaffold(
@@ -185,22 +201,14 @@ fun CustomListsScreen(
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
text = { Text(text = stringRes(R.string.labeled_bookmarks), overflow = TextOverflow.Visible) }, 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 = { floatingButton = {
// TODO: Show components based on current tab // TODO: Show components based on current tab
FollowSetFabsAndMenu( FollowSetFabsAndMenu(
onAddPrivateSet = { name: String, description: String? -> onAddSet = { name: String, description: String? ->
addItem(name, description, SetVisibility.Private) addItem(name, description)
},
onAddPublicSet = { name: String, description: String? ->
addItem(name, description, SetVisibility.Public)
}, },
) )
}, },
@@ -222,11 +230,13 @@ fun CustomListsScreen(
onRefresh = refresh, onRefresh = refresh,
onOpenItem = openItem, onOpenItem = openItem,
onRenameItem = renameItem, onRenameItem = renameItem,
onItemDescriptionChange = changeItemDescription,
onItemClone = cloneItem,
onDeleteItem = deleteItem, onDeleteItem = deleteItem,
) )
1 -> LabeledBookmarksFeedView() 1 -> LabeledBookmarksFeedView()
2 -> GeneralBookmarksFeedView() // 2 -> GeneralBookmarksFeedView()
} }
} }
} }
@@ -236,57 +246,34 @@ fun CustomListsScreen(
@Composable @Composable
private fun FollowSetFabsAndMenu( private fun FollowSetFabsAndMenu(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onAddPrivateSet: (name: String, description: String?) -> Unit, onAddSet: (name: String, description: String?) -> Unit,
onAddPublicSet: (name: String, description: String?) -> Unit,
) { ) {
val isSetAdditionDialogOpen = remember { mutableStateOf(false) } val isSetAdditionDialogOpen = remember { mutableStateOf(false) }
val isPrivateOptionTapped = remember { mutableStateOf(false) }
Row( ExtendedFloatingActionButton(
horizontalArrangement = Arrangement.spacedBy(10.dp), text = {
) { Text(text = stringRes(R.string.follow_set_create_btn_label))
FloatingActionButton( },
onClick = { icon = {
isPrivateOptionTapped.value = true
isSetAdditionDialogOpen.value = true
},
shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon( Icon(
painter = painterResource(R.drawable.lock_plus), imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
contentDescription = null, contentDescription = null,
tint = Color.White,
) )
} },
FloatingActionButton( onClick = {
onClick = { isSetAdditionDialogOpen.value = true
isSetAdditionDialogOpen.value = true },
}, shape = CircleShape,
shape = CircleShape, containerColor = MaterialTheme.colorScheme.primary,
containerColor = MaterialTheme.colorScheme.primary, )
) {
Icon(
painter = painterResource(R.drawable.earth_plus),
contentDescription = null,
tint = Color.White,
)
}
}
if (isSetAdditionDialogOpen.value) { if (isSetAdditionDialogOpen.value) {
NewSetCreationDialog( NewSetCreationDialog(
onDismiss = { onDismiss = {
isSetAdditionDialogOpen.value = false isSetAdditionDialogOpen.value = false
isPrivateOptionTapped.value = false
}, },
shouldBePrivate = isPrivateOptionTapped.value,
onCreateList = { name, description -> onCreateList = { name, description ->
if (isPrivateOptionTapped.value) { onAddSet(name, description)
onAddPrivateSet(name, description)
} else {
onAddPublicSet(name, description)
}
}, },
) )
} }
@@ -296,27 +283,10 @@ private fun FollowSetFabsAndMenu(
fun NewSetCreationDialog( fun NewSetCreationDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onDismiss: () -> Unit, onDismiss: () -> Unit,
shouldBePrivate: Boolean,
onCreateList: (name: String, description: String?) -> Unit, onCreateList: (name: String, description: String?) -> Unit,
) { ) {
val newListName = remember { mutableStateOf("") } val newListName = remember { mutableStateOf("") }
val newListDescription = remember { mutableStateOf<String?>(null) } 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( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@@ -325,12 +295,8 @@ fun NewSetCreationDialog(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Icon(
painter = painterResource(listTypeIcon),
contentDescription = null,
)
Text( 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) Spacer(modifier = DoubleVertSpacer)
// For the list description // For the set description
TextField( TextField(
value = value =
( (
@@ -425,6 +391,12 @@ private fun SetItemPreview() {
onFollowSetRename = { onFollowSetRename = {
println("Follow set new name: $it") 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 = { onFollowSetDelete = {
println(" The follow set ${sampleFollowSet.title} has been deleted.") println(" The follow set ${sampleFollowSet.title} has been deleted.")
}, },
@@ -37,13 +37,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@@ -56,15 +53,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet 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.components.ClickableBox
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import kotlin.let
@Composable @Composable
fun CustomSetItem( fun CustomSetItem(
@@ -72,6 +68,8 @@ fun CustomSetItem(
followSet: FollowSet, followSet: FollowSet,
onFollowSetClick: () -> Unit, onFollowSetClick: () -> Unit,
onFollowSetRename: (String) -> Unit, onFollowSetRename: (String) -> Unit,
onFollowSetDescriptionChange: (String?) -> Unit,
onFollowSetClone: (customName: String?, customDescription: String?) -> Unit,
onFollowSetDelete: () -> Unit, onFollowSetDelete: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -96,20 +94,75 @@ fun CustomSetItem(
) { ) {
Text(followSet.title, fontWeight = FontWeight.Bold) Text(followSet.title, fontWeight = FontWeight.Bold)
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
FilterChip( if (followSet.publicProfiles.isEmpty() && followSet.privateProfiles.isEmpty()) {
selected = true, FilterChip(
onClick = {}, selected = true,
label = { onClick = {},
Text(text = "${followSet.profiles.size}") label = {
}, Text(text = stringRes(R.string.follow_set_empty_label))
leadingIcon = { },
Icon( leadingIcon = {
imageVector = Icons.Default.People, Icon(
contentDescription = null, 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
},
) )
}, FilterChip(
shape = ButtonBorder, 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) Spacer(modifier = StdVertSpacer)
Text( Text(
@@ -119,34 +172,6 @@ fun CustomSetItem(
maxLines = 2, 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( Column(
@@ -159,7 +184,10 @@ fun CustomSetItem(
) { ) {
SetOptionsButton( SetOptionsButton(
followSetName = followSet.title, followSetName = followSet.title,
onListRename = onFollowSetRename, followSetDescription = followSet.description,
onSetRename = onFollowSetRename,
onSetDescriptionChange = onFollowSetDescriptionChange,
onSetCloneCreate = onFollowSetClone,
onListDelete = onFollowSetDelete, onListDelete = onFollowSetDelete,
) )
} }
@@ -167,10 +195,13 @@ fun CustomSetItem(
} }
@Composable @Composable
fun SetOptionsButton( private fun SetOptionsButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
followSetName: String, followSetName: String,
onListRename: (String) -> Unit, followSetDescription: String?,
onSetRename: (String) -> Unit,
onSetDescriptionChange: (String?) -> Unit,
onSetCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
onListDelete: () -> Unit, onListDelete: () -> Unit,
) { ) {
val isMenuOpen = remember { mutableStateOf(false) } val isMenuOpen = remember { mutableStateOf(false) }
@@ -181,10 +212,13 @@ fun SetOptionsButton(
VerticalDotsIcon() VerticalDotsIcon()
SetOptionsMenu( SetOptionsMenu(
listName = followSetName, setName = followSetName,
setDescription = followSetDescription,
isExpanded = isMenuOpen.value, isExpanded = isMenuOpen.value,
onDismiss = { isMenuOpen.value = false }, onDismiss = { isMenuOpen.value = false },
onListRename = onListRename, onSetRename = onSetRename,
onSetDescriptionChange = onSetDescriptionChange,
onSetClone = onSetCloneCreate,
onDelete = onListDelete, onDelete = onListDelete,
) )
} }
@@ -194,26 +228,27 @@ fun SetOptionsButton(
private fun SetOptionsMenu( private fun SetOptionsMenu(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isExpanded: Boolean, isExpanded: Boolean,
listName: String, setName: String,
onListRename: (String) -> Unit, setDescription: String?,
onSetRename: (String) -> Unit,
onSetDescriptionChange: (String?) -> Unit,
onSetClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
val isRenameDialogOpen = remember { mutableStateOf(false) } val isRenameDialogOpen = remember { mutableStateOf(false) }
val renameString = remember { mutableStateOf("") } 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( DropdownMenu(
expanded = isExpanded, expanded = isExpanded,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
) { ) {
DropdownMenuItem(
text = {
Text(text = stringRes(R.string.quick_action_delete))
},
onClick = {
onDelete()
},
)
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Text(text = stringRes(R.string.follow_set_rename_btn_label)) Text(text = stringRes(R.string.follow_set_rename_btn_label))
@@ -223,25 +258,76 @@ private fun SetOptionsMenu(
onDismiss() 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) { if (isRenameDialogOpen.value) {
RenameDialog( SetRenameDialog(
currentName = listName, currentName = setName,
newName = renameString.value, newName = renameString.value,
onStringRenameChange = { onStringRenameChange = {
renameString.value = it renameString.value = it
}, },
onDismissDialog = { isRenameDialogOpen.value = false }, onDismissDialog = { isRenameDialogOpen.value = false },
onListRename = { 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 @Composable
private fun RenameDialog( private fun SetRenameDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
currentName: String, currentName: String,
newName: 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))
}
},
)
}
@@ -51,6 +51,8 @@ fun FollowSetFeedView(
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
onOpenItem: (String) -> Unit = {}, onOpenItem: (String) -> Unit = {},
onRenameItem: (targetSet: FollowSet, newName: 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, onDeleteItem: (followSet: FollowSet) -> Unit,
) { ) {
when (followSetFeedState) { when (followSetFeedState) {
@@ -63,6 +65,8 @@ fun FollowSetFeedView(
onRefresh = onRefresh, onRefresh = onRefresh,
onItemClick = onOpenItem, onItemClick = onOpenItem,
onItemRename = onRenameItem, onItemRename = onRenameItem,
onItemDescriptionChange = onItemDescriptionChange,
onItemClone = onItemClone,
onItemDelete = onDeleteItem, onItemDelete = onDeleteItem,
) )
} }
@@ -91,6 +95,8 @@ fun FollowSetLoaded(
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
onItemClick: (itemIdentifier: String) -> Unit = {}, onItemClick: (itemIdentifier: String) -> Unit = {},
onItemRename: (followSet: FollowSet, newName: 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, onItemDelete: (followSet: FollowSet) -> Unit,
) { ) {
Log.d("FollowSetComposable", "FollowSetLoaded: Follow Set size: ${loadedFeedState.size}") Log.d("FollowSetComposable", "FollowSetLoaded: Follow Set size: ${loadedFeedState.size}")
@@ -113,6 +119,12 @@ fun FollowSetLoaded(
onFollowSetRename = { onFollowSetRename = {
onItemRename(set, it) onItemRename(set, it)
}, },
onFollowSetDescriptionChange = { newDescription ->
onItemDescriptionChange(set, newDescription)
},
onFollowSetClone = { cloneName, cloneDescription ->
onItemClone(set, cloneName, cloneDescription)
},
onFollowSetDelete = { onFollowSetDelete = {
onItemDelete(set) onItemDelete(set)
}, },
@@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet 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.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter
@@ -117,8 +116,8 @@ class FollowSetFeedViewModel(
fun addFollowSet( fun addFollowSet(
setName: String, setName: String,
setDescription: String?, setDescription: String?,
isListPrivate: Boolean,
optionalFirstMemberHex: String? = null, optionalFirstMemberHex: String? = null,
firstMemberShouldBePrivate: Boolean = false,
account: Account, account: Account,
) { ) {
if (!account.settings.isWriteable()) { if (!account.settings.isWriteable()) {
@@ -129,8 +128,9 @@ class FollowSetFeedViewModel(
dTag = UUID.randomUUID().toString(), dTag = UUID.randomUUID().toString(),
title = setName, title = setName,
description = setDescription, description = setDescription,
isPrivate = isListPrivate, isPrivate = firstMemberShouldBePrivate,
firstMemberHex = optionalFirstMemberHex, firstPublicMembers = if (optionalFirstMemberHex != null) listOf(optionalFirstMemberHex) else emptyList(),
firstPrivateMembers = if (optionalFirstMemberHex != null) listOf(optionalFirstMemberHex) else emptyList(),
signer = account.signer, signer = account.signer,
) { ) {
account.sendMyPublicAndPrivateOutbox(it) 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( fun deleteFollowSet(
followSet: FollowSet, followSet: FollowSet,
account: Account, account: Account,
@@ -179,6 +224,7 @@ class FollowSetFeedViewModel(
fun addUserToSet( fun addUserToSet(
userProfileHex: String, userProfileHex: String,
followSet: FollowSet, followSet: FollowSet,
shouldBePrivateMember: Boolean,
account: Account, account: Account,
) { ) {
if (!account.settings.isWriteable()) { if (!account.settings.isWriteable()) {
@@ -190,7 +236,7 @@ class FollowSetFeedViewModel(
PeopleListEvent.addUser( PeopleListEvent.addUser(
earlierVersion = followSetEvent, earlierVersion = followSetEvent,
pubKeyHex = userProfileHex, pubKeyHex = userProfileHex,
isPrivate = followSet.visibility == SetVisibility.Private, isPrivate = shouldBePrivateMember,
signer = account.signer, signer = account.signer,
) { ) {
account.sendMyPublicAndPrivateOutbox(it) account.sendMyPublicAndPrivateOutbox(it)
@@ -201,6 +247,7 @@ class FollowSetFeedViewModel(
fun removeUserFromSet( fun removeUserFromSet(
userProfileHex: String, userProfileHex: String,
userIsPrivate: Boolean,
followSet: FollowSet, followSet: FollowSet,
account: Account, account: Account,
) { ) {
@@ -213,6 +260,7 @@ class FollowSetFeedViewModel(
PeopleListEvent.removeUser( PeopleListEvent.removeUser(
earlierVersion = followSetEvent, earlierVersion = followSetEvent,
pubKeyHex = userProfileHex, pubKeyHex = userProfileHex,
isUserPrivate = userIsPrivate,
signer = account.signer, signer = account.signer,
) { ) {
account.sendMyPublicAndPrivateOutbox(it) account.sendMyPublicAndPrivateOutbox(it)
@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize 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.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet 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.components.ClickableBox
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.UserCompose 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.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton 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.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.VertPadding
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -144,7 +147,8 @@ fun FollowSetScreen(
when { when {
selectedSetState.value != null -> { selectedSetState.value != null -> {
val selectedSet = selectedSetState.value 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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -171,6 +175,7 @@ fun FollowSetScreen(
selectedSet, selectedSet,
accountViewModel.account, accountViewModel.account,
) )
navigator.popBack()
}, },
) )
}, },
@@ -190,10 +195,12 @@ fun FollowSetScreen(
bottom = padding.calculateBottomPadding(), bottom = padding.calculateBottomPadding(),
).consumeWindowInsets(padding) ).consumeWindowInsets(padding)
.imePadding(), .imePadding(),
followSetList = users, publicMemberList = publicMembers,
privateMemberList = privateMembers,
onDeleteUser = { onDeleteUser = {
followSetViewModel.removeUserFromSet( followSetViewModel.removeUserFromSet(
it, it,
userIsPrivate = selectedSet.privateProfiles.contains(it),
selectedSet, selectedSet,
accountViewModel.account, accountViewModel.account,
) )
@@ -233,14 +240,7 @@ fun TitleAndDescription(
) )
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
Icon( Icon(
painter = painter = painterResource(R.drawable.format_list_bulleted_type),
painterResource(
when (followSet.setVisibility) {
SetVisibility.Public -> R.drawable.ic_public
SetVisibility.Private -> R.drawable.lock
SetVisibility.Mixed -> R.drawable.format_list_bulleted_type
},
),
contentDescription = null, contentDescription = null,
) )
} }
@@ -260,7 +260,8 @@ fun TitleAndDescription(
@Composable @Composable
private fun FollowSetListView( private fun FollowSetListView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
followSetList: List<User>, publicMemberList: List<User>,
privateMemberList: List<User>,
onDeleteUser: (String) -> Unit, onDeleteUser: (String) -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
@@ -272,14 +273,61 @@ private fun FollowSetListView(
contentPadding = FeedPadding, contentPadding = FeedPadding,
state = listState, state = listState,
) { ) {
itemsIndexed(followSetList, key = { _, item -> item.pubkeyHex }) { _, item -> if (publicMemberList.isNotEmpty()) {
FollowSetListItem( stickyHeader {
modifier = Modifier.animateItem(), Column(
user = item, modifier = VertPadding,
accountViewModel = accountViewModel, ) {
nav = nav, Text(
onDeleteUser = onDeleteUser, 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,
accountViewModel = accountViewModel,
nav = nav,
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))
}
} }
} }
} }
@@ -45,6 +45,8 @@ import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider 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.res.painterResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@@ -165,16 +168,18 @@ fun FollowSetsManagementDialog(
) { ) {
when (followSetsState) { when (followSetsState) {
is FollowSetFeedState.Loaded -> { 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) Spacer(StdVertSpacer)
FollowSetItem( FollowSetItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
listHeader = list.title, listHeader = set.title,
setVisibility = list.visibility, setVisibility = set.visibility,
userName = userInfo.toBestDisplayName(), userName = userInfo.toBestDisplayName(),
isUserInList = list.profiles.contains(userHex), userIsPrivateMember = set.privateProfiles.contains(userHex),
userIsPublicMember = set.publicProfiles.contains(userHex),
onRemoveUser = { onRemoveUser = {
Log.d( Log.d(
"Amethyst", "Amethyst",
@@ -182,23 +187,31 @@ fun FollowSetsManagementDialog(
) )
followSetsViewModel.removeUserFromSet( followSetsViewModel.removeUserFromSet(
userHex, userHex,
list, userIsPrivate = set.privateProfiles.contains(userHex),
set,
account, account,
) )
Log.d( Log.d(
"Amethyst", "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( Log.d(
"Amethyst", "Amethyst",
"ProfileActions: Adding item to list ...", "ProfileActions: Adding item to list ...",
) )
followSetsViewModel.addUserToSet(userHex, list, account) followSetsViewModel.addUserToSet(
userHex,
set,
userShouldBePrivate,
account,
)
Log.d( Log.d(
"Amethyst", "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) { if (followSetsState != FollowSetFeedState.Loading) {
FollowSetsCreationMenu( FollowSetsCreationMenu(
userName = userInfo.toBestDisplayName(), userName = userInfo.toBestDisplayName(),
onSetCreate = { setName, setIsPrivate, description -> onSetCreate = { setName, memberShouldBePrivate, description ->
followSetsViewModel.addFollowSet( followSetsViewModel.addFollowSet(
setName = setName, setName = setName,
setDescription = description, setDescription = description,
isListPrivate = setIsPrivate, firstMemberShouldBePrivate = memberShouldBePrivate,
optionalFirstMemberHex = userHex, optionalFirstMemberHex = userHex,
account = account, account = account,
) )
@@ -306,11 +319,13 @@ fun FollowSetItem(
listHeader: String, listHeader: String,
setVisibility: SetVisibility, setVisibility: SetVisibility,
userName: String, userName: String,
isUserInList: Boolean, userIsPrivateMember: Boolean,
onAddUser: () -> Unit, userIsPublicMember: Boolean,
onAddUserToList: (shouldBePrivateMember: Boolean) -> Unit,
onRemoveUser: () -> Unit, onRemoveUser: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val isUserInList = userIsPrivateMember || userIsPublicMember
Row( Row(
modifier = modifier =
modifier modifier
@@ -330,26 +345,10 @@ fun FollowSetItem(
) { ) {
Text(listHeader, fontWeight = FontWeight.Bold) Text(listHeader, fontWeight = FontWeight.Bold)
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
setVisibility.let { Icon(
val text by derivedStateOf { painter = painterResource(R.drawable.format_list_bulleted_type),
when (it) { contentDescription = stringRes(R.string.follow_set_icon_description),
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),
)
}
} }
Spacer(modifier = StdVertSpacer) Spacer(modifier = StdVertSpacer)
@@ -362,7 +361,11 @@ fun FollowSetItem(
Text( Text(
text = text =
if (isUserInList) { 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 { } else {
stringRes(R.string.follow_set_absence_indicator, userName) stringRes(R.string.follow_set_absence_indicator, userName)
}, },
@@ -389,9 +392,14 @@ fun FollowSetItem(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
val isUserAddTapped = remember { mutableStateOf(false) }
IconButton( IconButton(
onClick = { onClick = {
if (isUserInList) onRemoveUser() else onAddUser() if (isUserInList) {
onRemoveUser()
} else {
isUserAddTapped.value = true
}
}, },
modifier = modifier =
Modifier Modifier
@@ -423,15 +431,55 @@ fun FollowSetItem(
text = stringRes(if (isUserInList) R.string.remove else R.string.add), text = stringRes(if (isUserInList) R.string.remove else R.string.add),
color = MaterialTheme.colorScheme.onBackground, 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 @Composable
fun FollowSetsCreationMenu( fun FollowSetsCreationMenu(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
userName: String, userName: String,
onSetCreate: (setName: String, setIsPrivate: Boolean, description: String?) -> Unit, onSetCreate: (setName: String, memberShouldBePrivate: Boolean, description: String?) -> Unit,
) { ) {
val isListAdditionDialogOpen = remember { mutableStateOf(false) } val isListAdditionDialogOpen = remember { mutableStateOf(false) }
val isPrivateOptionTapped = remember { mutableStateOf(false) } val isPrivateOptionTapped = remember { mutableStateOf(false) }
@@ -474,7 +522,6 @@ fun FollowSetsCreationMenu(
isListAdditionDialogOpen.value = false isListAdditionDialogOpen.value = false
isPrivateOptionTapped.value = false isPrivateOptionTapped.value = false
}, },
shouldBePrivate = isPrivateOptionTapped.value,
onCreateList = { name, description -> onCreateList = { name, description ->
onSetCreate(name, isPrivateOptionTapped.value, description) onSetCreate(name, isPrivateOptionTapped.value, description)
}, },
@@ -528,7 +575,11 @@ fun FollowSetCreationItem(
} }
Spacer(modifier = StdVertSpacer) Spacer(modifier = StdVertSpacer)
Text( 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, fontWeight = FontWeight.Light,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 2, maxLines = 2,
@@ -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_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_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_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_icon_description">Ikona pro seznam %1$s</string>
<string name="follow_set_presence_indicator">"%1$s je v tomto seznamu"</string>
<string name="follow_set_absence_indicator">"%1$s není v tomto seznamu"</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_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> <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>
+1 -2
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_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_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_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_icon_description">Ikona pro seznam %1$s</string>
<string name="follow_set_presence_indicator">%1$s je v tomto seznamu</string>
<string name="follow_set_absence_indicator">%1$s není v tomto seznamu</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_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> <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>
@@ -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_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_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_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_icon_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_absence_indicator">"%1$s ist nicht in dieser 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_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> <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>
+1 -2
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_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_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_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_icon_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_absence_indicator">%1$s ist nicht in dieser 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_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> <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>
@@ -457,8 +457,7 @@
</string> </string>
<string name="follow_set_add_author_from_note_action">लेखक जोडें अनुगम्य सूची में</string> <string name="follow_set_add_author_from_note_action">लेखक जोडें अनुगम्य सूची में</string>
<string name="follow_set_profile_actions_menu_description">प्रयोक्ता को सूचियों में जोडें अथवा हटाएँ अथवा इस प्रयोक्ता के साथ नई सूची बनाएँ।</string> <string name="follow_set_profile_actions_menu_description">प्रयोक्ता को सूचियों में जोडें अथवा हटाएँ अथवा इस प्रयोक्ता के साथ नई सूची बनाएँ।</string>
<string name="follow_set_type_description">सूची %1$s के लिए चिह्न</string> <string name="follow_set_icon_description">सूची %1$s के लिए चिह्न</string>
<string name="follow_set_presence_indicator">"%1$s इस सूची में है"</string>
<string name="follow_set_absence_indicator">"%1$s इस सूची में नहीं है"</string> <string name="follow_set_absence_indicator">"%1$s इस सूची में नहीं है"</string>
<string name="follow_set_man_dialog_title">आपके अनुगम्य सूचियाँ</string> <string name="follow_set_man_dialog_title">आपके अनुगम्य सूचियाँ</string>
<string name="follow_set_empty_dialog_msg">कोई अनुगम्य सूचियाँ प्राप्त नहीं। अथवा आपका कोई अनुगम्य सूचियाँ हैं नहीं। नवीकरण के लिए नीचे दबाएँ अथवा विकल्पसूची द्वारा एक नया बनाएँ।</string> <string name="follow_set_empty_dialog_msg">कोई अनुगम्य सूचियाँ प्राप्त नहीं। अथवा आपका कोई अनुगम्य सूचियाँ हैं नहीं। नवीकरण के लिए नीचे दबाएँ अथवा विकल्पसूची द्वारा एक नया बनाएँ।</string>
@@ -457,8 +457,7 @@
</string> </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_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_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_icon_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_absence_indicator">"A(z) %1$s nincs a listában"</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_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> <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>
@@ -114,7 +114,7 @@
<string name="yes"></string> <string name="yes"></string>
<string name="no"></string> <string name="no"></string>
<string name="follow_list_selection">Sekot saraksts</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_menu_title">Izveidot jaunu sarakstu</string>
<string name="follow_set_creation_dialog_title">Jaunais %1$s saraksts</string> <string name="follow_set_creation_dialog_title">Jaunais %1$s saraksts</string>
<string name="follow_set_creation_name_label">Kolekcijas nosaukums</string> <string name="follow_set_creation_name_label">Kolekcijas nosaukums</string>
@@ -454,8 +454,7 @@
</string> </string>
<string name="follow_set_add_author_from_note_action">Dodaj autora do zbioru obserwowanych</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_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_icon_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_absence_indicator">"%1$s nie jest na liście"</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_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> <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>
@@ -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_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_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_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_icon_description">Ícone da lista %1$s</string>
<string name="follow_set_presence_indicator">"%1$s está presente nesta lista"</string>
<string name="follow_set_absence_indicator">"%1$s não está nesta lista"</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_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> <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>
@@ -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_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_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_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_icon_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_absence_indicator">"%1$s finns inte i denna 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_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> <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>
@@ -457,8 +457,7 @@
</string> </string>
<string name="follow_set_add_author_from_note_action">添加作者到关注集</string> <string name="follow_set_add_author_from_note_action">添加作者到关注集</string>
<string name="follow_set_profile_actions_menu_description">从列表中添加或删除用户,或用此用户创建一个新列表。</string> <string name="follow_set_profile_actions_menu_description">从列表中添加或删除用户,或用此用户创建一个新列表。</string>
<string name="follow_set_type_description">%1$s 列表的图标</string> <string name="follow_set_icon_description">%1$s 列表的图标</string>
<string name="follow_set_presence_indicator">"此列表中有 %1$s"</string>
<string name="follow_set_absence_indicator">"此列表中没有 %1$s"</string> <string name="follow_set_absence_indicator">"此列表中没有 %1$s"</string>
<string name="follow_set_man_dialog_title">您的关注集</string> <string name="follow_set_man_dialog_title">您的关注集</string>
<string name="follow_set_empty_dialog_msg">未找到关注集,或者你还没有任何关注集。轻按下方刷新,或使用按钮新建。</string> <string name="follow_set_empty_dialog_msg">未找到关注集,或者你还没有任何关注集。轻按下方刷新,或使用按钮新建。</string>
+24 -6
View File
@@ -519,22 +519,40 @@
It seems you do not have any follow sets yet. It seems you do not have any follow sets yet.
\nTap below to refresh, or tap the add buttons to create a new one. \nTap below to refresh, or tap the add buttons to create a new one.
</string> </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_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_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_icon_description">Icon for follow set</string>
<string name="follow_set_presence_indicator">"%1$s is present in this list"</string> <string name="follow_set_public_presence_indicator">%1$s is a public member</string>
<string name="follow_set_absence_indicator">"%1$s is not in this list"</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_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_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_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_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_label">New list with %1$s membership</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_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 %1$s List</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_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_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_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_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_first_part">You are renaming from </string>
<string name="follow_set_rename_dialog_indicator_second_part"> to..</string> <string name="follow_set_rename_dialog_indicator_second_part"> to..</string>
@@ -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( suspend fun resign(
publicTags: TagArray, publicTags: TagArray,
privateTags: TagArray, privateTags: TagArray,
@@ -223,51 +248,61 @@ class PeopleListEvent(
title: String, title: String,
description: String? = null, description: String? = null,
isPrivate: Boolean, isPrivate: Boolean,
firstMemberHex: String? = null, firstPublicMembers: List<String> = emptyList(),
firstPrivateMembers: List<String> = emptyList(),
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit, onReady: (PeopleListEvent) -> Unit,
) { ) {
if (description == null) { val newListTemplate =
val newList = build(
create( name = title,
name = title, publicPeople =
person = UserTag(pubKey = firstMemberHex ?: signer.pubKey), if (!isPrivate && firstPublicMembers.isNotEmpty()) {
isPrivate = isPrivate, firstPublicMembers.map { UserTag(pubKey = it) }
signer = signer, } else {
dTag = dTag, emptyList()
createdAt = createdAt, },
) privatePeople =
onReady(newList) if (isPrivate && firstPrivateMembers.isNotEmpty()) {
} else { firstPrivateMembers.map { UserTag(pubKey = it) }
if (isPrivate) { } else {
val event = emptyList()
build( },
name = title, signer = signer,
privatePeople = listOf(UserTag(pubKey = firstMemberHex ?: signer.pubKey)), dTag = dTag,
signer = signer, createdAt = createdAt,
dTag = dTag, ) {
createdAt = createdAt, if (description != null) addUnique(DescriptionTag.assemble(description))
) {
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 newList = signer.sign(newListTemplate)
onReady(newList)
}
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,
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( suspend fun createListWithUser(
@@ -311,6 +346,7 @@ class PeopleListEvent(
suspend fun removeUser( suspend fun removeUser(
earlierVersion: PeopleListEvent, earlierVersion: PeopleListEvent,
pubKeyHex: String, pubKeyHex: String,
isUserPrivate: Boolean,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit, onReady: (PeopleListEvent) -> Unit,
@@ -319,6 +355,7 @@ class PeopleListEvent(
remove( remove(
earlierVersion = earlierVersion, earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex), person = UserTag(pubKey = pubKeyHex),
isPrivate = isUserPrivate,
signer = signer, signer = signer,
createdAt = createdAt, createdAt = createdAt,
) )
@@ -351,5 +388,51 @@ class PeopleListEvent(
) )
onReady(modified) 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)
}
}
}
} }
} }