Skip to content

Commit 86f797f

Browse files
committed
fix(token/discovery): fix sheet resignment in m3
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 7b34614 commit 86f797f

7 files changed

Lines changed: 124 additions & 64 deletions

File tree

apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ private fun BalanceScreenContent(
141141
CodeButton(
142142
modifier = Modifier
143143
.fillMaxWidth()
144-
.padding(horizontal = CodeTheme.dimens.inset),
144+
.padding(horizontal = CodeTheme.dimens.inset)
145+
.padding(bottom = CodeTheme.dimens.grid.x3),
145146
text = stringResource(R.string.action_discoverCurrencies),
146147
buttonState = ButtonState.Filled10,
147148
onClick = {

apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/TokenDiscoveryScreen.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import androidx.compose.ui.Alignment
88
import androidx.compose.ui.Modifier
99
import androidx.compose.ui.res.stringResource
1010
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
11+
import com.flipcash.app.core.AppRoute
1112
import com.flipcash.app.discovery.internal.TokenDiscoveryScreen
1213
import com.flipcash.app.discovery.internal.TokenDiscoveryViewModel
1314
import com.flipcash.core.R
1415
import com.getcode.navigation.core.LocalCodeNavigator
1516
import com.getcode.opencode.model.ui.DiscoverCategory
1617
import com.getcode.ui.components.AppBarWithTitle
18+
import kotlinx.coroutines.flow.filterIsInstance
19+
import kotlinx.coroutines.flow.launchIn
20+
import kotlinx.coroutines.flow.map
21+
import kotlinx.coroutines.flow.onEach
1722

1823
@Composable
1924
fun TokenDiscoveryScreen() {
@@ -39,4 +44,12 @@ fun TokenDiscoveryScreen() {
3944
viewModel.dispatchEvent(TokenDiscoveryViewModel.Event.OnCategorySelected(DiscoverCategory.Popular))
4045
}
4146
}
47+
48+
LaunchedEffect(viewModel) {
49+
viewModel.eventFlow
50+
.filterIsInstance<TokenDiscoveryViewModel.Event.OpenTokenInfo>()
51+
.map { it.mint }
52+
.onEach { navigator.navigate(AppRoute.Token.Info(it)) }
53+
.launchIn(this)
54+
}
4255
}

apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
1414
import androidx.compose.foundation.lazy.rememberLazyListState
1515
import androidx.compose.material3.Text
1616
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
17+
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
1718
import androidx.compose.runtime.Composable
1819
import androidx.compose.runtime.LaunchedEffect
1920
import androidx.compose.runtime.getValue
@@ -26,6 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview
2627
import androidx.compose.ui.unit.dp
2728
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2829
import com.flipcash.app.core.data.Loadable
30+
import com.flipcash.app.core.data.isLoaded
2931
import com.flipcash.app.core.data.isLoading
3032
import com.flipcash.app.discovery.internal.components.TokenLeaderboard
3133
import com.flipcash.app.theme.FlipcashPreview
@@ -41,12 +43,14 @@ import com.getcode.opencode.model.ui.WindowedRange
4143
import com.getcode.solana.keys.Mint
4244
import com.getcode.solana.keys.PublicKey
4345
import com.getcode.theme.CodeTheme
46+
import com.getcode.ui.core.addIf
4447
import com.getcode.ui.core.drawWithGradient
4548
import com.getcode.ui.core.measured
4649
import com.getcode.ui.core.verticalScrollStateGradient
4750
import com.getcode.ui.theme.CodeButton
4851
import com.getcode.ui.theme.CodeScaffold
4952
import com.getcode.ui.theme.CodeSegmentedControl
53+
import com.getcode.ui.utils.sheetResignmentBehavior
5054
import com.getcode.util.resources.LocalResources
5155
import kotlinx.coroutines.delay
5256

@@ -123,10 +127,19 @@ private fun TokenDiscoveryScreenContent(
123127
}
124128
},
125129
) { padding ->
130+
val ptrState = rememberPullToRefreshState()
131+
LaunchedEffect(state.tokens) {
132+
when (state.tokens) {
133+
is Loadable.Error -> Unit
134+
is Loadable.Loaded -> listState.scrollToItem(0)
135+
is Loadable.Loading -> ptrState.animateToHidden()
136+
}
137+
}
126138
PullToRefreshBox(
127139
modifier = Modifier
128140
.fillMaxSize(),
129141
isRefreshing = false,
142+
state = ptrState,
130143
onRefresh = {
131144
dispatch(TokenDiscoveryViewModel.Event.Refresh)
132145
},
@@ -136,22 +149,14 @@ private fun TokenDiscoveryScreenContent(
136149
transitionSpec = { fadeIn(tween()) togetherWith fadeOut(tween()) },
137150
contentKey = { it::class }, // only crossfade on type change, not data updates
138151
) { tokens ->
139-
Box(
140-
modifier = Modifier.verticalScrollStateGradient(
141-
listState,
142-
color = CodeTheme.colors.background,
143-
isLongGradient = true,
144-
showAtEnd = !state.createEnabled,
145-
)
146-
) {
147-
TokenLeaderboard(
148-
category = state.category,
149-
state = listState,
150-
tokens = tokens,
151-
padding = padding,
152-
dispatch = dispatch
153-
)
154-
}
152+
TokenLeaderboard(
153+
category = state.category,
154+
state = listState,
155+
tokens = tokens,
156+
padding = padding,
157+
showGradientAtEnd = !state.createEnabled,
158+
dispatch = dispatch
159+
)
155160
}
156161
}
157162
}

apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,49 @@ import androidx.compose.runtime.Composable
1616
import androidx.compose.ui.Alignment
1717
import androidx.compose.ui.Modifier
1818
import androidx.compose.ui.res.stringResource
19+
import androidx.compose.ui.unit.dp
1920
import com.flipcash.app.core.data.Loadable
21+
import com.flipcash.app.core.data.isLoaded
2022
import com.flipcash.app.discovery.internal.TokenDiscoveryViewModel
2123
import com.flipcash.features.discovery.R
2224
import com.getcode.opencode.model.financial.Token
2325
import com.getcode.opencode.model.ui.DiscoverCategory
2426
import com.getcode.solana.keys.base58
2527
import com.getcode.theme.CodeTheme
28+
import com.getcode.ui.core.addIf
29+
import com.getcode.ui.core.verticalScrollStateGradient
2630
import com.getcode.ui.theme.ButtonState
2731
import com.getcode.ui.theme.CodeButton
32+
import com.getcode.ui.utils.sheetResignmentBehavior
2833

2934
@Composable
3035
internal fun TokenLeaderboard(
3136
category: DiscoverCategory?,
3237
tokens: Loadable<List<Token>>,
38+
showGradientAtEnd: Boolean,
3339
padding: PaddingValues,
3440
state: LazyListState,
3541
dispatch: (TokenDiscoveryViewModel.Event) -> Unit
3642
) {
43+
val reduceBottomPadding = if (showGradientAtEnd) 0.dp else CodeTheme.dimens.grid.x4
3744
LazyColumn(
38-
modifier = Modifier.fillMaxSize(),
45+
modifier = Modifier
46+
.fillMaxSize()
47+
.verticalScrollStateGradient(
48+
state,
49+
color = CodeTheme.colors.background,
50+
isLongGradient = true,
51+
showAtEnd = showGradientAtEnd,
52+
)
53+
.addIf(tokens.isLoaded()) {
54+
Modifier.sheetResignmentBehavior(state)
55+
},
3956
state = state,
4057
contentPadding = PaddingValues(
4158
start = CodeTheme.dimens.inset,
4259
end = CodeTheme.dimens.inset,
4360
top = CodeTheme.dimens.grid.x2 + padding.calculateTopPadding(),
44-
bottom = CodeTheme.dimens.grid.x2,
61+
bottom = CodeTheme.dimens.grid.x2 + padding.calculateBottomPadding() - reduceBottomPadding,
4562
)
4663
) {
4764
when (tokens) {

apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenList.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,20 @@ fun TokenList(
9393
Fiat(0.0, cashReserves.rate.currency)
9494
)
9595
) {
96-
item { it(Mint.usdf, cashReserves) }
96+
item {
97+
it(Mint.usdf, cashReserves)
98+
Divider(
99+
modifier = Modifier.padding(bottom = CodeTheme.dimens.inset),
100+
color = CodeTheme.colors.dividerVariant
101+
)
102+
}
97103
}
98104
}
99105

100106
footer?.let {
101-
item { it() }
107+
item {
108+
it()
109+
}
102110
}
103111
}
104112
}

ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.getcode.navigation.scenes
22

