feat: bulk-remove for blocked users and hidden words
Long-press a row in Settings -> Security Filters to enter selection mode, pick more rows, tap Unblock. The mute list (kind 10000) and block list are rebuilt and re-broadcast once for all selected entries instead of once per item. - Quartz: add `MuteListEvent.removeAll` and `PeopleListEvent.removeAll` bulk overloads using the existing `TagArray.removeAny` primitive. - Account / state classes: add `showUsers(List)` and `showWords(List)` that produce a single resigned event per list. - AccountViewModel: thin wrappers `showUsers` / `showWords`. - SecurityFiltersScreen: hoist per-tab selection sets, swap top bar in selection mode, add a local selectable user list (the shared `UserCompose` row was kept untouched).
This commit is contained in:
@@ -2768,6 +2768,19 @@ class Account(
|
|||||||
hiddenUsers.showUser(pubkeyHex)
|
hiddenUsers.showUser(pubkeyHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun showUsers(pubkeys: List<HexKey>) {
|
||||||
|
if (pubkeys.isEmpty()) return
|
||||||
|
sendMyPublicAndPrivateOutbox(blockPeopleList.showUsers(pubkeys))
|
||||||
|
sendMyPublicAndPrivateOutbox(muteList.showUsers(pubkeys))
|
||||||
|
pubkeys.forEach { hiddenUsers.showUser(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun showWords(words: List<String>) {
|
||||||
|
if (words.isEmpty()) return
|
||||||
|
sendMyPublicAndPrivateOutbox(blockPeopleList.showWords(words))
|
||||||
|
sendMyPublicAndPrivateOutbox(muteList.showWords(words))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun requestDVMContentDiscovery(
|
suspend fun requestDVMContentDiscovery(
|
||||||
dvmPublicKey: User,
|
dvmPublicKey: User,
|
||||||
onReady: (event: NIP90ContentDiscoveryRequestEvent, relays: Set<NormalizedRelayUrl>) -> Unit,
|
onReady: (event: NIP90ContentDiscoveryRequestEvent, relays: Set<NormalizedRelayUrl>) -> Unit,
|
||||||
|
|||||||
+20
@@ -117,4 +117,24 @@ class BlockPeopleListState(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun showUsers(pubkeys: List<String>): PeopleListEvent? {
|
||||||
|
if (pubkeys.isEmpty()) return null
|
||||||
|
val blockList = getBlockList() ?: return null
|
||||||
|
return PeopleListEvent.removeAll(
|
||||||
|
earlierVersion = blockList,
|
||||||
|
persons = pubkeys.map { UserTag(it) },
|
||||||
|
signer = signer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun showWords(words: List<String>): PeopleListEvent? {
|
||||||
|
if (words.isEmpty()) return null
|
||||||
|
val blockList = getBlockList() ?: return null
|
||||||
|
return PeopleListEvent.removeAll(
|
||||||
|
earlierVersion = blockList,
|
||||||
|
persons = words.map { WordTag(it) },
|
||||||
|
signer = signer,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -139,6 +139,26 @@ class MuteListState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun showUsers(pubkeys: List<String>): MuteListEvent? {
|
||||||
|
if (pubkeys.isEmpty()) return null
|
||||||
|
val muteList = getMuteList() ?: return null
|
||||||
|
return MuteListEvent.removeAll(
|
||||||
|
earlierVersion = muteList,
|
||||||
|
mutes = pubkeys.map { UserTag(it) },
|
||||||
|
signer = signer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun showWords(words: List<String>): MuteListEvent? {
|
||||||
|
if (words.isEmpty()) return null
|
||||||
|
val muteList = getMuteList() ?: return null
|
||||||
|
return MuteListEvent.removeAll(
|
||||||
|
earlierVersion = muteList,
|
||||||
|
mutes = words.map { WordTag(it) },
|
||||||
|
signer = signer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
settings.backupMuteList?.let { event ->
|
settings.backupMuteList?.let { event ->
|
||||||
Log.d("AccountRegisterObservers") { "Loading saved mute list ${event.toJson()}" }
|
Log.d("AccountRegisterObservers") { "Loading saved mute list ${event.toJson()}" }
|
||||||
|
|||||||
+4
@@ -1173,6 +1173,10 @@ class AccountViewModel(
|
|||||||
|
|
||||||
fun showUser(pubkeyHex: String) = launchSigner { account.showUser(pubkeyHex) }
|
fun showUser(pubkeyHex: String) = launchSigner { account.showUser(pubkeyHex) }
|
||||||
|
|
||||||
|
fun showUsers(pubkeys: List<HexKey>) = launchSigner { account.showUsers(pubkeys) }
|
||||||
|
|
||||||
|
fun showWords(words: List<String>) = launchSigner { account.showWords(words) }
|
||||||
|
|
||||||
fun createStatus(newStatus: String) = launchSigner { account.createStatus(newStatus) }
|
fun createStatus(newStatus: String) = launchSigner { account.createStatus(newStatus) }
|
||||||
|
|
||||||
fun updateStatus(
|
fun updateStatus(
|
||||||
|
|||||||
+275
-12
@@ -20,20 +20,29 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
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.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -42,6 +51,7 @@ import androidx.compose.material3.SecondaryScrollableTabRow
|
|||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -60,6 +70,7 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
@@ -70,13 +81,26 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.WarningType
|
import com.vitorpamplona.amethyst.model.WarningType
|
||||||
import com.vitorpamplona.amethyst.model.parseWarningType
|
import com.vitorpamplona.amethyst.model.parseWarningType
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenWord
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenWord
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||||
|
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ShowUserButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.AddButton
|
import com.vitorpamplona.amethyst.ui.note.elements.AddButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
|
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.UserFeedState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenWordsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenWordsFeedViewModel
|
||||||
@@ -85,9 +109,12 @@ 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.ButtonPadding
|
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HorzPadding
|
import com.vitorpamplona.amethyst.ui.theme.HorzPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
@@ -129,7 +156,7 @@ fun SecurityFiltersScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SecurityFiltersScreen(
|
fun SecurityFiltersScreen(
|
||||||
hiddenFeedViewModel: HiddenAccountsFeedViewModel,
|
hiddenFeedViewModel: HiddenAccountsFeedViewModel,
|
||||||
@@ -155,9 +182,48 @@ fun SecurityFiltersScreen(
|
|||||||
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
|
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState { 3 }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var selectedUsers by remember { mutableStateOf(setOf<String>()) }
|
||||||
|
var selectedWords by remember { mutableStateOf(setOf<String>()) }
|
||||||
|
|
||||||
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
|
selectedUsers = emptySet()
|
||||||
|
selectedWords = emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectionMode = selectedUsers.isNotEmpty() || selectedWords.isNotEmpty()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
|
if (selectionMode) {
|
||||||
|
SelectionTopBar(
|
||||||
|
selectedCount = if (pagerState.currentPage == 2) selectedWords.size else selectedUsers.size,
|
||||||
|
onCancel = {
|
||||||
|
selectedUsers = emptySet()
|
||||||
|
selectedWords = emptySet()
|
||||||
|
},
|
||||||
|
onRemove = {
|
||||||
|
if (pagerState.currentPage == 2) {
|
||||||
|
if (!accountViewModel.isWriteable()) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_show_word,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
accountViewModel.showWords(selectedWords.toList())
|
||||||
|
selectedWords = emptySet()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
accountViewModel.showUsers(selectedUsers.toList())
|
||||||
|
selectedUsers = emptySet()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
TopBarWithBackButton(stringRes(id = R.string.security_filters), nav)
|
TopBarWithBackButton(stringRes(id = R.string.security_filters), nav)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -165,9 +231,6 @@ fun SecurityFiltersScreen(
|
|||||||
.padding(it)
|
.padding(it)
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState { 3 }
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
HeaderOptions(accountViewModel)
|
HeaderOptions(accountViewModel)
|
||||||
|
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
@@ -198,14 +261,77 @@ fun SecurityFiltersScreen(
|
|||||||
}
|
}
|
||||||
HorizontalPager(state = pagerState) { page ->
|
HorizontalPager(state = pagerState) { page ->
|
||||||
when (page) {
|
when (page) {
|
||||||
0 -> RefreshingFeedUserFeedView(hiddenFeedViewModel, accountViewModel, nav)
|
0 -> {
|
||||||
1 -> RefreshingFeedUserFeedView(spammerFeedViewModel, accountViewModel, nav)
|
SelectableHiddenUsersFeed(
|
||||||
2 -> HiddenWordsFeed(hiddenWordsViewModel, accountViewModel)
|
viewModel = hiddenFeedViewModel,
|
||||||
|
selected = selectedUsers,
|
||||||
|
onToggle = { hex ->
|
||||||
|
selectedUsers =
|
||||||
|
if (hex in selectedUsers) selectedUsers - hex else selectedUsers + hex
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
RefreshingFeedUserFeedView(spammerFeedViewModel, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
HiddenWordsFeed(
|
||||||
|
hiddenWordsViewModel = hiddenWordsViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
selected = selectedWords,
|
||||||
|
onToggle = { word ->
|
||||||
|
selectedWords =
|
||||||
|
if (word in selectedWords) selectedWords - word else selectedWords + word
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SelectionTopBar(
|
||||||
|
selectedCount: Int,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onRemove: () -> Unit,
|
||||||
|
) {
|
||||||
|
ShorterTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.num_selected, selectedCount),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
CloseButton(
|
||||||
|
modifier = HalfHorzPadding,
|
||||||
|
onPress = onCancel,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
Button(
|
||||||
|
modifier = HalfHorzPadding,
|
||||||
|
onClick = onRemove,
|
||||||
|
) {
|
||||||
|
Text(text = stringRes(R.string.unblock))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HeaderOptions(accountViewModel: AccountViewModel) {
|
private fun HeaderOptions(accountViewModel: AccountViewModel) {
|
||||||
@@ -302,14 +428,24 @@ private fun HeaderOptions(accountViewModel: AccountViewModel) {
|
|||||||
private fun HiddenWordsFeed(
|
private fun HiddenWordsFeed(
|
||||||
hiddenWordsViewModel: HiddenWordsFeedViewModel,
|
hiddenWordsViewModel: HiddenWordsFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
|
selected: Set<String>,
|
||||||
|
onToggle: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val selectionMode = selected.isNotEmpty()
|
||||||
RefresheableBox(hiddenWordsViewModel, false) {
|
RefresheableBox(hiddenWordsViewModel, false) {
|
||||||
StringFeedView(
|
StringFeedView(
|
||||||
hiddenWordsViewModel,
|
hiddenWordsViewModel,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
post = { AddMuteWordTextField(accountViewModel) },
|
post = { AddMuteWordTextField(accountViewModel) },
|
||||||
) {
|
) { word ->
|
||||||
MutedWordHeader(tag = it, account = accountViewModel)
|
MutedWordHeader(
|
||||||
|
tag = word,
|
||||||
|
account = accountViewModel,
|
||||||
|
isSelected = word in selected,
|
||||||
|
selectionMode = selectionMode,
|
||||||
|
onToggle = { onToggle(word) },
|
||||||
|
onLongClick = { onToggle(word) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,15 +508,32 @@ fun WatchAccountAndBlockList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MutedWordHeader(
|
fun MutedWordHeader(
|
||||||
tag: String,
|
tag: String,
|
||||||
modifier: Modifier = StdPadding,
|
modifier: Modifier = StdPadding,
|
||||||
account: AccountViewModel,
|
account: AccountViewModel,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
selectionMode: Boolean = false,
|
||||||
|
onToggle: () -> Unit = {},
|
||||||
|
onLongClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Column(
|
val rowModifier =
|
||||||
Modifier.fillMaxWidth(),
|
Modifier
|
||||||
) {
|
.fillMaxWidth()
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { if (selectionMode) onToggle() },
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
).let {
|
||||||
|
if (isSelected) {
|
||||||
|
it.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f))
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(rowModifier) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -392,11 +545,15 @@ fun MutedWordHeader(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (selectionMode) {
|
||||||
|
Checkbox(checked = isSelected, onCheckedChange = { onToggle() })
|
||||||
|
} else {
|
||||||
MutedWordActionOptions(tag, account)
|
MutedWordActionOptions(tag, account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MutedWordActionOptions(
|
fun MutedWordActionOptions(
|
||||||
@@ -479,3 +636,109 @@ private fun hideIfWritable(
|
|||||||
currentWordToAdd.value = ""
|
currentWordToAdd.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectableHiddenUsersFeed(
|
||||||
|
viewModel: UserFeedViewModel,
|
||||||
|
selected: Set<String>,
|
||||||
|
onToggle: (String) -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
RefresheableBox(viewModel, true) {
|
||||||
|
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
CrossfadeIfEnabled(
|
||||||
|
targetState = feedState,
|
||||||
|
animationSpec = tween(durationMillis = 100),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) { state ->
|
||||||
|
when (state) {
|
||||||
|
is UserFeedState.Empty -> {
|
||||||
|
FeedEmpty { viewModel.invalidateData() }
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserFeedState.FeedError -> {
|
||||||
|
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserFeedState.Loading -> {
|
||||||
|
LoadingFeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserFeedState.Loaded -> {
|
||||||
|
SelectableHiddenUsersList(
|
||||||
|
state = state,
|
||||||
|
selected = selected,
|
||||||
|
selectionMode = selectionMode,
|
||||||
|
onToggle = onToggle,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun SelectableHiddenUsersList(
|
||||||
|
state: UserFeedState.Loaded,
|
||||||
|
selected: Set<String>,
|
||||||
|
selectionMode: Boolean,
|
||||||
|
onToggle: (String) -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val items by state.feed.collectAsStateWithLifecycle()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||||
|
state = listState,
|
||||||
|
) {
|
||||||
|
itemsIndexed(items, key = { _, item -> item.pubkeyHex }) { _, user ->
|
||||||
|
val isSelected = user.pubkeyHex in selected
|
||||||
|
|
||||||
|
val rowModifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
if (selectionMode) {
|
||||||
|
onToggle(user.pubkeyHex)
|
||||||
|
} else {
|
||||||
|
nav.nav(routeFor(user))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { onToggle(user.pubkeyHex) },
|
||||||
|
).let {
|
||||||
|
if (isSelected) {
|
||||||
|
it.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f))
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = rowModifier.padding(horizontal = Size15dp, vertical = Size10dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
UserPicture(user, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
|
||||||
|
UsernameDisplay(user, accountViewModel = accountViewModel)
|
||||||
|
}
|
||||||
|
if (selectionMode) {
|
||||||
|
Checkbox(checked = isSelected, onCheckedChange = { onToggle(user.pubkeyHex) })
|
||||||
|
} else {
|
||||||
|
ShowUserButton { accountViewModel.show(user) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+18
@@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
|||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.removeAny
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -132,6 +133,23 @@ class MuteListEvent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun removeAll(
|
||||||
|
earlierVersion: MuteListEvent,
|
||||||
|
mutes: List<MuteTag>,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): MuteListEvent {
|
||||||
|
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||||
|
val ids = mutes.map { it.toTagIdOnly() }
|
||||||
|
|
||||||
|
return resign(
|
||||||
|
privateTags = privateTags.removeAny(ids),
|
||||||
|
publicTags = earlierVersion.tags.removeAny(ids),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun resign(
|
suspend fun resign(
|
||||||
publicTags: TagArray,
|
publicTags: TagArray,
|
||||||
privateTags: TagArray,
|
privateTags: TagArray,
|
||||||
|
|||||||
+18
@@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
|||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.removeAny
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
||||||
@@ -195,6 +196,23 @@ class PeopleListEvent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun removeAll(
|
||||||
|
earlierVersion: PeopleListEvent,
|
||||||
|
persons: List<MuteTag>,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): PeopleListEvent {
|
||||||
|
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||||
|
val ids = persons.map { it.toTagIdOnly() }
|
||||||
|
|
||||||
|
return resign(
|
||||||
|
privateTags = privateTags.removeAny(ids),
|
||||||
|
publicTags = earlierVersion.tags.removeAny(ids),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun remove(
|
suspend fun remove(
|
||||||
earlierVersion: PeopleListEvent,
|
earlierVersion: PeopleListEvent,
|
||||||
person: UserTag,
|
person: UserTag,
|
||||||
|
|||||||
Reference in New Issue
Block a user