diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index 4c3e8160c..bf15f6160 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -21,26 +21,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings import android.widget.Toast -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -51,26 +40,19 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -371,153 +353,3 @@ private fun ResetMarmotStateDialog( }, ) } - -@Composable -private fun SettingsSection( - title: Int, - isDanger: Boolean = false, - content: @Composable ColumnScope.() -> Unit, -) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text( - text = stringRes(title), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - color = - if (isDanger) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - }, - modifier = Modifier.padding(horizontal = 4.dp), - ) - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - ), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - ) { - Column(content = content) - } - } -} - -@Composable -private fun SettingsDivider() { - HorizontalDivider( - modifier = Modifier.padding(start = 68.dp), - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.outlineVariant, - ) -} - -@Composable -private fun SettingsItem( - title: Int, - icon: MaterialSymbol, - isDanger: Boolean = false, - onClick: () -> Unit, -) { - SettingsItemRow( - title = title, - isDanger = isDanger, - onClick = onClick, - leadingIcon = { tint -> - Icon( - symbol = icon, - contentDescription = stringRes(title), - modifier = Modifier.size(20.dp), - tint = tint, - ) - }, - ) -} - -@Composable -private fun SettingsItem( - title: Int, - iconPainter: Int, - iconPainterRef: Int, - isDanger: Boolean = false, - onClick: () -> Unit, -) { - val painter: Painter = painterRes(iconPainter, iconPainterRef) - SettingsItemRow( - title = title, - isDanger = isDanger, - onClick = onClick, - leadingIcon = { tint -> - Icon( - painter = painter, - contentDescription = stringRes(title), - modifier = Modifier.size(20.dp), - tint = tint, - ) - }, - ) -} - -@Composable -private fun SettingsItemRow( - title: Int, - isDanger: Boolean, - onClick: () -> Unit, - leadingIcon: @Composable (tint: Color) -> Unit, -) { - val containerColor = - if (isDanger) { - MaterialTheme.colorScheme.errorContainer - } else { - MaterialTheme.colorScheme.primaryContainer - } - val iconTint = - if (isDanger) { - MaterialTheme.colorScheme.onErrorContainer - } else { - MaterialTheme.colorScheme.onPrimaryContainer - } - val textColor = - if (isDanger) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.onSurface - } - - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = - Modifier - .size(36.dp) - .clip(RoundedCornerShape(10.dp)) - .background(containerColor), - contentAlignment = Alignment.Center, - ) { - leadingIcon(iconTint) - } - Text( - text = stringRes(title), - style = MaterialTheme.typography.bodyLarge, - color = textColor, - modifier = - Modifier - .weight(1f) - .padding(start = 16.dp), - ) - Icon( - symbol = MaterialSymbols.ChevronRight, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BlockedUsersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BlockedUsersScreen.kt index 05ccb6207..b9f2d672c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BlockedUsersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BlockedUsersScreen.kt @@ -20,47 +20,19 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth 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.material3.Checkbox -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor -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.screen.UserFeedState -import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.Size15dp -import com.vitorpamplona.amethyst.ui.theme.Size55dp @Composable fun BlockedUsersScreen( @@ -78,7 +50,7 @@ fun BlockedUsersScreen( topBar = { BlockListTopBar( title = R.string.blocked_users, - selected = selected, + selectedCount = selected.size, onCancel = { selected = emptySet() }, onUnblock = { accountViewModel.showUsers(selected.toList()) @@ -99,81 +71,3 @@ fun BlockedUsersScreen( ) } } - -@OptIn(ExperimentalFoundationApi::class) -@Composable -internal fun SelectableUserList( - modifier: Modifier = Modifier, - viewModel: UserFeedViewModel, - emptyMessage: Int, - selected: Set, - onToggle: ((String) -> Unit)? = null, - accountViewModel: AccountViewModel, - nav: INav, -) { - val feedState by viewModel.feedContent.collectAsStateWithLifecycle() - val selectionMode = selected.isNotEmpty() - - when (val state = feedState) { - is UserFeedState.Loaded -> { - 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 != null) { - onToggle(user.pubkeyHex) - } else { - nav.nav(routeFor(user)) - } - }, - onLongClick = { onToggle?.invoke(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?.invoke(user.pubkeyHex) }) - } else { - ShowUserButton { accountViewModel.show(user) } - } - } - HorizontalDivider(thickness = DividerThickness) - } - } - } - - else -> { - EmptyOrLoading(modifier = modifier, state = state, emptyMessage = emptyMessage) { - viewModel.invalidateData() - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt index 7608d79b3..ff64b1022 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt @@ -24,11 +24,14 @@ 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.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Checkbox @@ -79,10 +82,11 @@ fun HiddenWordsScreen( var selected by remember { mutableStateOf(setOf()) } Scaffold( + modifier = Modifier.imePadding(), topBar = { BlockListTopBar( title = R.string.hidden_words, - selected = selected, + selectedCount = selected.size, onCancel = { selected = emptySet() }, onUnblock = { if (!accountViewModel.isWriteable()) { @@ -109,7 +113,6 @@ fun HiddenWordsScreen( viewModel = viewModel, selected = selected, onToggle = { selected = if (it in selected) selected - it else selected + it }, - accountViewModel = accountViewModel, ) } } @@ -120,7 +123,6 @@ private fun HiddenWordsList( viewModel: HiddenWordsFeedViewModel, selected: Set, onToggle: (String) -> Unit, - accountViewModel: AccountViewModel, ) { val feedState by viewModel.feedContent.collectAsStateWithLifecycle() @@ -130,8 +132,12 @@ private fun HiddenWordsList( if (items.isEmpty()) { EmptyState(modifier, R.string.security_hidden_words_empty) } else { - Column(modifier = modifier.fillMaxSize()) { - items.forEach { word -> + val listState = rememberLazyListState() + LazyColumn( + modifier = modifier.fillMaxSize(), + state = listState, + ) { + items(items, key = { it }) { word -> MutedWordRow( tag = word, isSelected = word in selected, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 932efb399..a175ccf50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -21,24 +21,18 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton @@ -51,7 +45,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R @@ -110,19 +104,18 @@ private fun SensitiveContentTile(accountViewModel: AccountViewModel) { val showSensitive by accountViewModel.account.settings.syncedSettings.security.showSensitiveContent .collectAsStateWithLifecycle() - SettingsTile( - icon = MaterialSymbols.VisibilityOff, + SettingsBlockTile( + icon = MaterialSymbols.Visibility, title = stringRes(R.string.show_sensitive_content_title), description = stringRes(R.string.show_sensitive_content_explainer), ) { val current = parseWarningType(showSensitive) - val options = listOf(WarningType.WARN, WarningType.SHOW, WarningType.HIDE) SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - options.forEachIndexed { index, type -> + WarningType.entries.forEachIndexed { index, type -> SegmentedButton( selected = current == type, onClick = { accountViewModel.updateShowSensitiveContent(type.prefCode) }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), + shape = SegmentedButtonDefaults.itemShape(index = index, count = WarningType.entries.size), ) { Text(stringRes(type.resourceId)) } @@ -138,8 +131,8 @@ private fun FilterSpamTile(accountViewModel: AccountViewModel) { SwitchTile( icon = MaterialSymbols.FilterAlt, - title = stringRes(R.string.filter_spam_from_strangers_title), - description = stringRes(R.string.filter_spam_from_strangers_explainer), + titleRes = R.string.filter_spam_from_strangers_title, + descriptionRes = R.string.filter_spam_from_strangers_explainer, checked = filterSpam, onCheckedChange = { accountViewModel.updateFilterSpam(it) }, ) @@ -152,8 +145,8 @@ private fun HideCommunityViolationsTile(accountViewModel: AccountViewModel) { SwitchTile( icon = MaterialSymbols.Shield, - title = stringRes(R.string.hide_community_rules_violations_title), - description = stringRes(R.string.hide_community_rules_violations_explainer), + titleRes = R.string.hide_community_rules_violations_title, + descriptionRes = R.string.hide_community_rules_violations_explainer, checked = hideViolations, onCheckedChange = { accountViewModel.account.settings.changeHideCommunityRulesViolations(it) }, ) @@ -165,9 +158,9 @@ private fun DisableClientTagTile(accountViewModel: AccountViewModel) { .collectAsStateWithLifecycle() SwitchTile( - icon = MaterialSymbols.Tag, - title = stringRes(R.string.disable_client_tag_title), - description = stringRes(R.string.disable_client_tag_explainer), + icon = MaterialSymbols.Code, + titleRes = R.string.disable_client_tag_title, + descriptionRes = R.string.disable_client_tag_explainer, checked = disableClientTag, onCheckedChange = { accountViewModel.updateDisableClientTag(it) }, ) @@ -180,30 +173,25 @@ private fun WarnReportsTile(accountViewModel: AccountViewModel) { val threshold by accountViewModel.account.settings.syncedSettings.security.reportWarningThreshold .collectAsStateWithLifecycle() - Column { - SwitchTile( - icon = MaterialSymbols.Report, - title = stringRes(R.string.warn_when_posts_have_reports_from_your_follows_title), - description = stringRes(R.string.warn_when_posts_have_reports_from_your_follows_explainer), - checked = warnReports, - onCheckedChange = { accountViewModel.updateWarnReports(it) }, - ) - - SettingsTile( - icon = null, - title = stringRes(R.string.report_warning_threshold_title), - description = stringRes(R.string.report_warning_threshold_explainer), - indented = true, + SwitchTile( + icon = MaterialSymbols.Report, + titleRes = R.string.warn_when_posts_have_reports_from_your_follows_title, + descriptionRes = R.string.warn_when_posts_have_reports_from_your_follows_explainer, + checked = warnReports, + onCheckedChange = { accountViewModel.updateWarnReports(it) }, + ) + SettingsSubControlRow( + title = stringRes(R.string.report_warning_threshold_title), + description = stringRes(R.string.report_warning_threshold_explainer), + enabled = warnReports, + ) { + Stepper( + value = threshold.coerceAtLeast(1), + min = 1, + max = 999, enabled = warnReports, - ) { - Stepper( - value = threshold.coerceAtLeast(1), - min = 1, - max = 999, - enabled = warnReports, - onValueChange = { accountViewModel.updateReportWarningThreshold(it) }, - ) - } + onValueChange = { accountViewModel.updateReportWarningThreshold(it) }, + ) } } @@ -212,7 +200,7 @@ private fun MaxHashtagsTile(accountViewModel: AccountViewModel) { val maxHashtags by accountViewModel.account.settings.syncedSettings.security.maxHashtagLimit .collectAsStateWithLifecycle() - SettingsTile( + SettingsControlRow( icon = MaterialSymbols.Tag, title = stringRes(R.string.max_hashtag_limit_title), description = stringRes(R.string.max_hashtag_limit_explainer), @@ -227,6 +215,24 @@ private fun MaxHashtagsTile(accountViewModel: AccountViewModel) { } } +@Composable +private fun SwitchTile( + icon: MaterialSymbol, + titleRes: Int, + descriptionRes: Int, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + SettingsControlRow( + icon = icon, + title = stringRes(titleRes), + description = stringRes(descriptionRes), + onClick = { onCheckedChange(!checked) }, + ) { + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} + @Composable private fun BlockedContentSection( accountViewModel: AccountViewModel, @@ -236,224 +242,58 @@ private fun BlockedContentSection( .collectAsStateWithLifecycle() SettingsSection(R.string.security_section_blocked_content) { - BlockedContentRow( + SettingsItem( + title = R.string.blocked_users, icon = MaterialSymbols.PersonOff, - title = stringRes(R.string.blocked_users), - count = hidden.hiddenUsers.size, + trailing = { CountBadge(hidden.hiddenUsers.size) }, onClick = { nav.nav(Route.BlockedUsers) }, ) SettingsDivider() - BlockedContentRow( + SettingsItem( + title = R.string.spamming_users, icon = MaterialSymbols.Block, - title = stringRes(R.string.spamming_users), - count = hidden.spammers.size, + trailing = { CountBadge(hidden.spammers.size) }, onClick = { nav.nav(Route.SpammingUsers) }, ) SettingsDivider() - BlockedContentRow( - icon = MaterialSymbols.Tag, - title = stringRes(R.string.hidden_words), - count = hidden.hiddenWords.size, + SettingsItem( + title = R.string.hidden_words, + icon = MaterialSymbols.VisibilityOff, + trailing = { CountBadge(hidden.hiddenWords.size) }, onClick = { nav.nav(Route.HiddenWords) }, ) SettingsDivider() - BlockedContentRow( + SettingsItem( + title = R.string.settings_muted_threads_title, icon = MaterialSymbols.Forum, - title = stringRes(R.string.settings_muted_threads_title), - count = hidden.mutedThreads.size, + trailing = { CountBadge(hidden.mutedThreads.size) }, onClick = { nav.nav(Route.MutedThreads) }, ) } } -// ────────────────────────────────────────────────────────────────────────────── -// Building blocks -// ────────────────────────────────────────────────────────────────────────────── - -@Composable -private fun SettingsSection( - title: Int, - content: @Composable ColumnScope.() -> Unit, -) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text( - text = stringRes(title), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 4.dp), - ) - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - ), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - ) { - Column(content = content) - } - } -} - -@Composable -private fun SettingsDivider() { - HorizontalDivider( - modifier = Modifier.padding(start = 68.dp), - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.outlineVariant, - ) -} - -@Composable -private fun SettingsTile( - icon: MaterialSymbol?, - title: String, - description: String, - indented: Boolean = false, - enabled: Boolean = true, - trailing: @Composable () -> Unit, -) { - val alpha = if (enabled) 1f else 0.5f - Row( - modifier = - Modifier - .fillMaxWidth() - .padding( - start = if (indented) 56.dp else 16.dp, - end = 16.dp, - top = 12.dp, - bottom = 12.dp, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - if (icon != null) { - SettingsLeadingIcon(icon) - } - Column( - modifier = - Modifier - .weight(1f) - .padding(start = if (icon != null) 16.dp else 0.dp, end = 12.dp), - ) { - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha), - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha), - ) - } - Box(contentAlignment = Alignment.Center) { trailing() } - } -} - -@Composable -private fun SwitchTile( - icon: MaterialSymbol, - title: String, - description: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, -) { - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { onCheckedChange(!checked) } - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - SettingsLeadingIcon(icon) - Column( - modifier = - Modifier - .weight(1f) - .padding(start = 16.dp, end = 12.dp), - ) { - Text(text = title, style = MaterialTheme.typography.bodyLarge) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch(checked = checked, onCheckedChange = onCheckedChange) - } -} - -@Composable -private fun SettingsLeadingIcon(icon: MaterialSymbol) { - Box( - modifier = - Modifier - .size(36.dp) - .clip(RoundedCornerShape(10.dp)) - .background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center, - ) { - Icon( - symbol = icon, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } -} - -@Composable -private fun BlockedContentRow( - icon: MaterialSymbol, - title: String, - count: Int, - onClick: () -> Unit, -) { - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - SettingsLeadingIcon(icon) - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, - modifier = - Modifier - .weight(1f) - .padding(start = 16.dp), - ) - CountBadge(count) - Icon( - symbol = MaterialSymbols.ChevronRight, - contentDescription = null, - modifier = Modifier.padding(start = 8.dp).size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } -} - @Composable private fun CountBadge(count: Int) { - if (count <= 0) return + // Reserve space so transitions to/from 0 don't shift the row. Box( - modifier = - Modifier - .clip(RoundedCornerShape(50)) - .background(MaterialTheme.colorScheme.secondaryContainer) - .padding(horizontal = 10.dp, vertical = 2.dp), + modifier = Modifier.widthIn(min = 28.dp), + contentAlignment = Alignment.Center, ) { - Text( - text = count.toString(), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer, - ) + if (count > 0) { + Box( + modifier = + Modifier + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.secondaryContainer) + .padding(horizontal = 10.dp, vertical = 2.dp), + ) { + Text( + text = count.toString(), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } + } } } @@ -466,13 +306,11 @@ private fun Stepper( unsetLabel: String? = null, onValueChange: (Int) -> Unit, ) { - val alpha = if (enabled) 1f else 0.5f Row( modifier = Modifier - .height(40.dp) .clip(RoundedCornerShape(50)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = alpha)), + .background(MaterialTheme.colorScheme.surfaceContainerHigh), verticalAlignment = Alignment.CenterVertically, ) { IconButton( @@ -483,20 +321,13 @@ private fun Stepper( symbol = MaterialSymbols.Remove, contentDescription = "−", modifier = Modifier.size(18.dp), - tint = LocalContentColor.current.copy(alpha = alpha), ) } - val display = - if (unsetLabel != null && value <= 0) { - unsetLabel - } else { - value.toString() - } Text( - text = display, + text = if (unsetLabel != null && value <= 0) unsetLabel else value.toString(), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha), - modifier = Modifier.padding(horizontal = 4.dp), + textAlign = TextAlign.Center, + modifier = Modifier.widthIn(min = 28.dp), ) IconButton( enabled = enabled && value < max, @@ -506,7 +337,6 @@ private fun Stepper( symbol = MaterialSymbols.Add, contentDescription = "+", modifier = Modifier.size(18.dp), - tint = LocalContentColor.current.copy(alpha = alpha), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt index 87e21ae61..2fba72afd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt @@ -20,14 +20,23 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings +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.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -45,14 +54,25 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed +import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding 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.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.screen.UserFeedState +import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +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.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size15dp +import com.vitorpamplona.amethyst.ui.theme.Size55dp @Composable internal fun InvalidateOnBlockListChange( @@ -73,18 +93,18 @@ internal fun InvalidateOnBlockListChange( @Composable internal fun BlockListTopBar( title: Int, - selected: Set<*>, + selectedCount: Int, onCancel: () -> Unit, onUnblock: () -> Unit, nav: INav, ) { - if (selected.isEmpty()) { + if (selectedCount == 0) { TopBarWithBackButton(stringRes(id = title), nav) } else { ShorterTopAppBar( title = { Text( - text = stringRes(R.string.num_selected, selected.size), + text = stringRes(R.string.num_selected, selectedCount), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Start, overflow = TextOverflow.Ellipsis, @@ -107,18 +127,87 @@ internal fun BlockListTopBar( } } +@OptIn(ExperimentalFoundationApi::class) @Composable -internal fun EmptyOrLoading( +internal fun SelectableUserList( modifier: Modifier = Modifier, - state: UserFeedState, + viewModel: UserFeedViewModel, emptyMessage: Int, - onRefresh: () -> Unit, + selected: Set, + onToggle: (String) -> Unit, + accountViewModel: AccountViewModel, + nav: INav, ) { - when (state) { - is UserFeedState.Empty -> EmptyState(modifier, emptyMessage) - is UserFeedState.Loading -> LoadingFeed() - is UserFeedState.FeedError -> FeedError(state.errorMessage, onRefresh) - else -> Unit + val feedState by viewModel.feedContent.collectAsStateWithLifecycle() + val selectionMode = selected.isNotEmpty() + + when (val state = feedState) { + is UserFeedState.Loaded -> { + val items by state.feed.collectAsStateWithLifecycle() + val listState = rememberLazyListState() + + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = rememberFeedContentPadding(FeedPadding), + state = listState, + ) { + items(items, key = { it.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) + } + } + } + + is UserFeedState.Empty -> { + EmptyState(modifier, emptyMessage) + } + + is UserFeedState.Loading -> { + LoadingFeed() + } + + is UserFeedState.FeedError -> { + FeedError(state.errorMessage) { viewModel.invalidateData() } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt new file mode 100644 index 000000000..7da0f5410 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.painterRes +import com.vitorpamplona.amethyst.ui.stringRes +import androidx.compose.material3.Icon as Material3Icon + +@Composable +internal fun SettingsSection( + title: Int, + isDanger: Boolean = false, + content: @Composable ColumnScope.() -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringRes(title), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = + if (isDanger) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + }, + modifier = Modifier.padding(horizontal = 4.dp), + ) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + ), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + ) { + Column(content = content) + } + } +} + +@Composable +internal fun SettingsDivider() { + HorizontalDivider( + modifier = Modifier.padding(start = 68.dp), + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.outlineVariant, + ) +} + +/** Navigation row: icon + title + optional `trailing` (count badge, etc.) + chevron. */ +@Composable +internal fun SettingsItem( + title: Int, + icon: MaterialSymbol, + isDanger: Boolean = false, + trailing: @Composable () -> Unit = {}, + onClick: () -> Unit, +) { + SettingsItemRow( + title = title, + isDanger = isDanger, + trailing = trailing, + onClick = onClick, + ) { tint -> + Icon( + symbol = icon, + contentDescription = stringRes(title), + modifier = Modifier.size(20.dp), + tint = tint, + ) + } +} + +/** Navigation row variant taking a drawable painter as the leading icon. */ +@Composable +internal fun SettingsItem( + title: Int, + iconPainter: Int, + iconPainterRef: Int, + isDanger: Boolean = false, + trailing: @Composable () -> Unit = {}, + onClick: () -> Unit, +) { + val painter: Painter = painterRes(iconPainter, iconPainterRef) + SettingsItemRow( + title = title, + isDanger = isDanger, + trailing = trailing, + onClick = onClick, + ) { tint -> + Material3Icon( + painter = painter, + contentDescription = stringRes(title), + modifier = Modifier.size(20.dp), + tint = tint, + ) + } +} + +@Composable +private fun SettingsItemRow( + title: Int, + isDanger: Boolean, + trailing: @Composable () -> Unit, + onClick: () -> Unit, + leadingIcon: @Composable (tint: Color) -> Unit, +) { + val containerColor = + if (isDanger) { + MaterialTheme.colorScheme.errorContainer + } else { + MaterialTheme.colorScheme.primaryContainer + } + val iconTint = + if (isDanger) { + MaterialTheme.colorScheme.onErrorContainer + } else { + MaterialTheme.colorScheme.onPrimaryContainer + } + val textColor = + if (isDanger) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurface + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SettingsIconBox(containerColor) { leadingIcon(iconTint) } + Text( + text = stringRes(title), + style = MaterialTheme.typography.bodyLarge, + color = textColor, + modifier = + Modifier + .weight(1f) + .padding(start = 16.dp), + ) + trailing() + Icon( + symbol = MaterialSymbols.ChevronRight, + contentDescription = null, + modifier = Modifier.padding(start = 8.dp).size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +/** + * Control row: icon + title + description, with an inline `trailing` control (switch, stepper). + * Clicking anywhere on the row invokes [onClick] — pass `null` to disable row clicks + * (useful when the control alone should toggle state). + */ +@Composable +internal fun SettingsControlRow( + icon: MaterialSymbol, + title: String, + description: String, + enabled: Boolean = true, + onClick: (() -> Unit)? = null, + trailing: @Composable () -> Unit, +) { + val rowModifier = + Modifier + .fillMaxWidth() + .let { if (onClick != null && enabled) it.clickable(onClick = onClick) else it } + .padding(horizontal = 16.dp, vertical = 12.dp) + val alpha = if (enabled) 1f else 0.5f + + Row( + modifier = rowModifier, + verticalAlignment = Alignment.CenterVertically, + ) { + SettingsIconBox(MaterialTheme.colorScheme.primaryContainer) { + Icon( + symbol = icon, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = alpha), + ) + } + Column( + modifier = + Modifier + .weight(1f) + .padding(start = 16.dp, end = 12.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha), + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha), + ) + } + trailing() + } +} + +/** + * Sub-row variant of [SettingsControlRow]: indented in place of a leading icon, + * used for controls hierarchically grouped under the row above (e.g. a threshold + * that only matters when the parent toggle is on). + */ +@Composable +internal fun SettingsSubControlRow( + title: String, + description: String, + enabled: Boolean = true, + trailing: @Composable () -> Unit, +) { + val alpha = if (enabled) 1f else 0.5f + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 68.dp, end = 16.dp, top = 8.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f).padding(end = 12.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha), + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha), + ) + } + trailing() + } +} + +/** + * Tile where the control sits below the title + description (e.g. a full-width + * SegmentedButtonRow). Use when the control would crowd a trailing slot. + */ +@Composable +internal fun SettingsBlockTile( + icon: MaterialSymbol, + title: String, + description: String, + content: @Composable () -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + SettingsIconBox(MaterialTheme.colorScheme.primaryContainer) { + Icon( + symbol = icon, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + Column( + modifier = + Modifier + .weight(1f) + .padding(start = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + content() + } + } +} + +@Composable +private fun SettingsIconBox( + containerColor: Color, + content: @Composable () -> Unit, +) { + Box( + modifier = + Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(containerColor), + contentAlignment = Alignment.Center, + content = { content() }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SpammingUsersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SpammingUsersScreen.kt index c00c79b6c..fc4095298 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SpammingUsersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SpammingUsersScreen.kt @@ -50,7 +50,7 @@ fun SpammingUsersScreen( topBar = { BlockListTopBar( title = R.string.spamming_users, - selected = selected, + selectedCount = selected.size, onCancel = { selected = emptySet() }, onUnblock = { accountViewModel.showUsers(selected.toList())