3+
import android.os.Build
34
import android.os.Parcelable
45
import androidx.compose.foundation.layout.Box
56
import androidx.compose.foundation.layout.WindowInsets
@@ -9,14 +10,19 @@ import androidx.compose.material3.ExperimentalMaterial3Api
910
import androidx.compose.material3.ModalBottomSheet
1011
import androidx.compose.material3.ModalBottomSheetProperties
1112
import androidx.compose.material3.SheetState
13+
import androidx.compose.material3.SheetValue
1214
import androidx.compose.material3.rememberModalBottomSheetState
1315
import androidx.compose.runtime.Composable
1416
import androidx.compose.runtime.CompositionLocalProvider
1517
import androidx.compose.runtime.LaunchedEffect
1618
import androidx.compose.runtime.ProvidableCompositionLocal
1719
import androidx.compose.runtime.SideEffect
20+
import androidx.compose.runtime.getValue
1821
import androidx.compose.runtime.key
22+
import androidx.compose.runtime.mutableStateOf
1923
import androidx.compose.runtime.rememberCoroutineScope
24+
import androidx.compose.runtime.saveable.rememberSaveable
25+
import androidx.compose.runtime.setValue
2026
import androidx.compose.runtime.staticCompositionLocalOf
2127
import androidx.compose.ui.Modifier
2228
import androidx.compose.ui.platform.LocalView
@@ -34,6 +40,7 @@ import com.getcode.navigation.results.NavResultOrCanceled
3440
import com.getcode.navigation.results.NavResultStore
3541
import com.getcode.navigation.results.NavigationRetVal
3642
import com.getcode.theme.CodeTheme
43+
import com.getcode.ui.utils.LocalSheetGesturesState
3744
import kotlinx.coroutines.launch
3845

