refactor(ui): CompositionLocal for scaffold padding + scaffold hygiene

Follow-up polish to the disappearing-scaffold migration addressing
the review items:

1. Replace the `scaffoldPadding: PaddingValues` sprawl across ~15 feed
   entry points with a single `LocalDisappearingScaffoldPadding`
   CompositionLocal published by the scaffold, plus a
   `rememberFeedContentPadding(inner)` helper that merges it with the
   list's own baseline padding. The contract becomes implicit at the
   call site ("inner LazyColumn uses rememberFeedContentPadding") and
   there's no more optional parameter for a future dev to forget.
   `rememberMergedPadding` stays available for manual use.

2. Fix the stale-closure bug in `DisappearingScaffold`: the NSC is
   `remember`'d and keeps its captured `canScroll` lambda across
   recompositions, but the lambda was reading `allowBarHide`,
   `isActive`, and `accountViewModel` by closure — so later parameter
   updates wouldn't be visible. Wrapped them in `rememberUpdatedState`
   so the NSC's `canScroll` always sees current values.

3. Gate `ResetBarsOnResume` and the `Modifier.nestedScroll(connection)`
   install on `allowBarHide`. Chat detail screens (pinned chrome) no
   longer wire up a lifecycle observer or dispatch scroll events
   through the NSC for bars that never move.

4. Unify header-slot naming: `SearchScreen.DisplaySearchResults` now
   uses `headerContent` to match `CardFeedView.RenderCardFeed`.

5. Add `DisappearingBarNestedScrollTest` covering:
   - onPostScroll never consumes
   - hide/reveal deltas applied to both bars
   - per-bar clamping at their independent limits
   - consumed + available sum (so overscroll works)
   - canScroll=false freezes the bars
   - reverseLayout inverts the delta sign
   - setting a smaller heightLimit clamps the current offset
   - onPostFling returns Velocity.Zero to swallow phantom velocity

