Skip to content

Commit f556b2f

Browse files
authored
Merge pull request #508 from synonymdev/fix/toast-polish
fix: Toast polish
2 parents 4b062fe + 3e9a8d0 commit f556b2f

File tree

8 files changed

+196
-42
lines changed

8 files changed

+196
-42
lines changed

app/src/main/java/to/bitkit/di/ViewModelModule.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import dagger.Module
55
import dagger.Provides
66
import dagger.hilt.InstallIn
77
import dagger.hilt.components.SingletonComponent
8+
import kotlinx.coroutines.CoroutineScope
9+
import to.bitkit.ui.shared.toast.ToastQueueManager
810
import javax.inject.Singleton
911

1012
@Module
@@ -15,4 +17,9 @@ object ViewModelModule {
1517
fun provideFirebaseMessaging(): FirebaseMessaging {
1618
return FirebaseMessaging.getInstance()
1719
}
20+
21+
@Provides
22+
fun provideToastManagerProvider(): (CoroutineScope) -> ToastQueueManager {
23+
return ::ToastQueueManager
24+
}
1825
}

app/src/main/java/to/bitkit/ui/MainActivity.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,13 @@ class MainActivity : FragmentActivity() {
166166
}
167167
}
168168

169+
val currentToast by appViewModel.currentToast.collectAsStateWithLifecycle()
169170
ToastOverlay(
170-
toast = appViewModel.currentToast,
171+
toast = currentToast,
171172
hazeState = hazeState,
172-
onDismiss = {
173-
appViewModel.hideToast()
174-
}
173+
onDismiss = { appViewModel.hideToast() },
174+
onDragStart = { appViewModel.pauseToast() },
175+
onDragEnd = { appViewModel.resumeToast() }
175176
)
176177

177178
val transactionSheetDetails by appViewModel.transactionSheet.collectAsStateWithLifecycle()

app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import to.bitkit.R
3737
import to.bitkit.models.ActivityBannerType
3838
import to.bitkit.ui.shared.util.clickableAlpha
3939
import to.bitkit.ui.shared.util.outerGlow
40+
import to.bitkit.ui.theme.AppThemeSurface
4041
import to.bitkit.ui.theme.Colors
4142

