Skip to content
84 changes: 84 additions & 0 deletions app/src/main/java/to/bitkit/ui/components/PinnedTabsScaffold.kt
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there's also a .scaffold package for this kind of things.

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package to.bitkit.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import to.bitkit.ui.theme.Colors

private val PinnedTabsShadowHeight = 32.dp
private val PinnedTabsBlurRadius = 24.dp

@Composable
fun PinnedTabsScaffold(
header: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
content: @Composable (topPadding: Dp) -> Unit,
) {
val hazeState = rememberHazeState()
val density = LocalDensity.current
var headerHeight by remember { mutableStateOf(0.dp) }
val shadowBrush = remember {
Brush.verticalGradient(colors = listOf(Colors.Black, Color.Transparent))
}
val hazeStyle = remember {
HazeStyle(
backgroundColor = Colors.Black,
tint = HazeTint(Colors.Black70),
blurRadius = PinnedTabsBlurRadius,
)
}

Box(modifier = modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.hazeSource(hazeState)
) {
content(headerHeight)
}

Box(
modifier = Modifier
.fillMaxWidth()
.offset(y = headerHeight)
.height(PinnedTabsShadowHeight)
.background(shadowBrush)
.zIndex(1f)
)

Column(
modifier = Modifier
.align(Alignment.TopStart)
.fillMaxWidth()
.zIndex(2f)
.hazeEffect(state = hazeState, style = hazeStyle)
.onSizeChanged { headerHeight = with(density) { it.height.toDp() } }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should use subcompose layout to avoid re-renders during measurements composition pass.

) {
header()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import androidx.compose.foundation.background
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
Expand All @@ -22,6 +25,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -32,11 +36,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
import to.bitkit.R
import to.bitkit.env.Env
import to.bitkit.ext.configureForBasicWebContent
import to.bitkit.models.BitrefillCategory
import to.bitkit.ui.components.BodyM
import to.bitkit.ui.components.PinnedTabsScaffold
import to.bitkit.ui.components.SuggestionCard
import to.bitkit.ui.components.Text13Up
import to.bitkit.ui.components.VerticalSpacer
Expand Down Expand Up @@ -64,26 +70,42 @@ fun ShopDiscoverScreen(
modifier: Modifier = Modifier,
) {
val tabs = remember { ShopDiscoverTab.entries.toImmutableList() }
var selectedTab by remember { mutableStateOf(ShopDiscoverTab.Shop) }
val pagerState = rememberPagerState(pageCount = { tabs.size })
val scope = rememberCoroutineScope()

ScreenColumn(modifier = modifier) {
AppTopBar(
titleText = stringResource(R.string.other__shop__discover__nav_title),
onBackClick = onBack,
actions = { DrawerNavIcon() },
)
PinnedTabsScaffold(
header = {
Column(modifier = modifier.fillMaxWidth()) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid reusing outer modifier in header container

ShopDiscoverScreen now applies the same modifier parameter to both ScreenColumn and the pinned header Column. This duplicates any caller-provided layout/semantics modifiers across two nodes; for example, a size modifier like fillMaxSize can make the header measure as full height and push pager content off-screen, while semantics/test tags become duplicated. The header should use a local Modifier.fillMaxWidth() instead of reusing the screen-level modifier.

Useful? React with 👍 / 👎.

AppTopBar(
titleText = stringResource(R.string.other__shop__discover__nav_title),
onBackClick = onBack,
actions = { DrawerNavIcon() },
)

CustomTabRowWithSpacing(
tabs = tabs,
currentTabIndex = tabs.indexOf(selectedTab),
selectedColor = Colors.White,
onTabChange = { selectedTab = it },
modifier = Modifier.padding(horizontal = 16.dp)
)
CustomTabRowWithSpacing(
tabs = tabs,
currentTabIndex = pagerState.currentPage,
selectedColor = Colors.White,
onTabChange = { scope.launch { pagerState.animateScrollToPage(tabs.indexOf(it)) } },
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
) { topPadding ->
HorizontalPager(
state = pagerState,
userScrollEnabled = tabs[pagerState.settledPage] != ShopDiscoverTab.Map,
) { page ->
when (tabs[page]) {
ShopDiscoverTab.Shop -> ShopTabContent(
navigateWebView = navigateWebView,
contentPadding = PaddingValues(top = topPadding, bottom = 42.dp),
)

when (selectedTab) {
ShopDiscoverTab.Shop -> ShopTabContent(navigateWebView = navigateWebView)
ShopDiscoverTab.Map -> MapTabContent()
ShopDiscoverTab.Map -> MapTabContent(modifier = Modifier.padding(top = topPadding))
}
}
}
}
}
Expand All @@ -92,8 +114,10 @@ fun ShopDiscoverScreen(
private fun ShopTabContent(
navigateWebView: (String, String) -> Unit,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyColumn(
contentPadding = contentPadding,
modifier = modifier.padding(horizontal = 16.dp)
) {
item {
Expand Down Expand Up @@ -229,7 +253,7 @@ private fun MapTabContent(
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
.clip(Shapes.medium)
) {
AndroidView(
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,13 @@ private fun WidgetsPage(
text = stringResource(R.string.widgets__add),
onClick = onClickAddWidget,
enabled = !isCalculatorInputActive,
icon = {
Icon(
painter = painterResource(R.drawable.ic_plus),
contentDescription = null,
modifier = Modifier.size(16.dp)
)
},
modifier = Modifier
.alpha(footerAlpha)
.testTag("WidgetsAdd")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package to.bitkit.ui.screens.wallets.activity

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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
Expand All @@ -17,14 +15,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.synonym.bitkitcore.Activity
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import to.bitkit.R
import to.bitkit.ui.appViewModel
import to.bitkit.ui.components.PinnedTabsScaffold
import to.bitkit.ui.components.Sheet
import to.bitkit.ui.scaffold.AppTopBar
import to.bitkit.ui.scaffold.DrawerNavIcon
Expand Down Expand Up @@ -75,7 +73,6 @@ fun AllActivityScreen(
}

@Composable
@OptIn(ExperimentalHazeMaterialsApi::class)
private fun AllActivityScreenContent(
filteredActivities: ImmutableList<Activity>?,
searchText: String,
Expand All @@ -93,6 +90,12 @@ private fun AllActivityScreenContent(
onActivityItemClick: (String) -> Unit,
onEmptyActivityRowClick: () -> Unit,
) {
val listState = rememberLazyListState()

LaunchedEffect(currentTabIndex) {
listState.scrollToItem(0)
}

Column(
modifier = Modifier.screen()
) {
Expand All @@ -104,33 +107,30 @@ private fun AllActivityScreenContent(
},
)

ActivityListFilter(
searchText = searchText,
onSearchTextChange = onSearchTextChange,
hasTagFilter = hasTagFilter,
hasDateRangeFilter = hasDateRangeFilter,
onTagClick = onTagClick,
selectedTags = selectedTags,
onRemoveTag = onRemoveTag,
onDateRangeClick = onDateRangeClick,
tabs = tabs,
currentTabIndex = currentTabIndex,
onTabChange = { onTabChange(tabs.indexOf(it)) },
modifier = Modifier.padding(horizontal = 16.dp)

)
Spacer(modifier = Modifier.height(16.dp))

// List
Box(
modifier = Modifier
.fillMaxSize()
) {
PinnedTabsScaffold(
header = {
ActivityListFilter(
searchText = searchText,
onSearchTextChange = onSearchTextChange,
hasTagFilter = hasTagFilter,
hasDateRangeFilter = hasDateRangeFilter,
onTagClick = onTagClick,
selectedTags = selectedTags,
onRemoveTag = onRemoveTag,
onDateRangeClick = onDateRangeClick,
tabs = tabs,
currentTabIndex = currentTabIndex,
onTabChange = { onTabChange(tabs.indexOf(it)) },
modifier = Modifier.padding(horizontal = 16.dp)
)
}
) { topPadding ->
ActivityListGrouped(
items = filteredActivities,
onActivityItemClick = onActivityItemClick,
onEmptyActivityRowClick = onEmptyActivityRowClick,
contentPadding = PaddingValues(top = 0.dp),
listState = listState,
contentPadding = PaddingValues(top = topPadding + 16.dp),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Offset empty activity state below pinned header

After moving All Activity into PinnedTabsScaffold, the screen relies on contentPadding to keep content below the pinned filter/tabs, but that padding is only honored by the list branch in ActivityListGrouped. When filters produce no results, the fallback empty text is rendered at the top of the scaffold and is overlapped by the pinned header, so users can’t clearly see the "no activity" state. This is reproducible by opening All Activity and applying filters that return zero rows.

Useful? React with 👍 / 👎.

modifier = Modifier
.swipeToChangeTab(
currentTabIndex = currentTabIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -46,6 +48,7 @@ fun ActivityListGrouped(
onActivityItemClick: (String) -> Unit,
onEmptyActivityRowClick: () -> Unit,
modifier: Modifier = Modifier,
listState: LazyListState = rememberLazyListState(),
showFooter: Boolean = false,
onAllActivityButtonClick: () -> Unit = {},
contentPadding: PaddingValues = PaddingValues(top = 20.dp),
Expand All @@ -65,6 +68,7 @@ fun ActivityListGrouped(
val groupedItems = groupActivityItems(items)

LazyColumn(
state = listState,
horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = contentPadding,
modifier = Modifier.fillMaxWidth()
Expand Down
Loading
Loading