fix(ui): thread scaffold padding through Messages, DVMs, Search

Audit follow-up to the previous padding migration: four more
`DisappearingScaffold` consumers were still using the old
`HorizontalPager(contentPadding = it)` or `Modifier.padding(it)`
patterns, so their feeds didn't extend behind the chrome.

- Messages single-pane: `MessagesPager` no longer applies the scaffold
  padding to the pager's `contentPadding` (which shrinks pages); it
  threads it down to `ChatroomListFeedView` instead, which now accepts
  `scaffoldPadding` and applies it on its inner `LazyColumn` via
  `rememberMergedPadding(scaffoldPadding, FeedPadding)`.
- Messages two-pane: drops the outer `Modifier.padding(padding)
  .consumeWindowInsets(padding)` on the `TwoPane` and threads the
  padding through `ChatroomList` → `MessagesPager` →
  `ChatroomListFeedView`.
- `DvmContentDiscoveryScreen`: drops the wrapping `Column(Modifier
  .padding(paddingValues))` and threads the scaffold padding through
  `DvmContentDiscoveryScreen` → `ObserverContentDiscoveryResponse` →
  `PrepareViewContentDiscoveryModels` → `RenderNostrNIP90Content
  DiscoveryScreen` → `RenderFeedState`'s default `FeedLoaded`.
- `SearchScreen`: drops the wrapping
  `Column(Modifier.padding(it).consumeWindowInsets(it))` and turns the
  inbox-relay warning card into a `headerContent` slot rendered as the
  first item of the search-results `LazyColumn`. The list now also
  always exists (with the early `return` moved into the lazy-list
  builder via `return@LazyColumn`) so the header is visible even when
  no search is in progress.

