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