fix: pad Loading/Error states and tighten Stepper resync
- Wrap the when-branches in SelectableUserList, HiddenWordsList, and MutedThreadsList in Box(modifier.fillMaxSize()) so the Scaffold padding is applied to every state — Loading/Error/Empty/Loaded — not just the LazyColumn. Fixes a regression where the loading spinner and error UI rendered under the top bar. - SettingsStepper now clamps `value` once into `[min, max]` and uses the raw `value` (re-clamped) in the +/- handlers. If the model starts below `min`, the first tap of `+` resyncs it to `min` instead of jumping `min+1` (skipping a step). Display still falls back to `unsetLabel` when `value <= 0`. - WarnReportsTile no longer pre-coerces threshold to >= 1 at the call site; the stepper handles it. - EmptyState drops its now-unused modifier parameter.
This commit is contained in:
+31
-28
@@ -24,6 +24,7 @@ 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.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -126,40 +127,42 @@ private fun HiddenWordsList(
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
when (val state = feedState) {
|
||||
is StringFeedState.Loaded -> {
|
||||
val items by state.feed.collectAsStateWithLifecycle()
|
||||
if (items.isEmpty()) {
|
||||
EmptyState(modifier, R.string.security_hidden_words_empty)
|
||||
} else {
|
||||
val listState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
) {
|
||||
items(items, key = { it }) { word ->
|
||||
MutedWordRow(
|
||||
tag = word,
|
||||
isSelected = word in selected,
|
||||
selectionMode = selected.isNotEmpty(),
|
||||
onToggle = { onToggle(word) },
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
Box(modifier.fillMaxSize()) {
|
||||
when (val state = feedState) {
|
||||
is StringFeedState.Loaded -> {
|
||||
val items by state.feed.collectAsStateWithLifecycle()
|
||||
if (items.isEmpty()) {
|
||||
EmptyState(R.string.security_hidden_words_empty)
|
||||
} else {
|
||||
val listState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
) {
|
||||
items(items, key = { it }) { word ->
|
||||
MutedWordRow(
|
||||
tag = word,
|
||||
isSelected = word in selected,
|
||||
selectionMode = selected.isNotEmpty(),
|
||||
onToggle = { onToggle(word) },
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is StringFeedState.Empty -> {
|
||||
EmptyState(modifier, R.string.security_hidden_words_empty)
|
||||
}
|
||||
is StringFeedState.Empty -> {
|
||||
EmptyState(R.string.security_hidden_words_empty)
|
||||
}
|
||||
|
||||
is StringFeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
is StringFeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
|
||||
is StringFeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
is StringFeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-21
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -90,32 +91,34 @@ private fun MutedThreadsList(
|
||||
) {
|
||||
val feedState by viewModel.feedState.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
when (val state = feedState) {
|
||||
is FeedState.Loaded -> {
|
||||
val items by state.feed.collectAsStateWithLifecycle()
|
||||
val listState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
state = listState,
|
||||
) {
|
||||
items(items.list, key = { it.idHex }) { note ->
|
||||
MutedThreadRow(note = note, accountViewModel = accountViewModel)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
Box(modifier.fillMaxSize()) {
|
||||
when (val state = feedState) {
|
||||
is FeedState.Loaded -> {
|
||||
val items by state.feed.collectAsStateWithLifecycle()
|
||||
val listState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
state = listState,
|
||||
) {
|
||||
items(items.list, key = { it.idHex }) { note ->
|
||||
MutedThreadRow(note = note, accountViewModel = accountViewModel)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is FeedState.Empty -> {
|
||||
EmptyState(modifier, R.string.settings_muted_threads_empty)
|
||||
}
|
||||
is FeedState.Empty -> {
|
||||
EmptyState(R.string.settings_muted_threads_empty)
|
||||
}
|
||||
|
||||
is FeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
is FeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
|
||||
is FeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
is FeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ private fun WarnReportsTile(accountViewModel: AccountViewModel) {
|
||||
enabled = warnReports,
|
||||
) {
|
||||
SettingsStepper(
|
||||
value = threshold.coerceAtLeast(1),
|
||||
value = threshold,
|
||||
min = 1,
|
||||
max = 999,
|
||||
enabled = warnReports,
|
||||
|
||||
+62
-58
@@ -25,6 +25,7 @@ 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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -141,76 +142,80 @@ internal fun SelectableUserList(
|
||||
nav: INav,
|
||||
enablePullRefresh: Boolean = true,
|
||||
) {
|
||||
RefresheableBox(viewModel, enablePullRefresh) {
|
||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
||||
val selectionMode = selected.isNotEmpty()
|
||||
// Outer Box applies caller padding (Scaffold insets) so the Loading/Error
|
||||
// states inside RefresheableBox don't render under the top bar.
|
||||
Box(modifier.fillMaxSize()) {
|
||||
RefresheableBox(viewModel, enablePullRefresh) {
|
||||
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()
|
||||
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)
|
||||
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 {
|
||||
nav.nav(routeFor(user))
|
||||
it
|
||||
}
|
||||
},
|
||||
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),
|
||||
Row(
|
||||
modifier = rowModifier.padding(horizontal = Size15dp, vertical = Size10dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
UsernameDisplay(user, accountViewModel = accountViewModel)
|
||||
}
|
||||
if (selectionMode) {
|
||||
Checkbox(checked = isSelected, onCheckedChange = { onToggle(user.pubkeyHex) })
|
||||
} else {
|
||||
ShowUserButton { accountViewModel.show(user) }
|
||||
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)
|
||||
}
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is UserFeedState.Empty -> {
|
||||
EmptyState(modifier, emptyMessage)
|
||||
}
|
||||
is UserFeedState.Empty -> {
|
||||
EmptyState(emptyMessage)
|
||||
}
|
||||
|
||||
is UserFeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
is UserFeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
|
||||
is UserFeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
is UserFeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,11 +223,10 @@ internal fun SelectableUserList(
|
||||
|
||||
@Composable
|
||||
internal fun EmptyState(
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes message: Int,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize().padding(32.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
|
||||
+13
-6
@@ -352,7 +352,13 @@ internal fun SettingsCountBadge(count: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Numeric stepper: `-` value `+`. [unsetLabel] is shown when value <= 0 (e.g. "∞"). */
|
||||
/**
|
||||
* Numeric stepper: `-` value `+`. Display is clamped to `[min, max]`, and `-`/`+`
|
||||
* operate on the raw `value` then re-clamp, so a model that starts out below `min`
|
||||
* lifts to `min` on the first tap of `+` (no skipped step). [unsetLabel] is shown
|
||||
* when `value <= 0` (typically the model uses `0` to mean "unlimited"); pass
|
||||
* `null` to always show the number.
|
||||
*/
|
||||
@Composable
|
||||
internal fun SettingsStepper(
|
||||
value: Int,
|
||||
@@ -362,6 +368,7 @@ internal fun SettingsStepper(
|
||||
unsetLabel: String? = null,
|
||||
onValueChange: (Int) -> Unit,
|
||||
) {
|
||||
val clamped = value.coerceIn(min, max)
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -370,8 +377,8 @@ internal fun SettingsStepper(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
enabled = enabled && value > min,
|
||||
onClick = { onValueChange((value - 1).coerceAtLeast(min)) },
|
||||
enabled = enabled && clamped > min,
|
||||
onClick = { onValueChange((value - 1).coerceIn(min, max)) },
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Remove,
|
||||
@@ -380,14 +387,14 @@ internal fun SettingsStepper(
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = if (unsetLabel != null && value <= 0) unsetLabel else value.toString(),
|
||||
text = if (unsetLabel != null && value <= 0) unsetLabel else clamped.toString(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.widthIn(min = 40.dp),
|
||||
)
|
||||
IconButton(
|
||||
enabled = enabled && value < max,
|
||||
onClick = { onValueChange((value + 1).coerceAtMost(max)) },
|
||||
enabled = enabled && clamped < max,
|
||||
onClick = { onValueChange((value + 1).coerceIn(min, max)) },
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Add,
|
||||
|
||||
Reference in New Issue
Block a user