3946
// Adapted from code courtesy of https://github.com/android/nav3-recipes/pull/67
@@ -66,7 +73,7 @@ internal class ModalBottomSheetScene<T : Any> @OptIn(ExperimentalMaterial3Api::c
6673
key(key, navigator.sheetGeneration) {
6774
val isNonDismissable =
6875
(metadata[NavMetadataKeys.IsNonDismissable.key] as? Boolean ?: false)
69-
|| navigator.sheetDismissDisabled
76+
|| navigator.sheetDismissDisabled
7077

7178
val handleBackResult = {
7279
val navResultKey =
@@ -127,42 +134,54 @@ internal class ModalBottomSheetScene<T : Any> @OptIn(ExperimentalMaterial3Api::c
127134
}
128135
}
129136

130-
// Remove inset padding. Default adds nav bar padding.
131-
// Remove grab bar for bleed to top edge of sheet
132-
ModalBottomSheet(
133-
sheetState = sheetState,
134-
onDismissRequest = { if (!isNonDismissable) dismiss(false) },
135-
sheetGesturesEnabled = !navigator.sheetDragDisabled,
136-
scrimColor = CodeTheme.colors.scrim,
137-
properties = if (isNonDismissable) {
138-
ModalBottomSheetProperties(
139-
shouldDismissOnBackPress = false,
140-
shouldDismissOnClickOutside = false,
141-
)
142-
} else modalBottomSheetProperties,
143-
dragHandle = null,
144-
contentWindowInsets = { WindowInsets() },
145-
containerColor = CodeTheme.colors.surface,
137+
var gesturesEnabled by rememberSaveable(!navigator.sheetDragDisabled) {
138+
mutableStateOf(!navigator.sheetDragDisabled)
139+
}
140+
141+
CompositionLocalProvider(
142+
LocalSheetGesturesState provides { enabled ->
143+
gesturesEnabled = enabled && !navigator.sheetDragDisabled
144+
},
146145
) {
147-
// The sheet's popup window defaults to dark (black) status bar icons.
148-
// Force light icons so they're visible against the dark scrim.
149-
val view = LocalView.current
150-
SideEffect {
151-
view.rootView.windowInsetsController?.setSystemBarsAppearance(
152-
0, // clear light status bars flag → light (white) icons
153-
android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
154-
)
155-
}
156-
Box(
157-
modifier = Modifier
158-
.fillMaxWidth()
159-
.fillMaxHeight(CodeTheme.dimens.modalHeightRatio)
146+
// Remove inset padding. Default adds nav bar padding.
147+
// Remove grab bar for bleed to top edge of sheet
148+
ModalBottomSheet(
149+
sheetState = sheetState,
150+
onDismissRequest = { if (!isNonDismissable) dismiss(false) },
151+
sheetGesturesEnabled = gesturesEnabled,
152+
scrimColor = CodeTheme.colors.scrim,
153+
properties = if (isNonDismissable) {
154+
ModalBottomSheetProperties(
155+
shouldDismissOnBackPress = false,
156+
shouldDismissOnClickOutside = false,
157+
)
158+
} else modalBottomSheetProperties,
159+
dragHandle = null,
160+
contentWindowInsets = { WindowInsets() },
161+
containerColor = CodeTheme.colors.surface,
160162
) {
161-
CompositionLocalProvider(
162-
LocalBottomSheetDismissDispatcher provides { dismiss(true) },
163-
LocalSheetNavigator provides navigator,
163+
// The sheet's popup window defaults to dark (black) status bar icons.
164+
// Force light icons so they're visible against the dark scrim.
165+
val view = LocalView.current
166+
SideEffect {
167+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
168+
view.rootView.windowInsetsController?.setSystemBarsAppearance(
169+
0, // clear light status bars flag → light (white) icons
170+
android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
171+
)
172+
}
173+
}
174+
Box(
175+
modifier = Modifier
176+
.fillMaxWidth()
177+
.fillMaxHeight(CodeTheme.dimens.modalHeightRatio)
164178
) {
165-
entry.Content()
179+
CompositionLocalProvider(
180+
LocalBottomSheetDismissDispatcher provides { dismiss(true) },
181+
LocalSheetNavigator provides navigator,
182+
) {
183+
entry.Content()
184+
}
166185
}
167186
}
168187
}

ui/navigation/src/main/kotlin/com/getcode/ui/utils/SheetResignmentModifierNode.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,29 +46,26 @@ private class SheetResignmentModifierNode(
4646
override fun onAttach() {
4747
super.onAttach()
4848
observeJob = coroutineScope.launch {
49-
snapshotFlow { isAtTop }
50-
.collect { atTop -> updateGestures(atTop) }
49+
snapshotFlow { isAtTop to listState.isScrollInProgress }
50+
.collect { (atTop, scrolling) -> updateGestures(atTop, scrolling) }
5151
}
5252
}
5353

54-
private fun updateGestures(atTop: Boolean) {
54+
private fun updateGestures(atTop: Boolean, scrolling: Boolean) {
5555
resetJob?.cancel()
5656

57-
if (atTop) {
58-
// Just reached top → arm the "reject first downward drag"
57+
if (atTop && !scrolling) {
5958
waitingForSecondDrag = true
60-
setGesturesEnabled(false) // sheet drag disabled initially
59+
setGesturesEnabled(false)
6160

62-
// Optional: auto-allow after delay (so pause → second drag works)
6361
if (autoResetDelayMs > 0) {
6462
resetJob = coroutineScope.launch {
6563
delay(autoResetDelayMs)
6664
waitingForSecondDrag = false
67-
setGesturesEnabled(true) // now allow dismiss
65+
setGesturesEnabled(true)
6866
}
6967
}
7068
} else {
71-
// Scrolled away from top → normal scrolling mode, sheet drag disabled
7269
waitingForSecondDrag = false
7370
setGesturesEnabled(false)
7471
}

0 commit comments

Comments
 (0)