From 724b1a353d6754c90afe17c83ba7f37d6b5663c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 03:09:17 +0000 Subject: [PATCH 1/9] perf(ui): rewrite DisappearingScaffold for smoother scroll chrome The previous implementation shrank the bar slots via Modifier.layout on every scroll pixel, which forced Scaffold to re-measure its content and the inner LazyColumn/HorizontalPager to reflow every frame. It also over-claimed consumed offsets in onPreScroll (the list lost pixels at the edge of the bars' travel range) and ran an extra decay+snap after a fling, producing a visible "double animation". New approach: - Custom SubcomposeLayout keeps the bar slots at their natural height and moves them via Modifier.graphicsLayer { translationY = ... }, which is a compositor-only operation. The content placeable is measured at full parent size with a stable PaddingValues so the inner lists never re-measure while the bars slide. - Unified DisappearingBarState and DisappearingBarNestedScroll hide/ reveal both bars together and return the exact consumed delta, so no pixels are swallowed at the transition. - onPostFling snaps mid-way bars to the nearest edge without running a second decay, and returns Velocity.Zero so no phantom velocity leaks into parent nested-scroll containers. - Drops the Modifier.draggable on both bars, re-enables reset-on-resume for the top bar, and simplifies the FAB to a graphicsLayer-only holder that rides along with the bottom bar. https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa --- .../ui/layouts/DisappearingBarNestedScroll.kt | 107 +++++++ .../ui/layouts/DisappearingBarState.kt | 125 ++++++++ .../ui/layouts/DisappearingBottomBar.kt | 171 ---------- .../ui/layouts/DisappearingFloatingButton.kt | 62 ---- .../ui/layouts/DisappearingScaffold.kt | 187 ++++++++--- .../amethyst/ui/layouts/DisappearingTopBar.kt | 292 ------------------ .../ui/screen/loggedIn/home/NewNoteButton.kt | 21 +- 7 files changed, 379 insertions(+), 586 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarState.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBottomBar.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingFloatingButton.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt new file mode 100644 index 0000000000..0261d68576 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt @@ -0,0 +1,107 @@ +/* + * 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.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Velocity + +/** + * Single nested-scroll connection that hides/reveals the top and bottom bars together. + * + * Behaviour: + * - onPreScroll: on "hide" deltas, consumes exactly the amount used to shift the bars + * (never over-claims the available delta). This avoids the list "swallowing" pixels + * at the edge of the bars' travel range. + * - onPostScroll: on "reveal" deltas still available after the list consumed its share, + * moves the bars back in; again consumes only what it used. + * - onPostFling: snaps mid-way bars to the nearest edge. No additional decay. No + * phantom velocity is returned upward. + */ +class DisappearingBarNestedScroll( + private val state: DisappearingBarState, + private val canScroll: () -> Boolean, + private val reverseLayout: Boolean, +) : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset { + if (!canScroll()) return Offset.Zero + val deltaY = if (reverseLayout) -available.y else available.y + // Only hide on "hide" direction in the pre-scroll phase. + if (deltaY >= 0f) return Offset.Zero + + val consumed = applyDelta(deltaY) + if (consumed == 0f) return Offset.Zero + return Offset(0f, if (reverseLayout) -consumed else consumed) + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + if (!canScroll()) return Offset.Zero + val deltaY = if (reverseLayout) -available.y else available.y + // Only reveal on "reveal" direction in the post-scroll phase. + if (deltaY <= 0f) return Offset.Zero + + val applied = applyDelta(deltaY) + if (applied == 0f) return Offset.Zero + return Offset(0f, if (reverseLayout) -applied else applied) + } + + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity { + if (canScroll()) state.snapToNearestEdge() + // Do not propagate phantom velocity back up the nested-scroll tree. + return Velocity.Zero + } + + /** + * Applies the given delta (in "content-space" – negative hides, positive reveals) to + * both bar offsets, clamped to their travel range. + * + * Returns the delta that was actually absorbed (in the same sign convention) so the + * caller can report accurate consumption upward. + */ + private fun applyDelta(deltaY: Float): Float { + val prevTop = state.topHeightOffset + val prevBottom = state.bottomHeightOffset + val topLimit = state.topHeightLimit + val bottomLimit = state.bottomHeightLimit + + val newTop = (prevTop + deltaY).coerceIn(-topLimit, 0f) + val newBottom = (prevBottom + deltaY).coerceIn(-bottomLimit, 0f) + state.topHeightOffset = newTop + state.bottomHeightOffset = newBottom + + // If either bar moved we consider that portion consumed. Use the largest absorbed + // magnitude so we don't claim more than one bar's worth of pixels. + val topDelta = newTop - prevTop + val bottomDelta = newBottom - prevBottom + return if (deltaY < 0f) minOf(topDelta, bottomDelta) else maxOf(topDelta, bottomDelta) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarState.kt new file mode 100644 index 0000000000..50df41a080 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarState.kt @@ -0,0 +1,125 @@ +/* + * 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.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +/** + * Shared state for the disappearing top / bottom bar chrome. + * + * Both offsets are negative-or-zero. 0 = fully visible; -limit = fully hidden. + * + * Limits are updated by the layout pass once it measures the bar slots. The nested-scroll + * connection reads both limits and offsets to clamp movement to the visible travel range. + */ +@Stable +class DisappearingBarState( + initialTopHeightOffset: Float = 0f, + initialBottomHeightOffset: Float = 0f, +) { + var topHeightOffset by mutableFloatStateOf(initialTopHeightOffset) + var bottomHeightOffset by mutableFloatStateOf(initialBottomHeightOffset) + + var topHeightLimit: Float = 0f + set(value) { + field = value + if (topHeightOffset < -value) topHeightOffset = -value + } + + var bottomHeightLimit: Float = 0f + set(value) { + field = value + if (bottomHeightOffset < -value) bottomHeightOffset = -value + } + + val topCollapsedFraction: Float + get() = if (topHeightLimit <= 0f) 0f else (-topHeightOffset / topHeightLimit).coerceIn(0f, 1f) + + val bottomCollapsedFraction: Float + get() = if (bottomHeightLimit <= 0f) 0f else (-bottomHeightOffset / bottomHeightLimit).coerceIn(0f, 1f) + + /** + * Snaps both bars to the nearest edge (fully shown or fully hidden). + * Used after a fling to resolve the "mid-way" state without a decay animation. + */ + suspend fun snapToNearestEdge() { + coroutineScope { + launch { snapOne(topHeightLimit, { topHeightOffset }) { topHeightOffset = it } } + launch { snapOne(bottomHeightLimit, { bottomHeightOffset }) { bottomHeightOffset = it } } + } + } + + /** + * Animates both bars back to the fully visible resting state. Used on lifecycle resume. + */ + suspend fun resetToVisible() { + coroutineScope { + launch { animateOne({ topHeightOffset }, 0f) { topHeightOffset = it } } + launch { animateOne({ bottomHeightOffset }, 0f) { bottomHeightOffset = it } } + } + } + + private suspend fun snapOne( + limit: Float, + get: () -> Float, + set: (Float) -> Unit, + ) { + if (limit <= 0f) return + val current = get() + if (current >= 0f || current <= -limit) return + val target = if (-current < limit / 2f) 0f else -limit + animateOne(get, target, set) + } + + private suspend fun animateOne( + get: () -> Float, + target: Float, + set: (Float) -> Unit, + ) { + val start = get() + if (start == target) return + Animatable(start) + .animateTo(target, animationSpec = spring(stiffness = 600f)) { + set(value) + } + } + + companion object { + val Saver: Saver = + Saver( + save = { listOf(it.topHeightOffset, it.bottomHeightOffset) }, + restore = { DisappearingBarState(it[0], it[1]) }, + ) + } +} + +@Composable +fun rememberDisappearingBarState(): DisappearingBarState = rememberSaveable(saver = DisappearingBarState.Saver) { DisappearingBarState() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBottomBar.kt deleted file mode 100644 index 68281cf55d..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBottomBar.kt +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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.animation.core.AnimationSpec -import androidx.compose.animation.core.AnimationState -import androidx.compose.animation.core.DecayAnimationSpec -import androidx.compose.animation.core.animateDecay -import androidx.compose.animation.core.animateTo -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.BottomAppBarScrollBehavior -import androidx.compose.material3.BottomAppBarState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.layout -import androidx.compose.ui.unit.Velocity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner -import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.roundToInt - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DisappearingBottomBar( - scrollBehavior: BottomAppBarScrollBehavior, - content: @Composable (ColumnScope.() -> Unit), -) { - // Set up support for resizing the bottom app bar when vertically dragging the bar itself. - val appBarDragModifier = - if (!scrollBehavior.isPinned) { - Modifier.draggable( - orientation = Orientation.Vertical, - state = rememberDraggableState { delta -> scrollBehavior.state.heightOffset -= delta }, - onDragStopped = { velocity -> - settleAppBarBottom( - scrollBehavior.state, - velocity, - scrollBehavior.flingAnimationSpec, - scrollBehavior.snapAnimationSpec, - ) - }, - ) - } else { - Modifier - } - - ResetDisappearingOnResume(scrollBehavior) - - Column( - modifier = - Modifier - .fillMaxWidth() - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - scrollBehavior.state.heightOffsetLimit = -placeable.height.toFloat() - val height = placeable.height + scrollBehavior.state.heightOffset - layout(placeable.width, height.roundToInt().coerceAtLeast(0)) { placeable.place(0, 0) } - }.then(appBarDragModifier), - content = content, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ResetDisappearingOnResume(scrollBehavior: BottomAppBarScrollBehavior) { - // resets bar on resume - val lifeCycleOwner = LocalLifecycleOwner.current - val scope = rememberCoroutineScope() - DisposableEffect(lifeCycleOwner) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME && scrollBehavior.state.heightOffset != 0f) { - val spec = scrollBehavior.snapAnimationSpec - if (spec != null) { - scope.launch { - AnimationState(initialValue = scrollBehavior.state.heightOffset) - .animateTo(0f, animationSpec = spec) { - scrollBehavior.state.heightOffset = value - } - } - } else { - scrollBehavior.state.heightOffset = 0f - } - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -private suspend fun settleAppBarBottom( - state: BottomAppBarState, - velocity: Float, - flingAnimationSpec: DecayAnimationSpec?, - snapAnimationSpec: AnimationSpec?, -): Velocity { - // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, - // and just return Zero Velocity. - // Note that we don't check for 0f due to float precision with the collapsedFraction - // calculation. - if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { - return Velocity.Zero - } - var remainingVelocity = velocity - // In case there is an initial velocity that was left after a previous user fling, animate to - // continue the motion to expand or collapse the app bar. - if (flingAnimationSpec != null && abs(velocity) > 1f) { - var lastValue = 0f - AnimationState( - initialValue = 0f, - initialVelocity = velocity, - ).animateDecay(flingAnimationSpec) { - val delta = value - lastValue - val initialHeightOffset = state.heightOffset - state.heightOffset = initialHeightOffset + delta - val consumed = abs(initialHeightOffset - state.heightOffset) - lastValue = value - remainingVelocity = this.velocity - // avoid rounding errors and stop if anything is unconsumed - if (abs(delta - consumed) > 0.5f) this.cancelAnimation() - } - } - // Snap if animation specs were provided. - if (snapAnimationSpec != null) { - if (state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit) { - AnimationState(initialValue = state.heightOffset).animateTo( - if (state.collapsedFraction < 0.5f) { - 0f - } else { - state.heightOffsetLimit - }, - animationSpec = snapAnimationSpec, - ) { - state.heightOffset = value - } - } - } - - return Velocity(0f, remainingVelocity) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingFloatingButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingFloatingButton.kt deleted file mode 100644 index e8f9d1d24b..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingFloatingButton.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.material3.BottomAppBarScrollBehavior -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.layout - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DisappearingFloatingButton( - scrollBehavior: BottomAppBarScrollBehavior, - content: @Composable (BoxScope.() -> Unit), -) { - // We calculate the scale/alpha based on how much the bar is expanded - // 1.0 = fully visible, 0.0 = fully hidden - val progress = (1f - scrollBehavior.state.collapsedFraction).coerceAtLeast(0.001f) - - Box( - modifier = - Modifier - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - // Adjust the height of the layout so the FAB doesn't leave a "hole" - val currentHeight = (placeable.height * progress).toInt() - layout(placeable.width, currentHeight) { - placeable.placeRelative(0, 0) - } - }.graphicsLayer { - this.scaleX = progress - this.scaleY = progress - this.alpha = progress - clip = false - }, - contentAlignment = Alignment.TopCenter, - content = content, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt index 3096fb99a6..c9b2f728df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt @@ -20,20 +20,32 @@ */ package com.vitorpamplona.amethyst.ui.layouts +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.imePadding -import androidx.compose.material3.BottomAppBarDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import kotlinx.coroutines.launch + +private enum class DisappearingSlot { Top, Bottom, Content, Fab } + +private val FabEdgePadding = 16.dp -@OptIn(ExperimentalMaterial3Api::class) @Composable fun DisappearingScaffold( isInvertedLayout: Boolean, @@ -44,50 +56,141 @@ fun DisappearingScaffold( isActive: () -> Boolean = { true }, mainContent: @Composable (padding: PaddingValues) -> Unit, ) { - val topBehavior = - enterAlwaysScrollBehavior( - canScroll = { - topBar != null && isActive() && accountViewModel.settings.isImmersiveScrollingActive() - }, - reverseLayout = isInvertedLayout, - ) - val bottomBehavior = - BottomAppBarDefaults.exitAlwaysScrollBehavior( - canScroll = { - (bottomBar != null || floatingButton != null) && isActive() && accountViewModel.settings.isImmersiveScrollingActive() - }, - ) + val state = rememberDisappearingBarState() - Scaffold( + val canScroll = { + isActive() && accountViewModel.settings.isImmersiveScrollingActive() + } + + val connection = + remember(state, isInvertedLayout) { + DisappearingBarNestedScroll( + state = state, + canScroll = canScroll, + reverseLayout = isInvertedLayout, + ) + } + + ResetBarsOnResume(state) + + SubcomposeLayout( modifier = Modifier .imePadding() - .nestedScroll(topBehavior.nestedScrollConnection) - .nestedScroll(bottomBehavior.nestedScrollConnection), - bottomBar = { - bottomBar?.let { - DisappearingBottomBar(bottomBehavior) { - it() - } - } - }, - topBar = { - topBar?.let { - DisappearingTopBar(topBehavior) { - Column { - it() + .nestedScroll(connection), + ) { constraints -> + val layoutWidth = constraints.maxWidth + val layoutHeight = constraints.maxHeight + val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) + + val topPlaceable = + topBar?.let { bar -> + subcompose(DisappearingSlot.Top) { + Column( + modifier = + Modifier.graphicsLayer { + translationY = state.topHeightOffset + }, + ) { + bar() HorizontalDivider(thickness = DividerThickness) } + }.firstOrNull()?.measure(looseConstraints) + } + val topHeight = topPlaceable?.height ?: 0 + + val bottomPlaceable = + bottomBar?.let { bar -> + subcompose(DisappearingSlot.Bottom) { + Column( + modifier = + Modifier.graphicsLayer { + translationY = -state.bottomHeightOffset + }, + ) { + bar() + } + }.firstOrNull()?.measure(looseConstraints) + } + val bottomHeight = bottomPlaceable?.height ?: 0 + + // Publish the measured limits so the nested-scroll connection can clamp correctly. + state.topHeightLimit = topHeight.toFloat() + state.bottomHeightLimit = bottomHeight.toFloat() + + val contentPadding = + PaddingValues( + top = topHeight.toDp(), + bottom = bottomHeight.toDp(), + ) + + val contentPlaceable = + subcompose(DisappearingSlot.Content) { + mainContent(contentPadding) + }.firstOrNull()?.measure( + Constraints.fixed(layoutWidth, layoutHeight), + ) + + val fabPlaceable = + floatingButton?.let { fab -> + subcompose(DisappearingSlot.Fab) { + FloatingButtonHolder(state = state) { fab() } + }.firstOrNull()?.measure(looseConstraints) + } + + val fabEdgePx = FabEdgePadding.roundToPx() + + layout(layoutWidth, layoutHeight) { + contentPlaceable?.place(0, 0) + topPlaceable?.place(0, 0) + bottomPlaceable?.place(0, layoutHeight - bottomHeight) + if (fabPlaceable != null) { + val x = layoutWidth - fabPlaceable.width - fabEdgePx + val yBase = layoutHeight - bottomHeight - fabPlaceable.height - fabEdgePx + fabPlaceable.place(x, yBase) + } + } + } +} + +/** + * Holds the floating button. Scales / fades it with the bottom-bar collapse fraction and + * rides along with the bar via graphicsLayer, so the layout pass is untouched while + * the bar animates. + */ +@Composable +private fun FloatingButtonHolder( + state: DisappearingBarState, + content: @Composable () -> Unit, +) { + Box( + modifier = + Modifier.graphicsLayer { + val visible = (1f - state.bottomCollapsedFraction).coerceAtLeast(0f) + scaleX = visible + scaleY = visible + alpha = visible + translationY = -state.bottomHeightOffset + }, + ) { + content() + } +} + +@Composable +private fun ResetBarsOnResume(state: DisappearingBarState) { + val lifecycleOwner = LocalLifecycleOwner.current + val scope = rememberCoroutineScope() + DisposableEffect(lifecycleOwner, state) { + val observer = + LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + if (state.topHeightOffset != 0f || state.bottomHeightOffset != 0f) { + scope.launch { state.resetToVisible() } + } } } - }, - floatingActionButton = { - floatingButton?.let { - DisappearingFloatingButton(bottomBehavior) { - it() - } - } - }, - content = mainContent, - ) + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt deleted file mode 100644 index a70737255f..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt +++ /dev/null @@ -1,292 +0,0 @@ -/* - * 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.animation.core.AnimationSpec -import androidx.compose.animation.core.AnimationState -import androidx.compose.animation.core.DecayAnimationSpec -import androidx.compose.animation.core.animateDecay -import androidx.compose.animation.core.animateTo -import androidx.compose.animation.core.spring -import androidx.compose.animation.rememberSplineBasedDecay -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.TopAppBarState -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.layout.layout -import androidx.compose.ui.unit.Velocity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner -import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.roundToInt - -@Composable -fun DisappearingTopBar( - scrollBehavior: CustomEnterAlwaysScrollBehavior, - content: @Composable (ColumnScope.() -> Unit), -) { - // ResetDisappearingOnResume(scrollBehavior) - - // Set up support for resizing the top app bar when vertically dragging the bar itself. - val appBarDragModifier = - Modifier - .draggable( - orientation = Orientation.Vertical, - state = - rememberDraggableState { delta -> - scrollBehavior.state.heightOffset += delta - }, - onDragStopped = { velocity -> - settleAppBar( - scrollBehavior.state, - velocity, - scrollBehavior.flingAnimationSpec, - scrollBehavior.snapAnimationSpec, - ) - }, - ) - - Column( - Modifier - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - // Sets the app bar's height offset to collapse the entire bar's height when - // content is scrolled. - scrollBehavior.state.heightOffsetLimit = -placeable.height.toFloat() - val height = placeable.height + scrollBehavior.state.heightOffset - layout(placeable.width, height.roundToInt().coerceAtLeast(0)) { - // slides up together with the reduce in height - placeable.place(0, scrollBehavior.state.heightOffset.roundToInt()) - } - }.then(appBarDragModifier), - content = content, - ) -} - -@Composable -fun ResetDisappearingOnResume(scrollBehavior: CustomEnterAlwaysScrollBehavior) { - // resets bar on resume - val lifeCycleOwner = LocalLifecycleOwner.current - val scope = rememberCoroutineScope() - DisposableEffect(lifeCycleOwner) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME && scrollBehavior.state.heightOffset != 0f) { - val spec = scrollBehavior.snapAnimationSpec - if (spec != null) { - scope.launch { - AnimationState(initialValue = scrollBehavior.state.heightOffset) - .animateTo(0f, animationSpec = spec) { - scrollBehavior.state.heightOffset = value - } - - AnimationState(initialValue = scrollBehavior.state.contentOffset) - .animateTo(0f, animationSpec = spec) { - scrollBehavior.state.contentOffset = value - } - } - } else { - scrollBehavior.state.heightOffset = 0f - scrollBehavior.state.contentOffset = 0f - } - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } - } -} - -private suspend fun settleAppBar( - state: TopAppBarState, - velocity: Float, - flingAnimationSpec: DecayAnimationSpec?, - snapAnimationSpec: AnimationSpec?, -): Velocity { - // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, - // and just return Zero Velocity. - // Note that we don't check for 0f due to float precision with the collapsedFraction - // calculation. - if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { - return Velocity.Zero - } - var remainingVelocity = velocity - // In case there is an initial velocity that was left after a previous user fling, animate to - // continue the motion to expand or collapse the app bar. - if (flingAnimationSpec != null && abs(velocity) > 1f) { - var lastValue = 0f - AnimationState( - initialValue = 0f, - initialVelocity = velocity, - ).animateDecay(flingAnimationSpec) { - val delta = value - lastValue - val initialHeightOffset = state.heightOffset - state.heightOffset = initialHeightOffset + delta - val consumed = abs(initialHeightOffset - state.heightOffset) - lastValue = value - remainingVelocity = this.velocity - // avoid rounding errors and stop if anything is unconsumed - if (abs(delta - consumed) > 0.5f) this.cancelAnimation() - } - } - // Snap if animation specs were provided. - if (snapAnimationSpec != null) { - if (state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit) { - AnimationState(initialValue = state.heightOffset).animateTo( - if (state.collapsedFraction < 0.5f) { - 0f - } else { - state.heightOffsetLimit - }, - animationSpec = snapAnimationSpec, - ) { - state.heightOffset = value - } - } - } - - return Velocity(0f, remainingVelocity) -} - -/** - * EnterAlwaysScrollBehavior uses internal Material 3 animation specs - */ -val defaultMaterial3StandardSnap = - spring( - dampingRatio = 1.0f, - stiffness = 1600.0f, - ) - -/** - * Copy of enterAlwaysScrollBehavior to use CustomEnterAlwaysScrollBehavior - */ -@Composable -fun enterAlwaysScrollBehavior( - state: TopAppBarState = rememberTopAppBarState(), - canScroll: () -> Boolean = { true }, - // TODO Load the motionScheme tokens from the component tokens file - snapAnimationSpec: AnimationSpec? = defaultMaterial3StandardSnap, - flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), - reverseLayout: Boolean = false, -): CustomEnterAlwaysScrollBehavior = - remember(state, canScroll, snapAnimationSpec, flingAnimationSpec) { - CustomEnterAlwaysScrollBehavior( - state = state, - snapAnimationSpec = snapAnimationSpec, - flingAnimationSpec = flingAnimationSpec, - canScroll = canScroll, - reverseLayout = reverseLayout, - ) - } - -/** - * Custom copy of EnterAlwaysScrollBehavior that correctly handles reversed layouts - */ -@OptIn(ExperimentalMaterial3Api::class) -@Stable -class CustomEnterAlwaysScrollBehavior( - override val state: TopAppBarState, - override val snapAnimationSpec: AnimationSpec?, - override val flingAnimationSpec: DecayAnimationSpec?, - val canScroll: () -> Boolean = { true }, - val reverseLayout: Boolean = false, -) : TopAppBarScrollBehavior { - override val isPinned: Boolean = false - override var nestedScrollConnection = - object : NestedScrollConnection { - override fun onPreScroll( - available: Offset, - source: NestedScrollSource, - ): Offset { - if (!canScroll()) return Offset.Zero - val prevHeightOffset = state.heightOffset - - state.heightOffset += - if (reverseLayout) { - -available.y - } else { - available.y - } - - // The state's heightOffset is coerce in a minimum value of heightOffsetLimit and a - // maximum value 0f, so we check if its value was actually changed after the - // available.y was added to it in order to tell if the top app bar is currently - // collapsing or expanding. - // Note that when the content was set with a revered layout, we always return a - // zero offset. - return if (!reverseLayout && prevHeightOffset != state.heightOffset) { - available.copy(x = 0f) - } else { - Offset.Zero - } - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - if (!canScroll()) return Offset.Zero - state.contentOffset += consumed.y - state.heightOffset += - if (reverseLayout) { - -consumed.y - } else { - consumed.y - } - return Offset.Zero - } - - override suspend fun onPostFling( - consumed: Velocity, - available: Velocity, - ): Velocity { - val hasVelocityLeft = if (reverseLayout) available.y < 0f else available.y > 0f - if ( - hasVelocityLeft && - (state.heightOffset == 0f || state.heightOffset == state.heightOffsetLimit) - ) { - // Reset the total content offset to zero when scrolling all the way down. - // This will eliminate some float precision inaccuracies. - state.contentOffset = 0f - } - val superConsumed = super.onPostFling(consumed, available) - return superConsumed + settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt index e96c8e744a..298d5f5263 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt @@ -21,11 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.BottomAppBarDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -36,7 +33,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.layouts.DisappearingFloatingButton import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -46,25 +42,12 @@ import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow -@OptIn(ExperimentalMaterial3Api::class) @Preview @Composable fun NewNoteButtonPreview() { - val bottomBehavior = - BottomAppBarDefaults.exitAlwaysScrollBehavior( - canScroll = { true }, - ) - ThemeComparisonRow { - Column { - Box(Modifier.size(200.dp), contentAlignment = Alignment.Center) { - DisappearingFloatingButton(bottomBehavior) { - NewNoteButton(EmptyNav()) - } - } - Box(Modifier.size(200.dp), contentAlignment = Alignment.Center) { - NewNoteButton(EmptyNav()) - } + Box(Modifier.size(200.dp), contentAlignment = Alignment.Center) { + NewNoteButton(EmptyNav()) } } } From 6c7e2bd67f2807b188bf216758e21f904f021745 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 03:35:46 +0000 Subject: [PATCH 2/9] perf(ui): bar reveals on pre-scroll and rides the fling's velocity Two follow-up tweaks to the disappearing scaffold to remove the last sources of roughness users could notice: - Reveal now happens in onPreScroll as well as hide. The previous version only re-showed the bars when the list had leftover delta (at the top), so pulling finger-down mid-list did nothing until you actually reached the top. Matches M3 enterAlways expectations and still returns the exact consumed amount so the list never loses pixels. - Post-fling settle now picks up the fling's tail velocity as initialVelocity and a strong velocity also biases the snap target, so the bar continues the fling's motion rather than starting a separate animation after it. Spring softened from stiffness 600 to StiffnessMediumLow for a less abrupt finish, and we early-out when already at a resting edge. onPostFling still swallows residual velocity so parents don't get a phantom kick. https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa --- .../ui/layouts/DisappearingBarNestedScroll.kt | 51 +++++++------------ .../ui/layouts/DisappearingBarState.kt | 51 ++++++++++++++----- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt index 0261d68576..5d2db649d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt @@ -29,13 +29,12 @@ import androidx.compose.ui.unit.Velocity * Single nested-scroll connection that hides/reveals the top and bottom bars together. * * Behaviour: - * - onPreScroll: on "hide" deltas, consumes exactly the amount used to shift the bars - * (never over-claims the available delta). This avoids the list "swallowing" pixels - * at the edge of the bars' travel range. - * - onPostScroll: on "reveal" deltas still available after the list consumed its share, - * moves the bars back in; again consumes only what it used. - * - onPostFling: snaps mid-way bars to the nearest edge. No additional decay. No - * phantom velocity is returned upward. + * - onPreScroll: consumes the portion of the scroll delta used to move the bars + * (both hide and reveal), returning the exact amount absorbed so the list never + * "loses pixels" at the edge of the bars' travel range. + * - onPostFling: snaps a mid-way bar to the nearest edge, continuing the fling's + * tail velocity so the settle motion feels like part of the fling, not a second + * animation after it. No velocity is returned upward. */ class DisappearingBarNestedScroll( private val state: DisappearingBarState, @@ -47,25 +46,9 @@ class DisappearingBarNestedScroll( source: NestedScrollSource, ): Offset { if (!canScroll()) return Offset.Zero + if (available.y == 0f) return Offset.Zero + val deltaY = if (reverseLayout) -available.y else available.y - // Only hide on "hide" direction in the pre-scroll phase. - if (deltaY >= 0f) return Offset.Zero - - val consumed = applyDelta(deltaY) - if (consumed == 0f) return Offset.Zero - return Offset(0f, if (reverseLayout) -consumed else consumed) - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - if (!canScroll()) return Offset.Zero - val deltaY = if (reverseLayout) -available.y else available.y - // Only reveal on "reveal" direction in the post-scroll phase. - if (deltaY <= 0f) return Offset.Zero - val applied = applyDelta(deltaY) if (applied == 0f) return Offset.Zero return Offset(0f, if (reverseLayout) -applied else applied) @@ -75,17 +58,23 @@ class DisappearingBarNestedScroll( consumed: Velocity, available: Velocity, ): Velocity { - if (canScroll()) state.snapToNearestEdge() - // Do not propagate phantom velocity back up the nested-scroll tree. + if (canScroll()) { + // Feed the fling's remaining velocity into the settle so the bar keeps + // moving in the same direction rather than starting a fresh animation. + val velocityY = if (reverseLayout) -available.y else available.y + state.settleToNearestEdge(initialVelocityY = velocityY) + } + // Swallow any residual velocity so parents don't get a phantom fling kick. return Velocity.Zero } /** - * Applies the given delta (in "content-space" – negative hides, positive reveals) to + * Applies the given delta (in content-space – negative hides, positive reveals) to * both bar offsets, clamped to their travel range. * - * Returns the delta that was actually absorbed (in the same sign convention) so the - * caller can report accurate consumption upward. + * Returns the delta that was actually absorbed, using the side with the larger + * absorption so consumption reporting remains accurate when the two bars have + * different remaining travel. */ private fun applyDelta(deltaY: Float): Float { val prevTop = state.topHeightOffset @@ -98,8 +87,6 @@ class DisappearingBarNestedScroll( state.topHeightOffset = newTop state.bottomHeightOffset = newBottom - // If either bar moved we consider that portion consumed. Use the largest absorbed - // magnitude so we don't claim more than one bar's worth of pixels. val topDelta = newTop - prevTop val bottomDelta = newBottom - prevBottom return if (deltaY < 0f) minOf(topDelta, bottomDelta) else maxOf(topDelta, bottomDelta) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarState.kt index 50df41a080..d426b09e7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarState.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.layouts import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -68,12 +69,15 @@ class DisappearingBarState( /** * Snaps both bars to the nearest edge (fully shown or fully hidden). - * Used after a fling to resolve the "mid-way" state without a decay animation. + * + * If [initialVelocityY] is non-zero the spring continues the fling's motion rather than + * starting from rest, avoiding the "extra animation at the end of the fling" feel. The + * velocity is in content-space (negative = hide direction, positive = reveal direction). */ - suspend fun snapToNearestEdge() { + suspend fun settleToNearestEdge(initialVelocityY: Float = 0f) { coroutineScope { - launch { snapOne(topHeightLimit, { topHeightOffset }) { topHeightOffset = it } } - launch { snapOne(bottomHeightLimit, { bottomHeightOffset }) { bottomHeightOffset = it } } + launch { settleOne({ topHeightOffset }, topHeightLimit, initialVelocityY) { topHeightOffset = it } } + launch { settleOne({ bottomHeightOffset }, bottomHeightLimit, initialVelocityY) { bottomHeightOffset = it } } } } @@ -82,37 +86,60 @@ class DisappearingBarState( */ suspend fun resetToVisible() { coroutineScope { - launch { animateOne({ topHeightOffset }, 0f) { topHeightOffset = it } } - launch { animateOne({ bottomHeightOffset }, 0f) { bottomHeightOffset = it } } + launch { animateOne({ topHeightOffset }, 0f, 0f) { topHeightOffset = it } } + launch { animateOne({ bottomHeightOffset }, 0f, 0f) { bottomHeightOffset = it } } } } - private suspend fun snapOne( - limit: Float, + private suspend fun settleOne( get: () -> Float, + limit: Float, + initialVelocityY: Float, set: (Float) -> Unit, ) { if (limit <= 0f) return val current = get() if (current >= 0f || current <= -limit) return - val target = if (-current < limit / 2f) 0f else -limit - animateOne(get, target, set) + + // Decide target edge from position by default, but let a strong velocity bias it. + val positionBiasToHide = -current > limit / 2f + val target = + when { + initialVelocityY < -VELOCITY_BIAS_THRESHOLD -> -limit + initialVelocityY > VELOCITY_BIAS_THRESHOLD -> 0f + positionBiasToHide -> -limit + else -> 0f + } + animateOne(get, target, initialVelocityY, set) } private suspend fun animateOne( get: () -> Float, target: Float, + initialVelocity: Float, set: (Float) -> Unit, ) { val start = get() - if (start == target) return + if (start == target && initialVelocity == 0f) return Animatable(start) - .animateTo(target, animationSpec = spring(stiffness = 600f)) { + .animateTo( + targetValue = target, + animationSpec = SETTLE_SPRING, + initialVelocity = initialVelocity, + ) { set(value) } } companion object { + private const val VELOCITY_BIAS_THRESHOLD = 200f + + private val SETTLE_SPRING = + spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMediumLow, + ) + val Saver: Saver = Saver( save = { listOf(it.topHeightOffset, it.bottomHeightOffset) }, From f9cc86f5af44c90a8b3e5a81611e2d490455df83 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 14:22:24 +0000 Subject: [PATCH 3/9] fix(ui): scroll-linked bars, content renders behind hiding bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions from the previous rewrite: 1. "Nothing scrolls until the bars are gone" — the previous pass had the bar consume scroll delta in onPreScroll, so the content couldn't advance until the bar finished hiding. Switched to a scroll-linked model (Twitter/Instagram/Bluesky style): onPostScroll reads `consumed + available` (the total scroll attempt) and slides the bars by that delta without consuming anything. Content keeps full-speed scrolling; the bars just ride along at the same rate. 2. "Black/white strip where the bars used to be" — HomeScreen and DiscoverScreen were passing the scaffold padding to HorizontalPager's contentPadding, which shrinks the pages so they don't extend behind the bars. The scaffold padding now threads through to the inner LazyColumn/LazyVerticalGrid as its contentPadding instead, so pages fill the full screen and items scroll behind the bar layer. As the bar translates off-screen, the items previously hidden behind it become visible. onPostFling still snaps a mid-way bar to the nearest edge using the fling's tail velocity, and swallows residual velocity so parents don't get a phantom kick. Other screens that use `Modifier.padding(it)` around a scrollable (e.g. NotificationScreen, Video, Search) still have the strip when their bars hide; each will need the same "padding on inner LazyColumn" migration. Left for follow-ups. https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa --- .../ui/layouts/DisappearingBarNestedScroll.kt | 60 +++++++------------ .../loggedIn/discover/DiscoverScreen.kt | 40 ++++++++++++- .../ui/screen/loggedIn/home/HomeScreen.kt | 24 +++++++- 3 files changed, 81 insertions(+), 43 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt index 5d2db649d5..acab585c4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt @@ -26,32 +26,38 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.unit.Velocity /** - * Single nested-scroll connection that hides/reveals the top and bottom bars together. + * Scroll-linked connection that hides/reveals the top and bottom bars together. * - * Behaviour: - * - onPreScroll: consumes the portion of the scroll delta used to move the bars - * (both hide and reveal), returning the exact amount absorbed so the list never - * "loses pixels" at the edge of the bars' travel range. - * - onPostFling: snaps a mid-way bar to the nearest edge, continuing the fling's - * tail velocity so the settle motion feels like part of the fling, not a second - * animation after it. No velocity is returned upward. + * Philosophy: the bars never consume scroll input. They simply ride along with the + * content — their offset changes at the same rate as the scroll, so the user keeps full + * control of the list with their finger. This mirrors the behaviour of Twitter, Instagram, + * Bluesky, etc., where content scrolling is never delayed by the chrome. + * + * - onPostScroll reads `consumed.y + available.y` (the total scroll attempt that entered + * the nested-scroll chain) and updates the bar offsets. Using the sum means the bars + * also respond to overscroll attempts at the list edges. + * - onPostFling snaps a mid-way bar to the nearest edge, using the fling's remaining + * velocity as the spring's initial velocity so the settle feels continuous. No velocity + * is returned upward to avoid phantom scrolls on parent containers. */ class DisappearingBarNestedScroll( private val state: DisappearingBarState, private val canScroll: () -> Boolean, private val reverseLayout: Boolean, ) : NestedScrollConnection { - override fun onPreScroll( + override fun onPostScroll( + consumed: Offset, available: Offset, source: NestedScrollSource, ): Offset { if (!canScroll()) return Offset.Zero - if (available.y == 0f) return Offset.Zero + val totalY = consumed.y + available.y + if (totalY == 0f) return Offset.Zero - val deltaY = if (reverseLayout) -available.y else available.y - val applied = applyDelta(deltaY) - if (applied == 0f) return Offset.Zero - return Offset(0f, if (reverseLayout) -applied else applied) + val deltaY = if (reverseLayout) -totalY else totalY + applyDelta(deltaY) + // Never consume: the content scrolls freely while the bars slide along. + return Offset.Zero } override suspend fun onPostFling( @@ -59,36 +65,16 @@ class DisappearingBarNestedScroll( available: Velocity, ): Velocity { if (canScroll()) { - // Feed the fling's remaining velocity into the settle so the bar keeps - // moving in the same direction rather than starting a fresh animation. val velocityY = if (reverseLayout) -available.y else available.y state.settleToNearestEdge(initialVelocityY = velocityY) } - // Swallow any residual velocity so parents don't get a phantom fling kick. return Velocity.Zero } - /** - * Applies the given delta (in content-space – negative hides, positive reveals) to - * both bar offsets, clamped to their travel range. - * - * Returns the delta that was actually absorbed, using the side with the larger - * absorption so consumption reporting remains accurate when the two bars have - * different remaining travel. - */ - private fun applyDelta(deltaY: Float): Float { - val prevTop = state.topHeightOffset - val prevBottom = state.bottomHeightOffset + private fun applyDelta(deltaY: Float) { val topLimit = state.topHeightLimit val bottomLimit = state.bottomHeightLimit - - val newTop = (prevTop + deltaY).coerceIn(-topLimit, 0f) - val newBottom = (prevBottom + deltaY).coerceIn(-bottomLimit, 0f) - state.topHeightOffset = newTop - state.bottomHeightOffset = newBottom - - val topDelta = newTop - prevTop - val bottomDelta = newBottom - prevBottom - return if (deltaY < 0f) minOf(topDelta, bottomDelta) else maxOf(topDelta, bottomDelta) + state.topHeightOffset = (state.topHeightOffset + deltaY).coerceIn(-topLimit, 0f) + state.bottomHeightOffset = (state.bottomHeightOffset + deltaY).coerceIn(-bottomLimit, 0f) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index 4620d750aa..dd047f5838 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -23,7 +23,10 @@ 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.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -52,6 +55,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R @@ -264,7 +268,7 @@ private fun DiscoverPages( }, accountViewModel = accountViewModel, ) { - HorizontalPager(state = pagerState, contentPadding = it) { page -> + HorizontalPager(state = pagerState) { page -> if (page >= 0 && page < feedTabs.size) { val tab = feedTabs[page] RefresheableBox(tab.feedState, true) { @@ -275,6 +279,7 @@ private fun DiscoverPages( routeForLastRead = tab.routeForLastRead, forceEventKind = tab.forceEventKind, listState = listState, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -286,6 +291,7 @@ private fun DiscoverPages( routeForLastRead = tab.routeForLastRead, forceEventKind = tab.forceEventKind, listState = listState, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -303,6 +309,7 @@ private fun RenderDiscoverFeed( routeForLastRead: String?, forceEventKind: Int?, listState: LazyGridState, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { @@ -329,6 +336,7 @@ private fun RenderDiscoverFeed( routeForLastRead, listState, forceEventKind, + scaffoldPadding, accountViewModel, nav, ) @@ -391,6 +399,7 @@ private fun RenderDiscoverFeed( routeForLastRead: String?, forceEventKind: Int?, listState: LazyListState, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { @@ -417,6 +426,7 @@ private fun RenderDiscoverFeed( routeForLastRead, listState, forceEventKind, + scaffoldPadding, accountViewModel, nav, ) @@ -460,13 +470,25 @@ private fun DiscoverFeedLoaded( routeForLastRead: String?, listState: LazyListState, forceEventKind: Int?, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { val items by loaded.feed.collectAsStateWithLifecycle() + val layoutDirection = LocalLayoutDirection.current + val listPadding = + remember(scaffoldPadding, layoutDirection) { + PaddingValues( + start = scaffoldPadding.calculateStartPadding(layoutDirection), + top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), + end = scaffoldPadding.calculateEndPadding(layoutDirection), + bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), + ) + } + LazyColumn( - contentPadding = FeedPadding, + contentPadding = listPadding, state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> @@ -495,14 +517,26 @@ private fun DiscoverFeedColumnsLoaded( routeForLastRead: String?, listState: LazyGridState, forceEventKind: Int?, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { val items by loaded.feed.collectAsStateWithLifecycle() + val layoutDirection = LocalLayoutDirection.current + val gridPadding = + remember(scaffoldPadding, layoutDirection) { + PaddingValues( + start = scaffoldPadding.calculateStartPadding(layoutDirection), + top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), + end = scaffoldPadding.calculateEndPadding(layoutDirection), + bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), + ) + } + LazyVerticalGrid( columns = GridCells.Fixed(2), - contentPadding = FeedPadding, + contentPadding = gridPadding, state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 77adf8355f..2e838b1092 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -24,8 +24,11 @@ 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.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn @@ -51,7 +54,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection 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 @@ -211,7 +216,6 @@ private fun HomePages( accountViewModel = accountViewModel, ) { HorizontalPager( - contentPadding = it, state = pagerState, userScrollEnabled = true, modifier = @@ -225,6 +229,7 @@ private fun HomePages( routeForLastRead = tabs[page].routeForLastRead, scrollStateKey = tabs[page].scrollStateKey, liveSection = tabs[page].liveSection, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -278,6 +283,7 @@ fun HomeFeeds( enablePullRefresh: Boolean = true, scrollStateKey: String? = null, liveSection: ChannelFeedContentState? = null, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), accountViewModel: AccountViewModel, nav: INav, ) { @@ -289,7 +295,7 @@ fun HomeFeeds( listState = listState, nav = nav, routeForLastRead = routeForLastRead, - onLoaded = { FeedLoaded(it, listState, routeForLastRead, liveSection, accountViewModel, nav) }, + onLoaded = { FeedLoaded(it, listState, routeForLastRead, liveSection, scaffoldPadding, accountViewModel, nav) }, onEmpty = { HomeFeedEmpty(feedState::invalidateData) }, ) } @@ -303,13 +309,25 @@ 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 layoutDirection = LocalLayoutDirection.current + val listPadding = + remember(scaffoldPadding, layoutDirection) { + PaddingValues( + start = scaffoldPadding.calculateStartPadding(layoutDirection), + top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), + end = scaffoldPadding.calculateEndPadding(layoutDirection), + bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), + ) + } + LazyColumn( - contentPadding = FeedPadding, + contentPadding = listPadding, state = listState, ) { if (liveSection != null) { From f1cbf808267117d0d1c8c6e7ed3ca3e8f7e3b03c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 15:59:33 +0000 Subject: [PATCH 4/9] fix(ui): opaque tab rows and bottom-bar under nav inset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the disappearing bars translate on top of content, any translucent chrome lets items bleed through — which the user noticed on Home: posts became visible through the transparent SecondaryTabRow, and the strip under Android's gesture bar (covered by windowInsetsPadding on the bottom nav) was transparent too. - Swap `containerColor = Color.Transparent` to `MaterialTheme.colorScheme.background` on all tab rows that sit in a DisappearingScaffold topBar slot (Home, Discover, Polls, Community, ChatroomListTabs, OldBookmarkList). - Paint AppBottomBar's outer Column with the background before the windowInsetsPadding, so the system-gesture-bar strip at the bottom is opaque and items scrolling behind it are hidden. FollowPackFeedScreen intentionally uses a translucent (alpha 0.6f) top bar as a design choice, so it's left as-is. https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa --- .../amethyst/ui/navigation/bottombars/AppBottomBar.kt | 2 ++ .../loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt | 3 +-- .../ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt | 3 +-- .../amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt | 3 +-- .../amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/home/HomeScreen.kt | 3 +-- .../amethyst/ui/screen/loggedIn/polls/PollsScreen.kt | 3 +-- 7 files changed, 8 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt index 0b2ac67dd5..f98f1c3679 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.navigation.bottombars import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope @@ -71,6 +72,7 @@ private fun RenderBottomMenu( modifier = Modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) .windowInsetsPadding(windowInsets) .consumeWindowInsets(windowInsets) .height(50.dp), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt index 3e0b8176a3..39f5a2dcc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt @@ -43,7 +43,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -110,7 +109,7 @@ private fun RenderOldBookmarkScreen( Column { TopBarWithBackButton(stringRes(id = R.string.old_bookmarks_title), nav::popBack) SecondaryTabRow( - containerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, selectedTabIndex = pagerState.currentPage, modifier = TabRowHeight, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index d168e99edb..8168e1c648 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -46,7 +46,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.components.M3ActionDialog @@ -81,7 +80,7 @@ fun MessagesTabHeader( Box(Modifier.fillMaxWidth()) { SecondaryTabRow( - containerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, selectedTabIndex = pagerState.currentPage, modifier = TabRowHeight, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt index 3edfcadb8e..48f7507331 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt @@ -40,7 +40,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R @@ -168,7 +167,7 @@ fun CommunityScreen( } SecondaryTabRow( - containerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, modifier = TabRowHeight, selectedTabIndex = pagerState.currentPage, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index dd047f5838..41fa39a147 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -224,7 +224,7 @@ private fun DiscoverPages( Column { DiscoveryTopBar(accountViewModel, nav) SecondaryScrollableTabRow( - containerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, selectedTabIndex = pagerState.currentPage, modifier = TabRowHeight, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 2e838b1092..e2a78696aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -53,7 +53,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -185,7 +184,7 @@ private fun HomePages( Column { HomeTopBar(accountViewModel, nav) SecondaryTabRow( - containerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, modifier = TabRowHeight, selectedTabIndex = pagerState.currentPage, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt index 0b512d29c2..240751b241 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.graphics.Color import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState @@ -131,7 +130,7 @@ private fun PollsPages( Column { PollsTopBar(accountViewModel, nav) SecondaryTabRow( - containerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, modifier = TabRowHeight, selectedTabIndex = pagerState.currentPage, From 53cec9a6613371be525b917013afd3a8a19ea99c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 22:42:52 +0000 Subject: [PATCH 5/9] fix(ui): thread scaffold padding into innermost LazyColumn/Grid Continues the "scroll-under-bars" migration started for Home/Discover: the scaffold's PaddingValues is now applied as the inner LazyColumn / LazyVerticalGrid's contentPadding instead of Modifier.padding(it) on a wrapping Column or HorizontalPager's contentPadding. That lets pages/content extend the full height of the screen and scroll behind the bars, so the bars can hide without leaving the background-colored strip users were seeing. Shared infrastructure (opt-in, default PaddingValues(0) so external callers are unaffected): - feeds/FeedLoaded.kt adds `scaffoldPadding` - screen/FeedView.kt (`RefresheableFeedView`, `RenderFeedState`) adds `scaffoldPadding` and threads it to the default onLoaded - feeds/FeedContentStateView.kt (`RefresheableFeedContentStateView`, `RenderFeedContentState`) adds `scaffoldPadding` - screen/UserFeedView.kt (`RefreshingFeedUserFeedView`, `UserFeedView`) adds `scaffoldPadding` - notifications/CardFeedView.kt (`RenderCardFeed`) adds `scaffoldPadding` and a `headerContent` slot so NotificationScreen can show its inbox relay warning as the LazyColumn's first item instead of a padded outer Column - Per-surface FeedLoaded composables (Shorts, Pictures, Articles, Longs, Products) take `scaffoldPadding` Helper added: `ui/layouts/PaddingMerge.kt` exposes a `rememberMergedPadding(outer, inner)` so each inner list can combine the scaffold padding with its own FeedPadding correctly under both LTR and RTL. Consumer changes: - Home, Discover, Polls, Community, FollowPack, OldBookmark (all HorizontalPager pages), BookmarkList (pager), Notification, Video, WebBookmarks, Drafts, PinnedNotes, Relay, Thread, Hashtag, GeoHash, Shorts, Pictures, Articles, Longs, Products: dropped the `Modifier.padding(it)` wrapper or the `contentPadding = it` on the pager and pass `scaffoldPadding = it` into the feed instead. - BookmarkListScreen tab row switched to opaque background to match the others. Left unchanged on purpose: - Settings screens (non-scrollable, bars never hide). - Chat screens (inverted layout, different interaction model). - SearchScreen (header-and-feed pattern; migration is larger than this pass and is worth its own change). - FollowPackFeedScreen top bar stays translucent by design. https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa --- .../amethyst/ui/feeds/FeedContentStateView.kt | 8 ++- .../amethyst/ui/feeds/FeedLoaded.kt | 6 ++- .../amethyst/ui/layouts/PaddingMerge.kt | 50 +++++++++++++++++++ .../amethyst/ui/screen/FeedView.kt | 8 ++- .../amethyst/ui/screen/UserFeedView.kt | 12 +++-- .../loggedIn/articles/ArticlesFeedLoaded.kt | 6 ++- .../loggedIn/articles/ArticlesScreen.kt | 40 +++++++-------- .../default/BookmarkListScreen.kt | 42 ++++++++-------- .../old/OldBookmarkListScreen.kt | 39 +++++++-------- .../loggedIn/communities/CommunityScreen.kt | 3 +- .../loggedIn/discover/DiscoverScreen.kt | 30 ++--------- .../screen/loggedIn/drafts/DraftListScreen.kt | 32 ++++++------ .../followPacks/feed/FollowPackFeedScreen.kt | 4 +- .../screen/loggedIn/geohash/GeoHashScreen.kt | 17 +++---- .../screen/loggedIn/hashtag/HashtagScreen.kt | 17 +++---- .../ui/screen/loggedIn/home/HomeScreen.kt | 15 +----- .../screen/loggedIn/longs/LongsFeedLoaded.kt | 5 +- .../ui/screen/loggedIn/longs/LongsScreen.kt | 40 +++++++-------- .../loggedIn/notifications/CardFeedView.kt | 14 +++++- .../notifications/NotificationScreen.kt | 26 +++++----- .../loggedIn/pictures/PictureFeedLoaded.kt | 5 +- .../loggedIn/pictures/PicturesScreen.kt | 40 +++++++-------- .../loggedIn/pinnednotes/PinnedNotesScreen.kt | 19 +++---- .../ui/screen/loggedIn/polls/PollsScreen.kt | 2 +- .../loggedIn/products/ProductsFeedLoaded.kt | 8 ++- .../loggedIn/products/ProductsScreen.kt | 22 ++++---- .../screen/loggedIn/relay/RelayFeedScreen.kt | 17 +++---- .../loggedIn/shorts/ShortsFeedLoaded.kt | 5 +- .../ui/screen/loggedIn/shorts/ShortsScreen.kt | 40 +++++++-------- .../loggedIn/threadview/ThreadFeedView.kt | 7 ++- .../loggedIn/threadview/ThreadScreen.kt | 7 +-- .../ui/screen/loggedIn/video/VideoScreen.kt | 27 +++++----- .../webBookmarks/WebBookmarksScreen.kt | 30 +++++------ 33 files changed, 335 insertions(+), 308 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/PaddingMerge.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt index fb49009cbc..3c66d9f5d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt @@ -21,12 +21,14 @@ 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 @@ -39,12 +41,13 @@ 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) + RenderFeedContentState(feedContentState, accountViewModel, listState, nav, routeForLastRead, scaffoldPadding) } } } @@ -92,7 +95,8 @@ fun RenderFeedContentState( listState: LazyListState, nav: INav, routeForLastRead: String?, - onLoaded: @Composable (FeedState.Loaded) -> Unit = { FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav) }, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), + onLoaded: @Composable (FeedState.Loaded) -> Unit = { FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav, scaffoldPadding) }, onEmpty: @Composable () -> Unit = { FeedEmpty(feedContentState::invalidateData) }, onError: @Composable (String) -> Unit = { FeedError(it, feedContentState::invalidateData) }, onLoading: @Composable () -> Unit = { LoadingFeed() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt index e55d48f38c..cc326c0205 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt @@ -21,6 +21,7 @@ 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 @@ -30,8 +31,10 @@ 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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -46,11 +49,12 @@ fun FeedLoaded( routeForLastRead: String?, accountViewModel: AccountViewModel, nav: INav, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), ) { val items by loaded.feed.collectAsStateWithLifecycle() LazyColumn( - contentPadding = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/PaddingMerge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/PaddingMerge.kt new file mode 100644 index 0000000000..94383fb9b7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/PaddingMerge.kt @@ -0,0 +1,50 @@ +/* + * 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.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalLayoutDirection + +/** + * 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. + */ +@Composable +fun rememberMergedPadding( + outer: PaddingValues, + inner: PaddingValues, +): PaddingValues { + val layoutDirection = LocalLayoutDirection.current + return remember(outer, inner, layoutDirection) { + PaddingValues( + start = outer.calculateStartPadding(layoutDirection) + inner.calculateStartPadding(layoutDirection), + top = outer.calculateTopPadding() + inner.calculateTopPadding(), + end = outer.calculateEndPadding(layoutDirection) + inner.calculateEndPadding(layoutDirection), + bottom = outer.calculateBottomPadding() + inner.calculateBottomPadding(), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index ac14f13239..2928e32f2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -21,12 +21,14 @@ 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 @@ -48,12 +50,13 @@ 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) + RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead, scaffoldPadding) } } } @@ -101,8 +104,9 @@ fun RenderFeedState( listState: LazyListState, nav: INav, routeForLastRead: String?, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), onLoaded: @Composable (FeedState.Loaded) -> Unit = { - FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav) + FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav, scaffoldPadding) }, onEmpty: @Composable () -> Unit = { FeedEmpty { viewModel.invalidateData() } }, onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index 652c44eb60..1fedc9a076 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -21,6 +21,7 @@ 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 @@ -29,12 +30,14 @@ 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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -47,13 +50,15 @@ fun RefreshingFeedUserFeedView( accountViewModel: AccountViewModel, nav: INav, enablePullRefresh: Boolean = true, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), ) { - RefresheableBox(viewModel, enablePullRefresh) { UserFeedView(viewModel, accountViewModel, nav) } + RefresheableBox(viewModel, enablePullRefresh) { UserFeedView(viewModel, scaffoldPadding, accountViewModel, nav) } } @Composable fun UserFeedView( viewModel: UserFeedViewModel, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), accountViewModel: AccountViewModel, nav: INav, ) { @@ -70,7 +75,7 @@ fun UserFeedView( } is UserFeedState.Loaded -> { - FeedLoaded(state, accountViewModel, nav) + FeedLoaded(state, scaffoldPadding, accountViewModel, nav) } is UserFeedState.Loading -> { @@ -83,6 +88,7 @@ fun UserFeedView( @Composable private fun FeedLoaded( state: UserFeedState.Loaded, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { @@ -91,7 +97,7 @@ private fun FeedLoaded( LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed(items, key = { _, item -> item.pubkeyHex }) { _, item -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt index b2f20754a1..d369773e39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt @@ -20,6 +20,7 @@ */ 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 @@ -29,8 +30,10 @@ 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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose @@ -42,13 +45,14 @@ 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 = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt index 43a04ebfb9..35a0d0a83c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt @@ -20,12 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.articles -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox @@ -81,25 +78,24 @@ fun ArticlesScreen( }, accountViewModel = accountViewModel, ) { paddingValues -> - Column(Modifier.padding(paddingValues)) { - RefresheableBox(articlesFeedContentState, true) { - SaveableFeedContentState(articlesFeedContentState, scrollStateKey = ScrollStateKeys.ARTICLES_SCREEN) { listState -> - RenderFeedContentState( - feedContentState = articlesFeedContentState, - accountViewModel = accountViewModel, - listState = listState, - nav = nav, - routeForLastRead = "ArticlesFeed", - onLoaded = { loaded -> - ArticlesFeedLoaded( - loaded = loaded, - listState = listState, - accountViewModel = accountViewModel, - nav = nav, - ) - }, - ) - } + RefresheableBox(articlesFeedContentState, true) { + SaveableFeedContentState(articlesFeedContentState, scrollStateKey = ScrollStateKeys.ARTICLES_SCREEN) { listState -> + RenderFeedContentState( + feedContentState = articlesFeedContentState, + accountViewModel = accountViewModel, + listState = listState, + nav = nav, + routeForLastRead = "ArticlesFeed", + onLoaded = { loaded -> + ArticlesFeedLoaded( + loaded = loaded, + listState = listState, + scaffoldPadding = paddingValues, + accountViewModel = accountViewModel, + nav = nav, + ) + }, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt index 7ee7752c32..67b921860e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme @@ -37,7 +36,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R @@ -101,7 +99,7 @@ private fun RenderBookmarkScreen( Column { TopBarWithBackButton(stringRes(id = R.string.bookmarks_title), nav::popBack) SecondaryTabRow( - containerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, selectedTabIndex = pagerState.currentPage, modifier = TabRowHeight, @@ -121,26 +119,26 @@ private fun RenderBookmarkScreen( }, accountViewModel = accountViewModel, ) { - Column(Modifier.padding(it).fillMaxHeight()) { - HorizontalPager(state = pagerState) { page -> - when (page) { - 0 -> { - RefresheableFeedView( - privateFeedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + HorizontalPager(state = pagerState, modifier = Modifier.fillMaxHeight()) { page -> + when (page) { + 0 -> { + RefresheableFeedView( + privateFeedViewModel, + null, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } - 1 -> { - RefresheableFeedView( - publicFeedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + 1 -> { + RefresheableFeedView( + publicFeedViewModel, + null, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt index 39f5a2dcc3..a04dc7e27e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt @@ -25,7 +25,6 @@ import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons @@ -154,26 +153,26 @@ private fun RenderOldBookmarkScreen( }, accountViewModel = accountViewModel, ) { - Column(Modifier.padding(it).fillMaxHeight()) { - HorizontalPager(state = pagerState) { page -> - when (page) { - 0 -> { - RefresheableFeedView( - privateFeedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + HorizontalPager(state = pagerState, modifier = Modifier.fillMaxHeight()) { page -> + when (page) { + 0 -> { + RefresheableFeedView( + privateFeedViewModel, + null, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } - 1 -> { - RefresheableFeedView( - publicFeedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + 1 -> { + RefresheableFeedView( + publicFeedViewModel, + null, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt index 48f7507331..80e67f2104 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt @@ -191,7 +191,6 @@ fun CommunityScreen( accountViewModel = accountViewModel, ) { HorizontalPager( - contentPadding = it, state = pagerState, ) { page -> when (page) { @@ -199,6 +198,7 @@ fun CommunityScreen( RefresheableFeedView( feedViewModel, null, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -208,6 +208,7 @@ fun CommunityScreen( RefresheableFeedView( modFeedViewModel, null, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index 41fa39a147..c6456fda5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -25,8 +25,6 @@ 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.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -55,7 +53,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R @@ -73,6 +70,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.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -476,19 +474,8 @@ private fun DiscoverFeedLoaded( ) { val items by loaded.feed.collectAsStateWithLifecycle() - val layoutDirection = LocalLayoutDirection.current - val listPadding = - remember(scaffoldPadding, layoutDirection) { - PaddingValues( - start = scaffoldPadding.calculateStartPadding(layoutDirection), - top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), - end = scaffoldPadding.calculateEndPadding(layoutDirection), - bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), - ) - } - LazyColumn( - contentPadding = listPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> @@ -523,20 +510,9 @@ private fun DiscoverFeedColumnsLoaded( ) { val items by loaded.feed.collectAsStateWithLifecycle() - val layoutDirection = LocalLayoutDirection.current - val gridPadding = - remember(scaffoldPadding, layoutDirection) { - PaddingValues( - start = scaffoldPadding.calculateStartPadding(layoutDirection), - top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), - end = scaffoldPadding.calculateEndPadding(layoutDirection), - bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), - ) - } - LazyVerticalGrid( columns = GridCells.Fixed(2), - contentPadding = gridPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index 8d6df52e0b..ca9bca18c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -21,11 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed @@ -56,6 +54,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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon @@ -147,19 +146,17 @@ private fun RenderDraftListScreen( ) }, accountViewModel = accountViewModel, - ) { - Column(Modifier.padding(it).fillMaxHeight()) { - RefresheableBox(feedState) { - SaveableFeedState(feedState, DRAFTS) { listState -> - RenderFeedContentState( - feedContentState = feedState, - accountViewModel = accountViewModel, - listState = listState, - nav = nav, - routeForLastRead = null, - onLoaded = { DraftFeedLoaded(it, listState, accountViewModel, nav) }, - ) - } + ) { scaffoldPadding -> + RefresheableBox(feedState) { + SaveableFeedState(feedState, DRAFTS) { listState -> + RenderFeedContentState( + feedContentState = feedState, + accountViewModel = accountViewModel, + listState = listState, + nav = nav, + routeForLastRead = null, + onLoaded = { DraftFeedLoaded(it, listState, scaffoldPadding, accountViewModel, nav) }, + ) } } } @@ -169,13 +166,14 @@ 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 = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index 25bd378ba9..b4157f6f36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -172,7 +172,6 @@ fun FollowPackFeedScreen( accountViewModel = accountViewModel, ) { HorizontalPager( - contentPadding = it, state = pagerState, ) { page -> when (page) { @@ -180,6 +179,7 @@ fun FollowPackFeedScreen( RefresheableFeedView( newThreadFeedViewModel, null, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -189,6 +189,7 @@ fun FollowPackFeedScreen( RefresheableFeedView( conversationsFeedViewModel, null, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -197,6 +198,7 @@ fun FollowPackFeedScreen( 2 -> { UserFeedView( membersFeedViewModel, + it, accountViewModel, nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 96872d1a1c..80a42b4663 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -21,8 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash import android.annotation.SuppressLint -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -102,14 +100,13 @@ fun GeoHashScreen( }, accountViewModel = accountViewModel, ) { - Column(Modifier.padding(it)) { - RefresheableFeedView( - feedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RefresheableFeedView( + feedViewModel, + null, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index f5b862ecb6..e0cf66286c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -23,10 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag import android.annotation.SuppressLint import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -107,14 +105,13 @@ fun HashtagScreen( }, accountViewModel = accountViewModel, ) { - Column(Modifier.padding(it)) { - RefresheableFeedView( - feedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RefresheableFeedView( + feedViewModel, + null, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index e2a78696aa..c02e2216e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -27,8 +27,6 @@ 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.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn @@ -53,7 +51,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -78,6 +75,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.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -314,16 +312,7 @@ fun FeedLoaded( ) { val items by loaded.feed.collectAsStateWithLifecycle() - val layoutDirection = LocalLayoutDirection.current - val listPadding = - remember(scaffoldPadding, layoutDirection) { - PaddingValues( - start = scaffoldPadding.calculateStartPadding(layoutDirection), - top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), - end = scaffoldPadding.calculateEndPadding(layoutDirection), - bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), - ) - } + val listPadding = rememberMergedPadding(scaffoldPadding, FeedPadding) LazyColumn( contentPadding = listPadding, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt index 23639b9606..ba9c8ee304 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt @@ -20,6 +20,7 @@ */ 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 @@ -32,6 +33,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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.VideoCardCompose @@ -43,13 +45,14 @@ 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 = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt index f79711e742..c6b079599a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt @@ -20,12 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.longs -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox @@ -81,25 +78,24 @@ fun LongsScreen( }, accountViewModel = accountViewModel, ) { paddingValues -> - Column(Modifier.padding(paddingValues)) { - RefresheableBox(longsFeedContentState, true) { - SaveableFeedContentState(longsFeedContentState, scrollStateKey = ScrollStateKeys.LONGS_SCREEN) { listState -> - RenderFeedContentState( - feedContentState = longsFeedContentState, - accountViewModel = accountViewModel, - listState = listState, - nav = nav, - routeForLastRead = "LongsFeed", - onLoaded = { loaded -> - LongsFeedLoaded( - loaded = loaded, - listState = listState, - accountViewModel = accountViewModel, - nav = nav, - ) - }, - ) - } + RefresheableBox(longsFeedContentState, true) { + SaveableFeedContentState(longsFeedContentState, scrollStateKey = ScrollStateKeys.LONGS_SCREEN) { listState -> + RenderFeedContentState( + feedContentState = longsFeedContentState, + accountViewModel = accountViewModel, + listState = listState, + nav = nav, + routeForLastRead = "LongsFeed", + onLoaded = { loaded -> + LongsFeedLoaded( + loaded = loaded, + listState = listState, + scaffoldPadding = paddingValues, + accountViewModel = accountViewModel, + nav = nav, + ) + }, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index d7dd94423a..8d3dddeb5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -26,6 +26,7 @@ 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 @@ -51,6 +52,7 @@ 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 @@ -59,6 +61,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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.BadgeCompose import com.vitorpamplona.amethyst.ui.note.CloseIcon @@ -86,6 +89,8 @@ fun RenderCardFeed( nav: INav, routeForLastRead: String, scrollToEventId: String? = null, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), + headerContent: (@Composable () -> Unit)? = null, ) { val feedState by feedContent.feedContent.collectAsStateWithLifecycle() @@ -113,6 +118,8 @@ fun RenderCardFeed( accountViewModel = accountViewModel, nav = nav, scrollToEventId = scrollToEventId, + scaffoldPadding = scaffoldPadding, + headerContent = headerContent, ) } @@ -146,6 +153,8 @@ 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() val openPolls by polls.flow.collectAsStateWithLifecycle() @@ -170,9 +179,12 @@ private fun FeedLoaded( LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { + if (headerContent != null) { + item(key = "scaffold-header") { headerContent() } + } item { ShowDonationCard(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index 41d1f41833..a443359121 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -21,12 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider @@ -92,17 +89,22 @@ fun NotificationScreen( }, accountViewModel = accountViewModel, ) { - Column( - modifier = Modifier.padding(it).consumeWindowInsets(it), - ) { - ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) - RefresheableBox(notifFeedContentState, true) { - val listState = rememberForeverLazyListState(ScrollStateKeys.NOTIFICATION_SCREEN) + RefresheableBox(notifFeedContentState, true) { + val listState = rememberForeverLazyListState(ScrollStateKeys.NOTIFICATION_SCREEN) - WatchScrollToTop(notifFeedContentState, listState) + WatchScrollToTop(notifFeedContentState, listState) - RenderCardFeed(notifFeedContentState, notifPolls, accountViewModel, listState, nav, "Notification", scrollToEventId) - } + RenderCardFeed( + feedContent = notifFeedContentState, + pollContent = notifPolls, + accountViewModel = accountViewModel, + listState = listState, + nav = nav, + routeForLastRead = "Notification", + scrollToEventId = scrollToEventId, + scaffoldPadding = it, + headerContent = { ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) }, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt index f6c61424a7..5a971b86e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt @@ -20,6 +20,7 @@ */ 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 @@ -32,6 +33,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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -42,13 +44,14 @@ 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 = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt index 89b1629d6f..866fd310c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt @@ -20,12 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox @@ -81,25 +78,24 @@ fun PicturesScreen( }, accountViewModel = accountViewModel, ) { paddingValues -> - Column(Modifier.padding(paddingValues)) { - RefresheableBox(picturesFeedContentState, true) { - SaveableFeedContentState(picturesFeedContentState, scrollStateKey = ScrollStateKeys.PICTURES_SCREEN) { listState -> - RenderFeedContentState( - feedContentState = picturesFeedContentState, - accountViewModel = accountViewModel, - listState = listState, - nav = nav, - routeForLastRead = "PicturesFeed", - onLoaded = { loaded -> - PictureFeedLoaded( - loaded = loaded, - listState = listState, - accountViewModel = accountViewModel, - nav = nav, - ) - }, - ) - } + RefresheableBox(picturesFeedContentState, true) { + SaveableFeedContentState(picturesFeedContentState, scrollStateKey = ScrollStateKeys.PICTURES_SCREEN) { listState -> + RenderFeedContentState( + feedContentState = picturesFeedContentState, + accountViewModel = accountViewModel, + listState = listState, + nav = nav, + routeForLastRead = "PicturesFeed", + onLoaded = { loaded -> + PictureFeedLoaded( + loaded = loaded, + listState = listState, + scaffoldPadding = paddingValues, + accountViewModel = accountViewModel, + nav = nav, + ) + }, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pinnednotes/PinnedNotesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pinnednotes/PinnedNotesScreen.kt index 97b1880d1d..2c613011a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pinnednotes/PinnedNotesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pinnednotes/PinnedNotesScreen.kt @@ -20,15 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.pinnednotes -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R @@ -79,14 +75,13 @@ private fun RenderPinnedNotesScreen( }, accountViewModel = accountViewModel, ) { - Column(Modifier.padding(it).fillMaxHeight()) { - RefresheableFeedView( - pinnedNotesFeedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RefresheableFeedView( + pinnedNotesFeedViewModel, + null, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt index 240751b241..cb102fa5a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt @@ -161,7 +161,6 @@ private fun PollsPages( accountViewModel = accountViewModel, ) { HorizontalPager( - contentPadding = it, state = pagerState, userScrollEnabled = true, ) { page -> @@ -173,6 +172,7 @@ private fun PollsPages( listState = listState, nav = nav, routeForLastRead = tabs[page].routeForLastRead, + scaffoldPadding = it, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt index 68dbb1af9d..4469ebf528 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt @@ -22,6 +22,7 @@ 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 @@ -32,6 +33,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.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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose @@ -50,6 +53,7 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent fun RenderProductsFeed( feedContentState: FeedContentState, gridState: LazyGridState, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), accountViewModel: AccountViewModel, nav: INav, ) { @@ -74,6 +78,7 @@ fun RenderProductsFeed( ProductsFeedColumnsLoaded( state, gridState, + scaffoldPadding, accountViewModel, nav, ) @@ -91,6 +96,7 @@ fun RenderProductsFeed( private fun ProductsFeedColumnsLoaded( loaded: FeedState.Loaded, listState: LazyGridState, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { @@ -98,7 +104,7 @@ private fun ProductsFeedColumnsLoaded( LazyVerticalGrid( columns = GridCells.Fixed(2), - contentPadding = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt index 4d2e121605..977e6f30a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt @@ -20,12 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.products -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox @@ -80,16 +77,15 @@ fun ProductsScreen( }, accountViewModel = accountViewModel, ) { paddingValues -> - Column(Modifier.padding(paddingValues)) { - RefresheableBox(productsFeedContentState, true) { - SaveableGridFeedContentState(productsFeedContentState, scrollStateKey = ScrollStateKeys.PRODUCTS_SCREEN) { gridState -> - RenderProductsFeed( - feedContentState = productsFeedContentState, - gridState = gridState, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RefresheableBox(productsFeedContentState, true) { + SaveableGridFeedContentState(productsFeedContentState, scrollStateKey = ScrollStateKeys.PRODUCTS_SCREEN) { gridState -> + RenderProductsFeed( + feedContentState = productsFeedContentState, + gridState = gridState, + scaffoldPadding = paddingValues, + accountViewModel = accountViewModel, + nav = nav, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt index fa56b9875d..05a67ba266 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt @@ -21,8 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relay import android.annotation.SuppressLint -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -105,14 +103,13 @@ fun RelayFeedScreen( }, accountViewModel = accountViewModel, ) { - Column(Modifier.padding(it)) { - RefresheableFeedView( - feedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RefresheableFeedView( + feedViewModel, + null, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt index e68e075be0..b930b17e8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt @@ -20,6 +20,7 @@ */ 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 @@ -32,6 +33,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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -42,13 +44,14 @@ 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 = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt index a993b7f167..3bcfc9becd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt @@ -20,12 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox @@ -81,25 +78,24 @@ fun ShortsScreen( }, accountViewModel = accountViewModel, ) { paddingValues -> - Column(Modifier.padding(paddingValues)) { - RefresheableBox(shortsFeedContentState, true) { - SaveableFeedContentState(shortsFeedContentState, scrollStateKey = ScrollStateKeys.SHORTS_SCREEN) { listState -> - RenderFeedContentState( - feedContentState = shortsFeedContentState, - accountViewModel = accountViewModel, - listState = listState, - nav = nav, - routeForLastRead = "ShortsFeed", - onLoaded = { loaded -> - ShortsFeedLoaded( - loaded = loaded, - listState = listState, - accountViewModel = accountViewModel, - nav = nav, - ) - }, - ) - } + RefresheableBox(shortsFeedContentState, true) { + SaveableFeedContentState(shortsFeedContentState, scrollStateKey = ScrollStateKeys.SHORTS_SCREEN) { listState -> + RenderFeedContentState( + feedContentState = shortsFeedContentState, + accountViewModel = accountViewModel, + listState = listState, + nav = nav, + routeForLastRead = "ShortsFeed", + onLoaded = { loaded -> + ShortsFeedLoaded( + loaded = loaded, + listState = listState, + scaffoldPadding = paddingValues, + accountViewModel = accountViewModel, + nav = nav, + ) + }, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 40e49b4661..c4d01acb26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -85,6 +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.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -297,6 +298,7 @@ import kotlinx.coroutines.withContext fun ThreadFeedView( noteId: String, viewModel: LevelFeedViewModel, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), accountViewModel: AccountViewModel, nav: INav, ) { @@ -308,7 +310,7 @@ fun ThreadFeedView( nav = nav, routeForLastRead = null, onLoaded = { - RenderThreadFeed(noteId, it, viewModel.llState, viewModel, accountViewModel, nav) + RenderThreadFeed(noteId, it, viewModel.llState, viewModel, scaffoldPadding, accountViewModel, nav) }, ) } @@ -320,6 +322,7 @@ fun RenderThreadFeed( loaded: FeedState.Loaded, listState: LazyListState, viewModel: LevelFeedViewModel, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { @@ -356,7 +359,7 @@ fun RenderThreadFeed( LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt index 065d98839c..2d9f07f7c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt @@ -20,11 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription @@ -72,8 +69,6 @@ fun ThreadScreen( }, accountViewModel = accountViewModel, ) { - Column(Modifier.padding(it)) { - ThreadFeedView(noteId, feedViewModel, accountViewModel, nav) - } + ThreadFeedView(noteId, feedViewModel, it, accountViewModel, nav) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 30ccc67f33..6fc53d2ba5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -20,11 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed @@ -43,6 +41,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.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -97,16 +96,13 @@ fun VideoScreen( }, accountViewModel = accountViewModel, ) { - Column( - modifier = Modifier.padding(it).consumeWindowInsets(it), - ) { - RenderFeed( - videoFeedContentState = videoFeedContentState, - scrollKey = ScrollStateKeys.VIDEO_SCREEN, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RenderFeed( + videoFeedContentState = videoFeedContentState, + scrollKey = ScrollStateKeys.VIDEO_SCREEN, + scaffoldPadding = it, + accountViewModel = accountViewModel, + nav = nav, + ) } } @@ -129,6 +125,7 @@ fun WatchAccountForVideoScreen( private fun RenderFeed( videoFeedContentState: FeedContentState, scrollKey: String?, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { @@ -144,6 +141,7 @@ private fun RenderFeed( VideoFeedLoaded( loaded = loaded, listState = listState, + scaffoldPadding = scaffoldPadding, accountViewModel = accountViewModel, nav = nav, ) @@ -157,13 +155,14 @@ 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 = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt index 526e8b1fa0..fb0d080f9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt @@ -23,9 +23,9 @@ 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -81,6 +81,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.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon @@ -154,19 +155,17 @@ private fun RenderWebBookmarksScreen( } }, accountViewModel = accountViewModel, - ) { - Column(Modifier.padding(it).fillMaxHeight()) { - RefresheableBox(feedState) { - SaveableFeedState(feedState, ScrollStateKeys.WEB_BOOKMARKS) { listState -> - RenderFeedContentState( - feedContentState = feedState, - accountViewModel = accountViewModel, - listState = listState, - nav = nav, - routeForLastRead = null, - onLoaded = { WebBookmarksFeedLoaded(it, listState, accountViewModel, nav) }, - ) - } + ) { scaffoldPadding -> + RefresheableBox(feedState) { + SaveableFeedState(feedState, ScrollStateKeys.WEB_BOOKMARKS) { listState -> + RenderFeedContentState( + feedContentState = feedState, + accountViewModel = accountViewModel, + listState = listState, + nav = nav, + routeForLastRead = null, + onLoaded = { WebBookmarksFeedLoaded(it, listState, scaffoldPadding, accountViewModel, nav) }, + ) } } } @@ -176,13 +175,14 @@ 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 = FeedPadding, + contentPadding = rememberMergedPadding(scaffoldPadding, FeedPadding), state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> From 9b409723bd74d561875006bc330b2bfdf405d83c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 14:59:57 +0000 Subject: [PATCH 6/9] fix(ui): thread scaffold padding through Messages, DVMs, Search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../chats/rooms/feed/ChatroomListFeedView.kt | 12 +++- .../chats/rooms/feed/ChatroomListTabs.kt | 2 +- .../chats/rooms/twopane/ChatroomListPane.kt | 3 +- .../chats/rooms/twopane/MessagesTwoPane.kt | 4 +- .../dvms/DvmContentDiscoveryScreen.kt | 67 ++++++++++--------- .../ui/screen/loggedIn/search/SearchScreen.kt | 32 +++++---- 6 files changed, 67 insertions(+), 53 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index 60d6fc1421..a1c12f74d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index 8168e1c648..f0c52e4a02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt index 14928b2470..ae1e24fe16 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 1e187d6145..7fd398d2a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -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(), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 0d81a4cc8f..0f1488fab5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -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() + } + }, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 2dffe5d4c7..42b8623c86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -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" }, From 3730d2fb735954974b1ccbee81aba69013dc700c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 15:37:21 +0000 Subject: [PATCH 7/9] fix(ui): keep chrome visible on chat detail screens Chat detail screens (DMs, public chat, live-activity chat, ephemeral chat, marmot group) have a compound layout that doesn't play well with the scroll-under-bars model: Column { (optional) relay-warning header weight(1) Column { reverseLayout LazyColumn } Spacer PrivateMessageEditFieldRow // stays above the keyboard, has // its own IME / nav-inset handling } Making the messages scroll behind the top bar while the input field stays planted above the keyboard (and the warning header sits below the bar when present) requires a Box overlay + measured-input-height trick that's incompatible with the existing Column + weight layout. Most chat apps (Telegram, Signal, WhatsApp, etc.) keep chrome pinned on a conversation screen anyway, so the simple and correct answer is to just not hide the chrome here. - New `allowBarHide: Boolean = true` parameter on `DisappearingScaffold`. When false, `canScroll` returns false and the NSC no-ops, which keeps the top bar pinned regardless of `isImmersiveScrollingActive()`. - All 6 chat detail screens pass `allowBarHide = false`: - ChatroomScreen - ChatroomByAuthorScreen - PublicChatChannelScreen - EphemeralChatScreen - LiveActivityChannelScreen - MarmotGroupChatScreen The existing `Column(Modifier.padding(it)...)` layout is correct when the bars are pinned (no strip because the bar never translates), so no other changes needed inside the chat views themselves. https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa --- .../vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt | 3 ++- .../screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt | 1 + .../screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt | 1 + .../ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt | 1 + .../chats/publicChannels/ephemChat/EphemeralChatScreen.kt | 1 + .../publicChannels/nip28PublicChat/PublicChatChannelScreen.kt | 1 + .../nip53LiveActivities/LiveActivityChannelScreen.kt | 1 + 7 files changed, 8 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt index c9b2f728df..9907a85ff0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt @@ -54,12 +54,13 @@ fun DisappearingScaffold( floatingButton: (@Composable () -> Unit)? = null, accountViewModel: AccountViewModel, isActive: () -> Boolean = { true }, + allowBarHide: Boolean = true, mainContent: @Composable (padding: PaddingValues) -> Unit, ) { val state = rememberDisappearingBarState() val canScroll = { - isActive() && accountViewModel.settings.isImmersiveScrollingActive() + allowBarHide && isActive() && accountViewModel.settings.isImmersiveScrollingActive() } val connection = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt index 7e4ded1880..99b5e86a09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt @@ -98,6 +98,7 @@ fun MarmotGroupChatScreen( ) }, accountViewModel = accountViewModel, + allowBarHide = false, ) { Column(Modifier.padding(it).consumeWindowInsets(it).statusBarsPadding()) { MarmotGroupChatView( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt index 4d4a276be6..d95cbef9da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt @@ -51,6 +51,7 @@ fun ChatroomByAuthorScreen( RoomByAuthorTopBar(authorPubKeyHex, accountViewModel, nav) }, accountViewModel = accountViewModel, + allowBarHide = false, ) { Column(Modifier.padding(it)) { ChatroomByAuthor(authorPubKeyHex, draftMessage, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt index 06830903bb..a12f41959c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt @@ -82,6 +82,7 @@ fun ChatroomScreen( ) }, accountViewModel = accountViewModel, + allowBarHide = false, ) { Column(Modifier.padding(it).consumeWindowInsets(it).statusBarsPadding()) { ChatroomView( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index 6fe892d32c..5ee9bb87ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt @@ -55,6 +55,7 @@ fun EphemeralChatScreen( } }, accountViewModel = accountViewModel, + allowBarHide = false, ) { Column(Modifier.padding(it)) { EphemeralChatChannelView(channelId, draft, replyTo, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt index 1132fd56f1..d4286b56f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt @@ -53,6 +53,7 @@ fun PublicChatChannelScreen( } }, accountViewModel = accountViewModel, + allowBarHide = false, ) { Column(Modifier.padding(it)) { PublicChatChannelView(channelId, draft, replyTo, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt index d04ad5cb38..4f4cee53f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt @@ -61,6 +61,7 @@ fun LiveActivityChannelScreen( } }, accountViewModel = accountViewModel, + allowBarHide = false, ) { Column(Modifier.padding(it)) { LiveActivityChannelView(channelId, draft, replyTo, accountViewModel, nav) From 154adcf1118a0ee922af0b8b30e02bccc5b55b62 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 16:32:55 +0000 Subject: [PATCH 8/9] refactor(ui): CompositionLocal for scaffold padding + scaffold hygiene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../amethyst/ui/feeds/FeedContentStateView.kt | 8 +- .../amethyst/ui/feeds/FeedLoaded.kt | 7 +- .../ui/layouts/DisappearingScaffold.kt | 39 ++-- .../amethyst/ui/layouts/PaddingMerge.kt | 23 ++- .../amethyst/ui/screen/FeedView.kt | 8 +- .../amethyst/ui/screen/UserFeedView.kt | 13 +- .../loggedIn/articles/ArticlesFeedLoaded.kt | 7 +- .../loggedIn/articles/ArticlesScreen.kt | 3 +- .../default/BookmarkListScreen.kt | 2 - .../old/OldBookmarkListScreen.kt | 2 - .../chats/rooms/feed/ChatroomListFeedView.kt | 13 +- .../chats/rooms/feed/ChatroomListTabs.kt | 3 - .../rooms/singlepane/MessagesSinglePane.kt | 1 - .../chats/rooms/twopane/ChatroomListPane.kt | 4 - .../chats/rooms/twopane/MessagesTwoPane.kt | 1 - .../loggedIn/communities/CommunityScreen.kt | 2 - .../loggedIn/discover/DiscoverScreen.kt | 15 +- .../screen/loggedIn/drafts/DraftListScreen.kt | 10 +- .../dvms/DvmContentDiscoveryScreen.kt | 14 +- .../followPacks/feed/FollowPackFeedScreen.kt | 3 - .../screen/loggedIn/geohash/GeoHashScreen.kt | 1 - .../screen/loggedIn/hashtag/HashtagScreen.kt | 1 - .../ui/screen/loggedIn/home/HomeScreen.kt | 13 +- .../screen/loggedIn/longs/LongsFeedLoaded.kt | 6 +- .../ui/screen/loggedIn/longs/LongsScreen.kt | 3 +- .../loggedIn/notifications/CardFeedView.kt | 9 +- .../notifications/NotificationScreen.kt | 1 - .../loggedIn/pictures/PictureFeedLoaded.kt | 6 +- .../loggedIn/pictures/PicturesScreen.kt | 3 +- .../loggedIn/pinnednotes/PinnedNotesScreen.kt | 1 - .../ui/screen/loggedIn/polls/PollsScreen.kt | 1 - .../loggedIn/products/ProductsFeedLoaded.kt | 9 +- .../loggedIn/products/ProductsScreen.kt | 3 +- .../screen/loggedIn/relay/RelayFeedScreen.kt | 1 - .../ui/screen/loggedIn/search/SearchScreen.kt | 15 +- .../loggedIn/shorts/ShortsFeedLoaded.kt | 6 +- .../ui/screen/loggedIn/shorts/ShortsScreen.kt | 3 +- .../loggedIn/threadview/ThreadFeedView.kt | 8 +- .../loggedIn/threadview/ThreadScreen.kt | 2 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 9 +- .../webBookmarks/WebBookmarksScreen.kt | 10 +- .../DisappearingBarNestedScrollTest.kt | 170 ++++++++++++++++++ 42 files changed, 277 insertions(+), 182 deletions(-) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScrollTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt index 3c66d9f5d3..fb49009cbc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt @@ -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() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt index cc326c0205..1fe04f30f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt index 9907a85ff0..f6d60dcb44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt @@ -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), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/PaddingMerge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/PaddingMerge.kt index 94383fb9b7..3dad27520f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/PaddingMerge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/PaddingMerge.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 2928e32f2f..ac14f13239 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -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() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index 1fedc9a076..9b7a5895b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -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 -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt index d369773e39..534bc03854 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt index 35a0d0a83c..4a88641cf2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt index 67b921860e..d1a5821a3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt index a04dc7e27e..28788d4462 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index a1c12f74d7..b702de4d4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index f0c52e4a02..45b1e85837 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -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, - 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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt index f2c215e082..b56a5e12c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt @@ -98,7 +98,6 @@ fun MessagesSinglePane( MessagesPager( pagerState, tabs, - it, accountViewModel, nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt index ae1e24fe16..cadfd00a4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 7fd398d2a6..867b4788dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -96,7 +96,6 @@ fun MessagesTwoPane( ChatroomList( knownFeedContentState, newFeedContentState, - padding, accountViewModel, twoPaneNav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt index 80e67f2104..faf7432320 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index c6456fda5b..27e5c01516 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -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 -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index ca9bca18c8..0c9f326d8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -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 -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 0f1488fab5..7ef6ef8906 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index b4157f6f36..f9c7024034 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 80a42b4663..e8d851da69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -103,7 +103,6 @@ fun GeoHashScreen( RefresheableFeedView( feedViewModel, null, - scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index e0cf66286c..d3d640f331 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -108,7 +108,6 @@ fun HashtagScreen( RefresheableFeedView( feedViewModel, null, - scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index c02e2216e7..aec5cde958 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -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) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt index ba9c8ee304..c35455894d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt index c6b079599a..002e483e4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index 8d3dddeb5f..54cb9cfda2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -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) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index a443359121..6ff66075d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -102,7 +102,6 @@ fun NotificationScreen( nav = nav, routeForLastRead = "Notification", scrollToEventId = scrollToEventId, - scaffoldPadding = it, headerContent = { ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt index 5a971b86e9..c9d972238e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt index 866fd310c3..cfb27f98b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pinnednotes/PinnedNotesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pinnednotes/PinnedNotesScreen.kt index 2c613011a9..d8ee106b2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pinnednotes/PinnedNotesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pinnednotes/PinnedNotesScreen.kt @@ -78,7 +78,6 @@ private fun RenderPinnedNotesScreen( RefresheableFeedView( pinnedNotesFeedViewModel, null, - scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt index cb102fa5a9..09815d9a7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollsScreen.kt @@ -172,7 +172,6 @@ private fun PollsPages( listState = listState, nav = nav, routeForLastRead = tabs[page].routeForLastRead, - scaffoldPadding = it, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt index 4469ebf528..4d50129921 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt @@ -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 -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt index 977e6f30a3..5bb9ac321c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt index 05a67ba266..837e0ad444 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt @@ -106,7 +106,6 @@ fun RelayFeedScreen( RefresheableFeedView( feedViewModel, null, - scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 42b8623c86..a4e0bdf8ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt index b930b17e8e..529334e299 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt index 3bcfc9becd..be9cda0592 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index c4d01acb26..ab6b830cdb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt index 2d9f07f7c4..c301d1e88c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt @@ -69,6 +69,6 @@ fun ThreadScreen( }, accountViewModel = accountViewModel, ) { - ThreadFeedView(noteId, feedViewModel, it, accountViewModel, nav) + ThreadFeedView(noteId, feedViewModel, accountViewModel, nav) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 6fc53d2ba5..880e7b2ced 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt index fb0d080f9c..1fee93c83a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt @@ -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 -> diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScrollTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScrollTest.kt new file mode 100644 index 0000000000..791c54b798 --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScrollTest.kt @@ -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) + } +} From c20bb75590b684b221135e4357056af9c145ece2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 17:39:34 +0000 Subject: [PATCH 9/9] fix(ui): wrap DisappearingScaffold in a Surface (restore LocalContentColor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom SubcomposeLayout that replaced Scaffold inherited Material's theme but not its implicit Surface, which is what provides `LocalContentColor = onBackground`. Without that provider, `LocalContentColor` falls back to `Color.Black` — which in the dark theme is invisible against the dark background, making all feed text unreadable. Fixed by wrapping the root modifier chain in a `Surface(color = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground)`, matching what M3 Scaffold does internally. Extracted the SubcomposeLayout into a small private `ScaffoldLayout` composable so the Surface wraps it cleanly. https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa --- .../ui/layouts/DisappearingScaffold.kt | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt index f6d60dcb44..29253e02b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt @@ -25,6 +25,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.imePadding import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -85,6 +87,9 @@ fun DisappearingScaffold( if (allowBarHide) ResetBarsOnResume(state) // When bars are pinned, skip attaching the nested-scroll connection entirely. + // The outer Surface provides the Material container color + onBackground as + // LocalContentColor, matching M3 Scaffold's behaviour (without it, default text + // color falls back to Color.Black and is invisible on the dark theme). val rootModifier = if (allowBarHide) { Modifier.imePadding().nestedScroll(connection) @@ -92,7 +97,30 @@ fun DisappearingScaffold( Modifier.imePadding() } - SubcomposeLayout(modifier = rootModifier) { constraints -> + Surface( + modifier = rootModifier, + color = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + ) { + ScaffoldLayout( + state = state, + topBar = topBar, + bottomBar = bottomBar, + floatingButton = floatingButton, + mainContent = mainContent, + ) + } +} + +@Composable +private fun ScaffoldLayout( + state: DisappearingBarState, + topBar: (@Composable () -> Unit)?, + bottomBar: (@Composable () -> Unit)?, + floatingButton: (@Composable () -> Unit)?, + mainContent: @Composable (padding: PaddingValues) -> Unit, +) { + SubcomposeLayout { constraints -> val layoutWidth = constraints.maxWidth val layoutHeight = constraints.maxHeight val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)