Settings (non-scrollable) and chat detail screens (inverted layout
plus a custom input field at the bottom — these need a more careful
refactor to handle IME + nav inset together) intentionally left
unchanged.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
This commit is contained in:
Claude
2026-04-19 14:59:57 +00:00
parent 53cec9a661
commit 9b409723bd
6 changed files with 67 additions and 53 deletions
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
@@ -30,6 +31,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
@@ -39,6 +41,7 @@ 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.SaveableFeedContentState
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose
@@ -49,12 +52,13 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding
fun ChatroomListFeedView(
feedContentState: FeedContentState,
scrollStateKey: String,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
RefresheableBox(feedContentState, true) {
SaveableFeedContentState(feedContentState, scrollStateKey) { listState ->
CrossFadeState(feedContentState, listState, accountViewModel, nav)
CrossFadeState(feedContentState, listState, scaffoldPadding, accountViewModel, nav)
}
}
}
@@ -63,6 +67,7 @@ fun ChatroomListFeedView(
private fun CrossFadeState(
feedContentState: FeedContentState,
listState: LazyListState,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -83,7 +88,7 @@ private fun CrossFadeState(
}
is FeedState.Loaded -> {
FeedLoaded(state, listState, accountViewModel, nav)
FeedLoaded(state, listState, scaffoldPadding, accountViewModel, nav)
}
FeedState.Loading -> {
@@ -97,13 +102,14 @@ private fun CrossFadeState(
private fun FeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = FeedPadding,
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -127,7 +127,6 @@ fun MessagesPager(
nav: INav,
) {
HorizontalPager(
contentPadding = paddingValues,
state = pagerState,
userScrollEnabled = true,
modifier =
@@ -139,6 +138,7 @@ fun MessagesPager(
ChatroomListFeedView(
feedContentState = tabs[page].feedContentState,
scrollStateKey = tabs[page].scrollStateKey,
scaffoldPadding = paddingValues,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -44,6 +44,7 @@ import kotlinx.collections.immutable.persistentListOf
fun ChatroomList(
knownFeedContentState: FeedContentState,
newFeedContentState: FeedContentState,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -75,7 +76,7 @@ fun ChatroomList(
MessagesPager(
pagerState,
tabs,
PaddingValues(0.dp),
scaffoldPadding,
accountViewModel,
nav,
)
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
@@ -97,6 +96,7 @@ fun MessagesTwoPane(
ChatroomList(
knownFeedContentState,
newFeedContentState,
padding,
accountViewModel,
twoPaneNav,
)
@@ -134,7 +134,7 @@ fun MessagesTwoPane(
strategy = strategy,
displayFeatures = displayFeatures,
foldAwareConfiguration = FoldAwareConfiguration.VerticalFoldsOnly,
modifier = Modifier.padding(padding).consumeWindowInsets(padding).fillMaxSize(),
modifier = Modifier.fillMaxSize(),
)
}
}
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
@@ -97,20 +97,18 @@ fun DvmContentDiscoveryScreen(
},
accountViewModel = accountViewModel,
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { note ->
note?.let { baseNote ->
WatchNoteEvent(
baseNote,
onNoteEventFound = {
DvmContentDiscoveryScreen(baseNote, accountViewModel, nav)
},
onBlank = {
FeedEmptyWithStatus(baseNote, stringRes(R.string.dvm_looking_for_app), accountViewModel, nav)
},
accountViewModel,
)
}
LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { note ->
note?.let { baseNote ->
WatchNoteEvent(
baseNote,
onNoteEventFound = {
DvmContentDiscoveryScreen(baseNote, paddingValues, accountViewModel, nav)
},
onBlank = {
FeedEmptyWithStatus(baseNote, stringRes(R.string.dvm_looking_for_app), accountViewModel, nav)
},
accountViewModel,
)
}
}
}
@@ -119,6 +117,7 @@ fun DvmContentDiscoveryScreen(
@Composable
fun DvmContentDiscoveryScreen(
appDefinition: Note,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -153,6 +152,7 @@ fun DvmContentDiscoveryScreen(
appDefinition,
myRequestEventID,
onRefresh,
scaffoldPadding,
accountViewModel,
nav,
)
@@ -169,6 +169,7 @@ fun ObserverContentDiscoveryResponse(
appDefinition: Note,
dvmRequestId: Note,
onRefresh: () -> Unit,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -197,6 +198,7 @@ fun ObserverContentDiscoveryResponse(
dvmRequestId.idHex,
myResponse,
onRefresh,
scaffoldPadding,
accountViewModel,
nav,
)
@@ -249,6 +251,7 @@ fun PrepareViewContentDiscoveryModels(
dvmRequestId: String,
latestResponse: NIP90ContentDiscoveryResponseEvent,
onRefresh: () -> Unit,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -262,32 +265,32 @@ fun PrepareViewContentDiscoveryModels(
resultFeedViewModel.invalidateData()
}
RenderNostrNIP90ContentDiscoveryScreen(resultFeedViewModel, onRefresh, accountViewModel, nav)
RenderNostrNIP90ContentDiscoveryScreen(resultFeedViewModel, onRefresh, scaffoldPadding, accountViewModel, nav)
}
@Composable
fun RenderNostrNIP90ContentDiscoveryScreen(
resultFeedViewModel: NIP90ContentDiscoveryFeedViewModel,
onRefresh: () -> Unit,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
Column(Modifier.fillMaxHeight()) {
SaveableFeedState(resultFeedViewModel.feedState, null) { listState ->
// TODO (Optional) Instead of a like reaction, do a Kind 31989 NIP89 App recommendation
RenderFeedState(
resultFeedViewModel,
accountViewModel,
listState,
nav,
null,
onEmpty = {
FeedEmpty {
onRefresh()
}
},
)
}
SaveableFeedState(resultFeedViewModel.feedState, null) { listState ->
// TODO (Optional) Instead of a like reaction, do a Kind 31989 NIP89 App recommendation
RenderFeedState(
resultFeedViewModel,
accountViewModel,
listState,
nav,
null,
scaffoldPadding,
onEmpty = {
FeedEmpty {
onRefresh()
}
},
)
}
}
@@ -22,9 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.search
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
@@ -59,6 +58,7 @@ import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -123,13 +123,14 @@ fun SearchScreen(
}
},
accountViewModel = accountViewModel,
) {
Column(
modifier = Modifier.padding(it).consumeWindowInsets(it),
) {
ObserveRelayListForSearchAndDisplayIfNotFound(accountViewModel, nav)
DisplaySearchResults(searchBarViewModel, nav, accountViewModel)
}
) { scaffoldPadding ->
DisplaySearchResults(
searchBarViewModel = searchBarViewModel,
scaffoldPadding = scaffoldPadding,
header = { ObserveRelayListForSearchAndDisplayIfNotFound(accountViewModel, nav) },
nav = nav,
accountViewModel = accountViewModel,
)
}
}
@@ -219,13 +220,12 @@ private fun SearchTextField(
@Composable
private fun DisplaySearchResults(
searchBarViewModel: SearchBarViewModel,
scaffoldPadding: PaddingValues,
header: @Composable () -> Unit,
nav: INav,
accountViewModel: AccountViewModel,
) {
if (!searchBarViewModel.isRefreshing.value) {
return
}
val isRefreshing by searchBarViewModel.isRefreshing
val hashTags by searchBarViewModel.hashtagResults.collectAsStateWithLifecycle()
val relays by searchBarViewModel.relayResults.collectAsStateWithLifecycle()
val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle()
@@ -236,9 +236,13 @@ private fun DisplaySearchResults(
LazyColumn(
modifier = Modifier.fillMaxHeight(),
contentPadding = FeedPadding,
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
state = searchBarViewModel.listState,
) {
item(key = "scaffold-header") { header() }
if (!isRefreshing) return@LazyColumn
itemsIndexed(
hashTags,
key = { _, item -> "#$item" },