Skip to content

Commit ebc8b2b

Browse files
committed
Merge branch 'master' into chore/update-deps
# Conflicts: # app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
2 parents b5581f1 + f556b2f commit ebc8b2b

File tree

7 files changed

+196
-40
lines changed

7 files changed

+196
-40
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.modifiers.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 & 2 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
) {

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
@@ -99,6 +100,7 @@ import to.bitkit.ui.Routes
99100
import to.bitkit.ui.components.Sheet
100101
import to.bitkit.ui.components.TimedSheetType
101102
import to.bitkit.ui.shared.toast.ToastEventBus
103+
import to.bitkit.ui.shared.toast.ToastQueueManager
102104
import to.bitkit.ui.sheets.SendRoute
103105
import to.bitkit.ui.theme.TRANSITION_SCREEN_MS
104106
import to.bitkit.utils.Logger
@@ -130,6 +132,7 @@ class AppViewModel @Inject constructor(
130132
private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler,
131133
private val cacheStore: CacheStore,
132134
private val transferRepo: TransferRepo,
135+
private val toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager,
133136
) : ViewModel() {
134137
val healthState = healthRepo.healthState
135138

@@ -1536,8 +1539,8 @@ class AppViewModel @Inject constructor(
15361539
// endregion
15371540

15381541
// region Toasts
1539-
var currentToast by mutableStateOf<Toast?>(null)
1540-
private set
1542+
private val toastManager = toastManagerProvider(viewModelScope)
1543+
val currentToast: StateFlow<Toast?> = toastManager.currentToast
15411544

15421545
fun toast(
15431546
type: Toast.ToastType,
@@ -1547,20 +1550,16 @@ class AppViewModel @Inject constructor(
15471550
visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT,
15481551
testTag: String? = null,
15491552
) {
1550-
currentToast = Toast(
1551-
type = type,
1552-
title = title,
1553-
description = description,
1554-
autoHide = autoHide,
1555-
visibilityTime = visibilityTime,
1556-
testTag = testTag,
1553+
toastManager.enqueue(
1554+
Toast(
1555+
type = type,
1556+
title = title,
1557+
description = description,
1558+
autoHide = autoHide,
1559+
visibilityTime = visibilityTime,
1560+
testTag = testTag,
1561+
)
15571562
)
1558-
if (autoHide) {
1559-
viewModelScope.launch {
1560-
delay(visibilityTime)
1561-
currentToast = null
1562-
}
1563-
}
15641563
}
15651564

15661565
fun toast(error: Throwable) {
@@ -1577,9 +1576,11 @@ class AppViewModel @Inject constructor(
15771576
)
15781577
}
15791578

1580-
fun hideToast() {
1581-
currentToast = null
1582-
}
1579+
fun hideToast() = toastManager.dismissCurrentToast()
1580+
1581+
fun pauseToast() = toastManager.pauseCurrentToast()
1582+
1583+
fun resumeToast() = toastManager.resumeCurrentToast()
15831584
// endregion
15841585

15851586
// region security

0 commit comments

Comments
 (0)