All 41 consumer / shared-helper files touched to remove the now-unused
`scaffoldPadding` parameter, replaced with `rememberFeedContentPadding(
FeedPadding)` at the innermost LazyColumn / LazyVerticalGrid. Behaviour
is unchanged; API surface is smaller.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
This commit is contained in:
Claude
2026-04-19 16:32:55 +00:00
parent 3730d2fb73
commit 154adcf111
42 changed files with 277 additions and 182 deletions
@@ -21,14 +21,12 @@
package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
@@ -41,13 +39,12 @@ fun RefresheableFeedContentStateView(
routeForLastRead: String?,
enablePullRefresh: Boolean = true,
scrollStateKey: String? = null,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
RefresheableBox(feedContentState, enablePullRefresh) {
SaveableFeedContentState(feedContentState, scrollStateKey) { listState ->
RenderFeedContentState(feedContentState, accountViewModel, listState, nav, routeForLastRead, scaffoldPadding)
RenderFeedContentState(feedContentState, accountViewModel, listState, nav, routeForLastRead)
}
}
}
@@ -95,8 +92,7 @@ fun RenderFeedContentState(
listState: LazyListState,
nav: INav,
routeForLastRead: String?,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
onLoaded: @Composable (FeedState.Loaded) -> Unit = { FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav, scaffoldPadding) },
onLoaded: @Composable (FeedState.Loaded) -> Unit = { FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav) },
onEmpty: @Composable () -> Unit = { FeedEmpty(feedContentState::invalidateData) },
onError: @Composable (String) -> Unit = { FeedError(it, feedContentState::invalidateData) },
onLoading: @Composable () -> Unit = { LoadingFeed() },
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
@@ -31,10 +30,9 @@ 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.FeedState
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -49,12 +47,11 @@ fun FeedLoaded(
routeForLastRead: String?,
accountViewModel: AccountViewModel,
nav: INav,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -26,9 +26,12 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.imePadding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -59,27 +62,37 @@ fun DisappearingScaffold(
) {
val state = rememberDisappearingBarState()
val canScroll = {
allowBarHide && isActive() && accountViewModel.settings.isImmersiveScrollingActive()
}
// Hold the latest values in state so the NSC's captured lambda stays fresh across
// recompositions without rebuilding the NSC itself.
val latestIsActive by rememberUpdatedState(isActive)
val latestAllowBarHide by rememberUpdatedState(allowBarHide)
val latestAccountViewModel by rememberUpdatedState(accountViewModel)
val connection =
remember(state, isInvertedLayout) {
DisappearingBarNestedScroll(
state = state,
canScroll = canScroll,
canScroll = {
latestAllowBarHide &&
latestIsActive() &&
latestAccountViewModel.settings.isImmersiveScrollingActive()
},
reverseLayout = isInvertedLayout,
)
}
ResetBarsOnResume(state)
// Only wire the lifecycle observer when the scaffold actually moves its bars.
if (allowBarHide) ResetBarsOnResume(state)
SubcomposeLayout(
modifier =
Modifier
.imePadding()
.nestedScroll(connection),
) { constraints ->
// When bars are pinned, skip attaching the nested-scroll connection entirely.
val rootModifier =
if (allowBarHide) {
Modifier.imePadding().nestedScroll(connection)
} else {
Modifier.imePadding()
}
SubcomposeLayout(modifier = rootModifier) { constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
@@ -127,7 +140,9 @@ fun DisappearingScaffold(
val contentPlaceable =
subcompose(DisappearingSlot.Content) {
mainContent(contentPadding)
CompositionLocalProvider(LocalDisappearingScaffoldPadding provides contentPadding) {
mainContent(contentPadding)
}
}.firstOrNull()?.measure(
Constraints.fixed(layoutWidth, layoutHeight),
)
@@ -24,14 +24,24 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
/**
* The padding the surrounding [DisappearingScaffold] would like its inner scrollable
* to apply as `contentPadding`. Defaults to [PaddingValues] of 0 when no scaffold is
* providing it, so feed composables used outside a scaffold behave as before.
*
* Read it via [rememberFeedContentPadding] to merge with the list's own
* baseline padding (typically `FeedPadding`).
*/
val LocalDisappearingScaffoldPadding = compositionLocalOf { PaddingValues(0.dp) }
/**
* Merges two [PaddingValues] component-wise, resolving start/end against the current
* [LocalLayoutDirection]. Used to combine the scaffold's bar padding with an inner
* list's own padding (e.g. FeedPadding) into a single `contentPadding` value for a
* LazyColumn / LazyVerticalGrid.
* [LocalLayoutDirection].
*/
@Composable
fun rememberMergedPadding(
@@ -48,3 +58,10 @@ fun rememberMergedPadding(
)
}
}
/**
* Convenience for inner LazyColumns/LazyVerticalGrids inside a [DisappearingScaffold]:
* merges the scaffold's reserved space with the list's own baseline padding.
*/
@Composable
fun rememberFeedContentPadding(inner: PaddingValues): PaddingValues = rememberMergedPadding(LocalDisappearingScaffoldPadding.current, inner)
@@ -21,14 +21,12 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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
@@ -50,13 +48,12 @@ fun RefresheableFeedView(
routeForLastRead: String?,
enablePullRefresh: Boolean = true,
scrollStateKey: String? = null,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
RefresheableBox(viewModel, enablePullRefresh) {
SaveableFeedState(viewModel.feedState, scrollStateKey) { listState ->
RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead, scaffoldPadding)
RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead)
}
}
}
@@ -104,9 +101,8 @@ fun RenderFeedState(
listState: LazyListState,
nav: INav,
routeForLastRead: String?,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
onLoaded: @Composable (FeedState.Loaded) -> Unit = {
FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav, scaffoldPadding)
FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav)
},
onEmpty: @Composable () -> Unit = { FeedEmpty { viewModel.invalidateData() } },
onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } },
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -30,14 +29,13 @@ 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.ui.actions.CrossfadeIfEnabled
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.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -50,15 +48,13 @@ fun RefreshingFeedUserFeedView(
accountViewModel: AccountViewModel,
nav: INav,
enablePullRefresh: Boolean = true,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
) {
RefresheableBox(viewModel, enablePullRefresh) { UserFeedView(viewModel, scaffoldPadding, accountViewModel, nav) }
RefresheableBox(viewModel, enablePullRefresh) { UserFeedView(viewModel, accountViewModel, nav) }
}
@Composable
fun UserFeedView(
viewModel: UserFeedViewModel,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -75,7 +71,7 @@ fun UserFeedView(
}
is UserFeedState.Loaded -> {
FeedLoaded(state, scaffoldPadding, accountViewModel, nav)
FeedLoaded(state, accountViewModel, nav)
}
is UserFeedState.Loading -> {
@@ -88,7 +84,6 @@ fun UserFeedView(
@Composable
private fun FeedLoaded(
state: UserFeedState.Loaded,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -97,7 +92,7 @@ private fun FeedLoaded(
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(items, key = { _, item -> item.pubkeyHex }) { _, item ->
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.articles
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,10 +29,9 @@ 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.FeedState
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose
@@ -45,14 +43,13 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
fun ArticlesFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -77,7 +77,7 @@ fun ArticlesScreen(
NewArticleButton(nav)
},
accountViewModel = accountViewModel,
) { paddingValues ->
) {
RefresheableBox(articlesFeedContentState, true) {
SaveableFeedContentState(articlesFeedContentState, scrollStateKey = ScrollStateKeys.ARTICLES_SCREEN) { listState ->
RenderFeedContentState(
@@ -90,7 +90,6 @@ fun ArticlesScreen(
ArticlesFeedLoaded(
loaded = loaded,
listState = listState,
scaffoldPadding = paddingValues,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -125,7 +125,6 @@ private fun RenderBookmarkScreen(
RefresheableFeedView(
privateFeedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -135,7 +134,6 @@ private fun RenderBookmarkScreen(
RefresheableFeedView(
publicFeedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -159,7 +159,6 @@ private fun RenderOldBookmarkScreen(
RefresheableFeedView(
privateFeedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -169,7 +168,6 @@ private fun RenderOldBookmarkScreen(
RefresheableFeedView(
publicFeedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -21,7 +21,6 @@
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
@@ -31,7 +30,6 @@ 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
@@ -41,7 +39,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.layouts.rememberFeedContentPadding
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
@@ -52,13 +50,12 @@ 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, scaffoldPadding, accountViewModel, nav)
CrossFadeState(feedContentState, listState, accountViewModel, nav)
}
}
}
@@ -67,7 +64,6 @@ fun ChatroomListFeedView(
private fun CrossFadeState(
feedContentState: FeedContentState,
listState: LazyListState,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -88,7 +84,7 @@ private fun CrossFadeState(
}
is FeedState.Loaded -> {
FeedLoaded(state, listState, scaffoldPadding, accountViewModel, nav)
FeedLoaded(state, listState, accountViewModel, nav)
}
FeedState.Loading -> {
@@ -102,14 +98,13 @@ 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 = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
@@ -122,7 +121,6 @@ fun MessagesTabHeader(
fun MessagesPager(
pagerState: PagerState,
tabs: List<MessagesTabItem>,
paddingValues: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -138,7 +136,6 @@ fun MessagesPager(
ChatroomListFeedView(
feedContentState = tabs[page].feedContentState,
scrollStateKey = tabs[page].scrollStateKey,
scaffoldPadding = paddingValues,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -98,7 +98,6 @@ fun MessagesSinglePane(
MessagesPager(
pagerState,
tabs,
it,
accountViewModel,
nav,
)
@@ -21,13 +21,11 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
@@ -44,7 +42,6 @@ import kotlinx.collections.immutable.persistentListOf
fun ChatroomList(
knownFeedContentState: FeedContentState,
newFeedContentState: FeedContentState,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -76,7 +73,6 @@ fun ChatroomList(
MessagesPager(
pagerState,
tabs,
scaffoldPadding,
accountViewModel,
nav,
)
@@ -96,7 +96,6 @@ fun MessagesTwoPane(
ChatroomList(
knownFeedContentState,
newFeedContentState,
padding,
accountViewModel,
twoPaneNav,
)
@@ -198,7 +198,6 @@ fun CommunityScreen(
RefresheableFeedView(
feedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -208,7 +207,6 @@ fun CommunityScreen(
RefresheableFeedView(
modFeedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
@@ -70,7 +69,7 @@ import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -277,7 +276,6 @@ private fun DiscoverPages(
routeForLastRead = tab.routeForLastRead,
forceEventKind = tab.forceEventKind,
listState = listState,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -289,7 +287,6 @@ private fun DiscoverPages(
routeForLastRead = tab.routeForLastRead,
forceEventKind = tab.forceEventKind,
listState = listState,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -307,7 +304,6 @@ private fun RenderDiscoverFeed(
routeForLastRead: String?,
forceEventKind: Int?,
listState: LazyGridState,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -334,7 +330,6 @@ private fun RenderDiscoverFeed(
routeForLastRead,
listState,
forceEventKind,
scaffoldPadding,
accountViewModel,
nav,
)
@@ -397,7 +392,6 @@ private fun RenderDiscoverFeed(
routeForLastRead: String?,
forceEventKind: Int?,
listState: LazyListState,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -424,7 +418,6 @@ private fun RenderDiscoverFeed(
routeForLastRead,
listState,
forceEventKind,
scaffoldPadding,
accountViewModel,
nav,
)
@@ -468,14 +461,13 @@ private fun DiscoverFeedLoaded(
routeForLastRead: String?,
listState: LazyListState,
forceEventKind: Int?,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
@@ -504,7 +496,6 @@ private fun DiscoverFeedColumnsLoaded(
routeForLastRead: String?,
listState: LazyGridState,
forceEventKind: Int?,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -512,7 +503,7 @@ private fun DiscoverFeedColumnsLoaded(
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
@@ -54,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS
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.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
@@ -146,7 +145,7 @@ private fun RenderDraftListScreen(
)
},
accountViewModel = accountViewModel,
) { scaffoldPadding ->
) {
RefresheableBox(feedState) {
SaveableFeedState(feedState, DRAFTS) { listState ->
RenderFeedContentState(
@@ -155,7 +154,7 @@ private fun RenderDraftListScreen(
listState = listState,
nav = nav,
routeForLastRead = null,
onLoaded = { DraftFeedLoaded(it, listState, scaffoldPadding, accountViewModel, nav) },
onLoaded = { DraftFeedLoaded(it, listState, accountViewModel, nav) },
)
}
}
@@ -166,14 +165,13 @@ private fun RenderDraftListScreen(
private fun DraftFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
@@ -23,7 +23,6 @@ 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.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -96,13 +95,13 @@ fun DvmContentDiscoveryScreen(
DvmTopBar(appDefinitionEventId, accountViewModel, nav)
},
accountViewModel = accountViewModel,
) { paddingValues ->
) {
LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { note ->
note?.let { baseNote ->
WatchNoteEvent(
baseNote,
onNoteEventFound = {
DvmContentDiscoveryScreen(baseNote, paddingValues, accountViewModel, nav)
DvmContentDiscoveryScreen(baseNote, accountViewModel, nav)
},
onBlank = {
FeedEmptyWithStatus(baseNote, stringRes(R.string.dvm_looking_for_app), accountViewModel, nav)
@@ -117,7 +116,6 @@ fun DvmContentDiscoveryScreen(
@Composable
fun DvmContentDiscoveryScreen(
appDefinition: Note,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -152,7 +150,6 @@ fun DvmContentDiscoveryScreen(
appDefinition,
myRequestEventID,
onRefresh,
scaffoldPadding,
accountViewModel,
nav,
)
@@ -169,7 +166,6 @@ fun ObserverContentDiscoveryResponse(
appDefinition: Note,
dvmRequestId: Note,
onRefresh: () -> Unit,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -198,7 +194,6 @@ fun ObserverContentDiscoveryResponse(
dvmRequestId.idHex,
myResponse,
onRefresh,
scaffoldPadding,
accountViewModel,
nav,
)
@@ -251,7 +246,6 @@ fun PrepareViewContentDiscoveryModels(
dvmRequestId: String,
latestResponse: NIP90ContentDiscoveryResponseEvent,
onRefresh: () -> Unit,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -265,14 +259,13 @@ fun PrepareViewContentDiscoveryModels(
resultFeedViewModel.invalidateData()
}
RenderNostrNIP90ContentDiscoveryScreen(resultFeedViewModel, onRefresh, scaffoldPadding, accountViewModel, nav)
RenderNostrNIP90ContentDiscoveryScreen(resultFeedViewModel, onRefresh, accountViewModel, nav)
}
@Composable
fun RenderNostrNIP90ContentDiscoveryScreen(
resultFeedViewModel: NIP90ContentDiscoveryFeedViewModel,
onRefresh: () -> Unit,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -284,7 +277,6 @@ fun RenderNostrNIP90ContentDiscoveryScreen(
listState,
nav,
null,
scaffoldPadding,
onEmpty = {
FeedEmpty {
onRefresh()
@@ -179,7 +179,6 @@ fun FollowPackFeedScreen(
RefresheableFeedView(
newThreadFeedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -189,7 +188,6 @@ fun FollowPackFeedScreen(
RefresheableFeedView(
conversationsFeedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -198,7 +196,6 @@ fun FollowPackFeedScreen(
2 -> {
UserFeedView(
membersFeedViewModel,
it,
accountViewModel,
nav,
)
@@ -103,7 +103,6 @@ fun GeoHashScreen(
RefresheableFeedView(
feedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -108,7 +108,6 @@ fun HashtagScreen(
RefresheableFeedView(
feedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -24,7 +24,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -52,7 +51,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
@@ -75,7 +73,7 @@ import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -226,7 +224,6 @@ private fun HomePages(
routeForLastRead = tabs[page].routeForLastRead,
scrollStateKey = tabs[page].scrollStateKey,
liveSection = tabs[page].liveSection,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -280,7 +277,6 @@ fun HomeFeeds(
enablePullRefresh: Boolean = true,
scrollStateKey: String? = null,
liveSection: ChannelFeedContentState? = null,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -292,7 +288,7 @@ fun HomeFeeds(
listState = listState,
nav = nav,
routeForLastRead = routeForLastRead,
onLoaded = { FeedLoaded(it, listState, routeForLastRead, liveSection, scaffoldPadding, accountViewModel, nav) },
onLoaded = { FeedLoaded(it, listState, routeForLastRead, liveSection, accountViewModel, nav) },
onEmpty = { HomeFeedEmpty(feedState::invalidateData) },
)
}
@@ -306,16 +302,13 @@ fun FeedLoaded(
listState: LazyListState,
routeForLastRead: String?,
liveSection: ChannelFeedContentState? = null,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
val listPadding = rememberMergedPadding(scaffoldPadding, FeedPadding)
LazyColumn(
contentPadding = listPadding,
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
if (liveSection != null) {
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.longs
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
@@ -33,7 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.VideoCardCompose
@@ -45,14 +44,13 @@ import com.vitorpamplona.quartz.nip71Video.VideoEvent
fun LongsFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -77,7 +77,7 @@ fun LongsScreen(
NewLongVideoButton(accountViewModel, nav, longsFeedContentState::sendToTop)
},
accountViewModel = accountViewModel,
) { paddingValues ->
) {
RefresheableBox(longsFeedContentState, true) {
SaveableFeedContentState(longsFeedContentState, scrollStateKey = ScrollStateKeys.LONGS_SCREEN) { listState ->
RenderFeedContentState(
@@ -90,7 +90,6 @@ fun LongsScreen(
LongsFeedLoaded(
loaded = loaded,
listState = listState,
scaffoldPadding = paddingValues,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -26,7 +26,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -52,7 +51,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.notifications.Card
@@ -61,7 +59,7 @@ import com.vitorpamplona.amethyst.logTime
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
import com.vitorpamplona.amethyst.ui.note.CloseIcon
@@ -89,7 +87,6 @@ fun RenderCardFeed(
nav: INav,
routeForLastRead: String,
scrollToEventId: String? = null,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
headerContent: (@Composable () -> Unit)? = null,
) {
val feedState by feedContent.feedContent.collectAsStateWithLifecycle()
@@ -118,7 +115,6 @@ fun RenderCardFeed(
accountViewModel = accountViewModel,
nav = nav,
scrollToEventId = scrollToEventId,
scaffoldPadding = scaffoldPadding,
headerContent = headerContent,
)
}
@@ -153,7 +149,6 @@ private fun FeedLoaded(
accountViewModel: AccountViewModel,
nav: INav,
scrollToEventId: String? = null,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
headerContent: (@Composable () -> Unit)? = null,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
@@ -179,7 +174,7 @@ private fun FeedLoaded(
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
if (headerContent != null) {
@@ -102,7 +102,6 @@ fun NotificationScreen(
nav = nav,
routeForLastRead = "Notification",
scrollToEventId = scrollToEventId,
scaffoldPadding = it,
headerContent = { ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) },
)
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
@@ -33,7 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -44,14 +43,13 @@ import com.vitorpamplona.quartz.nip68Picture.PictureEvent
fun PictureFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -77,7 +77,7 @@ fun PicturesScreen(
NewPictureButton(accountViewModel, nav, picturesFeedContentState::sendToTop)
},
accountViewModel = accountViewModel,
) { paddingValues ->
) {
RefresheableBox(picturesFeedContentState, true) {
SaveableFeedContentState(picturesFeedContentState, scrollStateKey = ScrollStateKeys.PICTURES_SCREEN) { listState ->
RenderFeedContentState(
@@ -90,7 +90,6 @@ fun PicturesScreen(
PictureFeedLoaded(
loaded = loaded,
listState = listState,
scaffoldPadding = paddingValues,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -78,7 +78,6 @@ private fun RenderPinnedNotesScreen(
RefresheableFeedView(
pinnedNotesFeedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -172,7 +172,6 @@ private fun PollsPages(
listState = listState,
nav = nav,
routeForLastRead = tabs[page].routeForLastRead,
scaffoldPadding = it,
)
}
}
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.products
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.GridCells
@@ -33,7 +32,6 @@ 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
@@ -41,7 +39,7 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
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.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose
@@ -53,7 +51,6 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
fun RenderProductsFeed(
feedContentState: FeedContentState,
gridState: LazyGridState,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -78,7 +75,6 @@ fun RenderProductsFeed(
ProductsFeedColumnsLoaded(
state,
gridState,
scaffoldPadding,
accountViewModel,
nav,
)
@@ -96,7 +92,6 @@ fun RenderProductsFeed(
private fun ProductsFeedColumnsLoaded(
loaded: FeedState.Loaded,
listState: LazyGridState,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -104,7 +99,7 @@ private fun ProductsFeedColumnsLoaded(
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
@@ -76,13 +76,12 @@ fun ProductsScreen(
NewProductButton(accountViewModel, nav)
},
accountViewModel = accountViewModel,
) { paddingValues ->
) {
RefresheableBox(productsFeedContentState, true) {
SaveableGridFeedContentState(productsFeedContentState, scrollStateKey = ScrollStateKeys.PRODUCTS_SCREEN) { gridState ->
RenderProductsFeed(
feedContentState = productsFeedContentState,
gridState = gridState,
scaffoldPadding = paddingValues,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -106,7 +106,6 @@ fun RelayFeedScreen(
RefresheableFeedView(
feedViewModel,
null,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.search
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
@@ -58,7 +57,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.layouts.rememberFeedContentPadding
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,11 +122,10 @@ fun SearchScreen(
}
},
accountViewModel = accountViewModel,
) { scaffoldPadding ->
) {
DisplaySearchResults(
searchBarViewModel = searchBarViewModel,
scaffoldPadding = scaffoldPadding,
header = { ObserveRelayListForSearchAndDisplayIfNotFound(accountViewModel, nav) },
headerContent = { ObserveRelayListForSearchAndDisplayIfNotFound(accountViewModel, nav) },
nav = nav,
accountViewModel = accountViewModel,
)
@@ -220,8 +218,7 @@ private fun SearchTextField(
@Composable
private fun DisplaySearchResults(
searchBarViewModel: SearchBarViewModel,
scaffoldPadding: PaddingValues,
header: @Composable () -> Unit,
headerContent: @Composable () -> Unit,
nav: INav,
accountViewModel: AccountViewModel,
) {
@@ -236,10 +233,10 @@ private fun DisplaySearchResults(
LazyColumn(
modifier = Modifier.fillMaxHeight(),
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = searchBarViewModel.listState,
) {
item(key = "scaffold-header") { header() }
item(key = "scaffold-header") { headerContent() }
if (!isRefreshing) return@LazyColumn
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
@@ -33,7 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -44,14 +43,13 @@ import com.vitorpamplona.quartz.nip71Video.VideoEvent
fun ShortsFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -77,7 +77,7 @@ fun ShortsScreen(
NewShortVideoButton(accountViewModel, nav, shortsFeedContentState::sendToTop)
},
accountViewModel = accountViewModel,
) { paddingValues ->
) {
RefresheableBox(shortsFeedContentState, true) {
SaveableFeedContentState(shortsFeedContentState, scrollStateKey = ScrollStateKeys.SHORTS_SCREEN) { listState ->
RenderFeedContentState(
@@ -90,7 +90,6 @@ fun ShortsScreen(
ShortsFeedLoaded(
loaded = loaded,
listState = listState,
scaffoldPadding = paddingValues,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -85,7 +85,7 @@ import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.layouts.rememberMergedPadding
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
@@ -298,7 +298,6 @@ import kotlinx.coroutines.withContext
fun ThreadFeedView(
noteId: String,
viewModel: LevelFeedViewModel,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -310,7 +309,7 @@ fun ThreadFeedView(
nav = nav,
routeForLastRead = null,
onLoaded = {
RenderThreadFeed(noteId, it, viewModel.llState, viewModel, scaffoldPadding, accountViewModel, nav)
RenderThreadFeed(noteId, it, viewModel.llState, viewModel, accountViewModel, nav)
},
)
}
@@ -322,7 +321,6 @@ fun RenderThreadFeed(
loaded: FeedState.Loaded,
listState: LazyListState,
viewModel: LevelFeedViewModel,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -359,7 +357,7 @@ fun RenderThreadFeed(
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -69,6 +69,6 @@ fun ThreadScreen(
},
accountViewModel = accountViewModel,
) {
ThreadFeedView(noteId, feedViewModel, it, accountViewModel, nav)
ThreadFeedView(noteId, feedViewModel, accountViewModel, nav)
}
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.video
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
@@ -41,7 +40,7 @@ import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
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.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -99,7 +98,6 @@ fun VideoScreen(
RenderFeed(
videoFeedContentState = videoFeedContentState,
scrollKey = ScrollStateKeys.VIDEO_SCREEN,
scaffoldPadding = it,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -125,7 +123,6 @@ fun WatchAccountForVideoScreen(
private fun RenderFeed(
videoFeedContentState: FeedContentState,
scrollKey: String?,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -141,7 +138,6 @@ private fun RenderFeed(
VideoFeedLoaded(
loaded = loaded,
listState = listState,
scaffoldPadding = scaffoldPadding,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -155,14 +151,13 @@ private fun RenderFeed(
fun VideoFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
scaffoldPadding: PaddingValues = PaddingValues(0.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -81,7 +80,7 @@ import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
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.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
@@ -155,7 +154,7 @@ private fun RenderWebBookmarksScreen(
}
},
accountViewModel = accountViewModel,
) { scaffoldPadding ->
) {
RefresheableBox(feedState) {
SaveableFeedState(feedState, ScrollStateKeys.WEB_BOOKMARKS) { listState ->
RenderFeedContentState(
@@ -164,7 +163,7 @@ private fun RenderWebBookmarksScreen(
listState = listState,
nav = nav,
routeForLastRead = null,
onLoaded = { WebBookmarksFeedLoaded(it, listState, scaffoldPadding, accountViewModel, nav) },
onLoaded = { WebBookmarksFeedLoaded(it, listState, accountViewModel, nav) },
)
}
}
@@ -175,14 +174,13 @@ private fun RenderWebBookmarksScreen(
private fun WebBookmarksFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
scaffoldPadding: PaddingValues,
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding),
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
@@ -0,0 +1,170 @@
/*
* 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.layouts
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.Velocity
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
class DisappearingBarNestedScrollTest {
private fun state(
topLimit: Float = 100f,
bottomLimit: Float = 50f,
) = DisappearingBarState().apply {
topHeightLimit = topLimit
bottomHeightLimit = bottomLimit
}
private fun nsc(
state: DisappearingBarState,
canScroll: Boolean = true,
reverseLayout: Boolean = false,
) = DisappearingBarNestedScroll(
state = state,
canScroll = { canScroll },
reverseLayout = reverseLayout,
)
@Test
fun `onPostScroll does not consume any delta`() {
val state = state()
val connection = nsc(state)
val consumed = connection.onPostScroll(Offset(0f, -20f), Offset(0f, 0f), NestedScrollSource.UserInput)
assertEquals(Offset.Zero, consumed)
}
@Test
fun `scrolling content up hides both bars by the total delta`() {
val state = state(topLimit = 100f, bottomLimit = 50f)
val connection = nsc(state)
connection.onPostScroll(Offset(0f, -30f), Offset(0f, 0f), NestedScrollSource.UserInput)
assertEquals(-30f, state.topHeightOffset)
assertEquals(-30f, state.bottomHeightOffset)
}
@Test
fun `bars clamp at their individual limits`() {
val state = state(topLimit = 100f, bottomLimit = 50f)
val connection = nsc(state)
connection.onPostScroll(Offset(0f, -200f), Offset(0f, 0f), NestedScrollSource.UserInput)
assertEquals(-100f, state.topHeightOffset)
assertEquals(-50f, state.bottomHeightOffset)
}
@Test
fun `scrolling content down reveals both bars from a hidden state`() {
val state = state(topLimit = 100f, bottomLimit = 50f)
state.topHeightOffset = -100f
state.bottomHeightOffset = -50f
val connection = nsc(state)
connection.onPostScroll(Offset(0f, 30f), Offset(0f, 0f), NestedScrollSource.UserInput)
assertEquals(-70f, state.topHeightOffset)
assertEquals(-20f, state.bottomHeightOffset)
}
@Test
fun `uses consumed plus available so the bars see the whole scroll attempt`() {
val state = state()
state.topHeightOffset = -50f
state.bottomHeightOffset = -50f
val connection = nsc(state)
// The list consumed 20px of a 40px reveal drag; 20 more was left as overscroll.
// The bars should move by the total 40, not just one of the halves.
connection.onPostScroll(Offset(0f, 20f), Offset(0f, 20f), NestedScrollSource.UserInput)
assertEquals(-10f, state.topHeightOffset)
assertEquals(-10f, state.bottomHeightOffset)
}
@Test
fun `canScroll returning false freezes the bars`() {
val state = state()
val connection = nsc(state, canScroll = false)
connection.onPostScroll(Offset(0f, -100f), Offset(0f, 0f), NestedScrollSource.UserInput)
assertEquals(0f, state.topHeightOffset)
assertEquals(0f, state.bottomHeightOffset)
}
@Test
fun `reverseLayout inverts the delta sign`() {
val state = state()
val connection = nsc(state, reverseLayout = true)
// In reverse layout, a positive Y is a "hide" direction
connection.onPostScroll(Offset(0f, 20f), Offset(0f, 0f), NestedScrollSource.UserInput)
assertEquals(-20f, state.topHeightOffset)
assertEquals(-20f, state.bottomHeightOffset)
}
@Test
fun `bar offsets never exceed zero when revealing`() {
val state = state()
// Start from a mid-hidden position
state.topHeightOffset = -10f
state.bottomHeightOffset = -10f
val connection = nsc(state)
connection.onPostScroll(Offset(0f, 100f), Offset(0f, 0f), NestedScrollSource.UserInput)
assertEquals(0f, state.topHeightOffset)
assertEquals(0f, state.bottomHeightOffset)
}
@Test
fun `setting topHeightLimit smaller than current offset clamps it in`() {
val state = state(topLimit = 100f)
state.topHeightOffset = -80f
state.topHeightLimit = 40f
assertEquals(-40f, state.topHeightOffset)
}
@Test
fun `onPostFling swallows all velocity so parents don't get a phantom fling`() =
runTest {
val state = state()
val connection = nsc(state)
val remaining =
connection.onPostFling(
consumed = Velocity(0f, 1000f),
available = Velocity(0f, 200f),
)
assertEquals(Velocity.Zero, remaining)
}
}