4243
private const val GLOW_ANIMATION_MILLIS = 1200
@@ -174,18 +175,20 @@ fun ActivityBanner(
174175
@Preview(showSystemUi = true)
175176
@Composable
176177
private fun Preview() {
177-
LazyColumn(
178-
verticalArrangement = Arrangement.spacedBy(4.dp),
179-
modifier = Modifier.fillMaxSize(),
180-
) {
181-
items(items = ActivityBannerType.entries) { item ->
182-
ActivityBanner(
183-
gradientColor = item.color,
184-
title = stringResource(R.string.activity_banner__transfer_in_progress),
185-
icon = item.icon,
186-
onClick = {},
187-
modifier = Modifier.fillMaxWidth()
188-
)
178+
AppThemeSurface {
179+
LazyColumn(
180+
verticalArrangement = Arrangement.spacedBy(4.dp),
181+
modifier = Modifier.fillMaxSize(),
182+
) {
183+
items(items = ActivityBannerType.entries) { item ->
184+
ActivityBanner(
185+
gradientColor = item.color,
186+
title = stringResource(R.string.activity_banner__transfer_in_progress),
187+
icon = item.icon,
188+
onClick = {},
189+
modifier = Modifier.fillMaxWidth()
190+
)
191+
}
189192
}
190193
}
191194
}

app/src/main/java/to/bitkit/ui/components/ToastView.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.compose.animation.SizeTransform
55
import androidx.compose.animation.core.Animatable
66
import androidx.compose.animation.core.Spring
77
import androidx.compose.animation.core.spring
8+
import androidx.compose.animation.core.tween
89
import androidx.compose.animation.fadeIn
910
import androidx.compose.animation.fadeOut
1011
import androidx.compose.animation.slideInVertically
@@ -126,7 +127,14 @@ fun ToastView(
126127
val isHorizontalSwipe = horizontalSwipeDistance > verticalSwipeDistance
127128

128129
if (isHorizontalSwipe && horizontalSwipeDistance > dismissThreshold.toPx()) {
129-
// Horizontal swipe dismiss
130+
// Horizontal swipe dismiss - animate off-screen horizontally
131+
val swipeDirection = if (dragOffsetX.value > 0) 1f else -1f
132+
val targetOffsetX = swipeDirection * 1200.dp.toPx()
133+
134+
dragOffsetX.animateTo(
135+
targetValue = targetOffsetX,
136+
animationSpec = tween(durationMillis = 200)
137+
)
130138
onDismiss()
131139
} else if (!isHorizontalSwipe && dragOffsetY.value < -dismissThreshold.toPx()) {
132140
// Vertical swipe up dismiss
@@ -285,9 +293,9 @@ private fun ToastHost(
285293
@Composable
286294
fun ToastOverlay(
287295
toast: Toast?,
296+
onDismiss: () -> Unit,
288297
modifier: Modifier = Modifier,
289298
hazeState: HazeState = rememberHazeState(blurEnabled = true),
290-
onDismiss: () -> Unit,
291299
onDragStart: () -> Unit = {},
292300
onDragEnd: () -> Unit = {},
293301
) {
@@ -312,7 +320,6 @@ private fun ToastViewPreview() {
312320
ScreenColumn(
313321
verticalArrangement = Arrangement.spacedBy(16.dp),
314322
) {
315-
316323
ToastView(
317324
toast = Toast(
318325
type = Toast.ToastType.WARNING,

app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
88
import kotlinx.coroutines.flow.StateFlow
99
import kotlinx.coroutines.flow.asStateFlow
1010
import kotlinx.coroutines.flow.combine
11-
import kotlinx.coroutines.flow.distinctUntilChanged
1211
import kotlinx.coroutines.flow.map
1312
import kotlinx.coroutines.flow.update
1413
import kotlinx.coroutines.launch
@@ -217,14 +216,13 @@ class HomeViewModel @Inject constructor(
217216

218217
private suspend fun createBannersFlow() {
219218
transferRepo.activeTransfers
220-
.distinctUntilChanged()
221219
.collect { transfers ->
222220
val banners = listOfNotNull(
223221
ActivityBannerType.SPENDING.takeIf {
224-
transfers.any { it.type == TransferType.TO_SPENDING || it.type == TransferType.MANUAL_SETUP }
222+
transfers.any { it.type.isToSpending() }
225223
},
226224
ActivityBannerType.SAVINGS.takeIf {
227-
transfers.any { it.type == TransferType.COOP_CLOSE || it.type == TransferType.FORCE_CLOSE }
225+
transfers.any { it.type.isToSavings() }
228226
},
229227
)
230228
_uiState.update { it.copy(banners = banners) }
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package to.bitkit.ui.shared.toast
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Job
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.flow.MutableStateFlow
7+
import kotlinx.coroutines.flow.StateFlow
8+
import kotlinx.coroutines.flow.asStateFlow
9+
import kotlinx.coroutines.flow.update
10+
import kotlinx.coroutines.launch
11+
import to.bitkit.models.Toast
12+
13+
private const val MAX_QUEUE_SIZE = 5
14+
15+
/**
16+
* Manages a queue of toasts to display sequentially.
17+
*
18+
* This ensures that toasts are shown one at a time without premature cancellation.
19+
* When a toast is displayed, it waits for its full visibility duration before
20+
* showing the next toast in the queue.
21+
*
22+
* Features:
23+
* - Thread-safe queue using StateFlow
24+
* - Sequential display (one toast at a time)
25+
* - Pause/resume timer on drag interactions
26+
* - Auto-advance to next toast on completion
27+
* - Max queue size with FIFO overflow handling
28+
*/
29+
class ToastQueueManager(private val scope: CoroutineScope) {
30+
// Public state exposed to UI
31+
private val _currentToast = MutableStateFlow<Toast?>(null)
32+
val currentToast: StateFlow<Toast?> = _currentToast.asStateFlow()
33+
34+
// Internal queue state
35+
private val _queue = MutableStateFlow<List<Toast>>(emptyList())
36+
private var timerJob: Job? = null
37+
private var isPaused = false
38+
39+
/**
40+
* Add toast to queue. If queue is full, drops oldest.
41+
*/
42+
fun enqueue(toast: Toast) {
43+
_queue.update { current ->
44+
val newQueue = if (current.size >= MAX_QUEUE_SIZE) {
45+
// Drop oldest (first item) when queue full
46+
current.drop(1) + toast
47+
} else {
48+
current + toast
49+
}
50+
newQueue
51+
}
52+
// If no toast is currently displayed, show this one immediately
53+
showNextToastIfAvailable()
54+
}
55+
56+
/**
57+
* Dismiss current toast and advance to next in queue.
58+
*/
59+
fun dismissCurrentToast() {
60+
cancelTimer()
61+
_currentToast.value = null
62+
isPaused = false
63+
// Check if there are more toasts waiting and show next one
64+
showNextToastIfAvailable()
65+
}
66+
67+
/**
68+
* Pause current toast timer (called on drag start).
69+
*/
70+
fun pauseCurrentToast() {
71+
if (_currentToast.value?.autoHide == true) {
72+
isPaused = true
73+
cancelTimer()
74+
}
75+
}
76+
77+
/**
78+
* Resume current toast timer with FULL duration (called on drag end).
79+
*/
80+
fun resumeCurrentToast() {
81+
val toast = _currentToast.value
82+
if (isPaused && toast != null) {
83+
isPaused = false
84+
if (toast.autoHide) {
85+
startTimer(toast.visibilityTime)
86+
}
87+
}
88+
}
89+
90+
/**
91+
* Clear all queued toasts and hide current toast.
92+
*/
93+
fun clear() {
94+
cancelTimer()
95+
_queue.value = emptyList()
96+
_currentToast.value = null
97+
isPaused = false
98+
}
99+
100+
private fun showNextToast() {
101+
val nextToast = _queue.value.firstOrNull() ?: return
102+
103+
// Remove from queue
104+
_queue.update { it.drop(1) }
105+
106+
// Display toast
107+
_currentToast.value = nextToast
108+
isPaused = false
109+
110+
// Start auto-hide timer if enabled
111+
if (nextToast.autoHide) {
112+
startTimer(nextToast.visibilityTime)
113+
}
114+
}
115+
116+
private fun startTimer(duration: Long) {
117+
cancelTimer()
118+
timerJob = scope.launch {
119+
delay(duration)
120+
if (!isPaused) {
121+
_currentToast.value = null
122+
// Show next toast if available
123+
showNextToastIfAvailable()
124+
}
125+
}
126+
}
127+
128+
private fun showNextToastIfAvailable() {
129+
if (_currentToast.value == null && _queue.value.isNotEmpty()) {
130+
showNextToast()
131+
}
132+
}
133+
134+
private fun cancelTimer() {
135+
timerJob?.cancel()
136+
timerJob = null
137+
}
138+
}

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import kotlinx.coroutines.delay
3737
import kotlinx.coroutines.flow.MutableSharedFlow
3838
import kotlinx.coroutines.flow.MutableStateFlow
3939
import kotlinx.coroutines.flow.SharingStarted
40+
import kotlinx.coroutines.flow.StateFlow
4041
import kotlinx.coroutines.flow.asSharedFlow
4142
import kotlinx.coroutines.flow.asStateFlow
4243
import kotlinx.coroutines.flow.first
@@ -98,6 +99,7 @@ import to.bitkit.ui.Routes
9899
import to.bitkit.ui.components.Sheet
99100
import to.bitkit.ui.components.TimedSheetType
100101
import to.bitkit.ui.shared.toast.ToastEventBus
102+
import to.bitkit.ui.shared.toast.ToastQueueManager
101103
import to.bitkit.ui.sheets.SendRoute
102104
import to.bitkit.ui.theme.TRANSITION_SCREEN_MS
103105
import to.bitkit.utils.Logger
@@ -124,6 +126,7 @@ class AppViewModel @Inject constructor(
124126
private val appUpdaterService: AppUpdaterService,
125127
private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler,
126128
private val cacheStore: CacheStore,
129+
private val toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager,
127130
) : ViewModel() {
128131
val healthState = healthRepo.healthState
129132

@@ -1520,8 +1523,8 @@ class AppViewModel @Inject constructor(
15201523
// endregion
15211524

15221525
// region Toasts
1523-
var currentToast by mutableStateOf<Toast?>(null)
1524-
private set
1526+
private val toastManager = toastManagerProvider(viewModelScope)
1527+
val currentToast: StateFlow<Toast?> = toastManager.currentToast
15251528

15261529
fun toast(
15271530
type: Toast.ToastType,
@@ -1531,20 +1534,16 @@ class AppViewModel @Inject constructor(
15311534
visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT,
15321535
testTag: String? = null,
15331536
) {
1534-
currentToast = Toast(
1535-
type = type,
1536-
title = title,
1537-
description = description,
1538-
autoHide = autoHide,
1539-
visibilityTime = visibilityTime,
1540-
testTag = testTag,
1537+
toastManager.enqueue(
1538+
Toast(
1539+
type = type,
1540+
title = title,
1541+
description = description,
1542+
autoHide = autoHide,
1543+
visibilityTime = visibilityTime,
1544+
testTag = testTag,
1545+
)
15411546
)
1542-
if (autoHide) {
1543-
viewModelScope.launch {
1544-
delay(visibilityTime)
1545-
currentToast = null
1546-
}
1547-
}
15481547
}
15491548

15501549
fun toast(error: Throwable) {
@@ -1561,9 +1560,11 @@ class AppViewModel @Inject constructor(
15611560
)
15621561
}
15631562

1564-
fun hideToast() {
1565-
currentToast = null
1566-
}
1563+
fun hideToast() = toastManager.dismissCurrentToast()
1564+
1565+
fun pauseToast() = toastManager.pauseCurrentToast()
1566+
1567+
fun resumeToast() = toastManager.resumeCurrentToast()
15671568
// endregion
15681569

15691570
// region security

app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import org.lightningdevkit.ldknode.ChannelDetails
2929
import to.bitkit.R
3030
import to.bitkit.data.CacheStore
3131
import to.bitkit.data.SettingsStore
32-
import to.bitkit.env.Env
3332
import to.bitkit.ext.amountOnClose
3433
import to.bitkit.models.Toast
3534
import to.bitkit.models.TransactionSpeed

0 commit comments

Comments
 (0)