@@ -34,33 +34,40 @@ import androidx.compose.ui.unit.dp
34
34
import io.element.android.libraries.designsystem.components.button.ButtonVisuals
35
35
import io.element.android.libraries.designsystem.theme.components.IconSource
36
36
import io.element.android.libraries.designsystem.theme.components.Snackbar
37
+ import kotlinx.coroutines.CancellationException
38
+ import kotlinx.coroutines.currentCoroutineContext
37
39
import kotlinx.coroutines.flow.Flow
38
- import kotlinx.coroutines.flow.MutableStateFlow
39
- import kotlinx.coroutines.flow.asStateFlow
40
- import kotlinx.coroutines.flow.update
40
+ import kotlinx.coroutines.flow.flow
41
41
import kotlinx.coroutines.isActive
42
- import kotlinx.coroutines.launch
43
42
import kotlinx.coroutines.sync.Mutex
44
- import kotlinx.coroutines.sync.withLock
43
+ import java.util.concurrent.atomic.AtomicBoolean
45
44
46
45
/* *
47
46
* A global dispatcher of [SnackbarMessage] to be displayed in [Snackbar] via a [SnackbarHostState].
48
47
*/
49
48
class SnackbarDispatcher {
50
- private val mutex = Mutex ()
51
-
52
- private val _snackbarMessage = MutableStateFlow <SnackbarMessage ?>(null )
53
- val snackbarMessage: Flow <SnackbarMessage ?> = _snackbarMessage .asStateFlow()
49
+ private val queueMutex = Mutex ()
50
+ private val snackBarMessageQueue = ArrayDeque <SnackbarMessage >()
51
+ val snackbarMessage: Flow <SnackbarMessage ?> = flow {
52
+ while (currentCoroutineContext().isActive) {
53
+ queueMutex.lock()
54
+ emit(snackBarMessageQueue.firstOrNull())
55
+ }
56
+ }
54
57
55
58
suspend fun post (message : SnackbarMessage ) {
56
- mutex.withLock {
57
- _snackbarMessage .update { message }
59
+ if (snackBarMessageQueue.isEmpty()) {
60
+ snackBarMessageQueue.add(message)
61
+ if (queueMutex.isLocked) queueMutex.unlock()
62
+ } else {
63
+ snackBarMessageQueue.add(message)
58
64
}
59
65
}
60
66
61
- suspend fun clear () {
62
- mutex.withLock {
63
- _snackbarMessage .update { null }
67
+ fun clear () {
68
+ if (snackBarMessageQueue.isNotEmpty()) {
69
+ snackBarMessageQueue.removeFirstOrNull()
70
+ if (queueMutex.isLocked) queueMutex.unlock()
64
71
}
65
72
}
66
73
}
@@ -87,31 +94,51 @@ fun SnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) {
87
94
}
88
95
}
89
96
97
+ /* *
98
+ * Helper method to display a [SnackbarMessage] in a [SnackbarHostState] handling cancellations.
99
+ */
90
100
@Composable
91
101
fun rememberSnackbarHostState (snackbarMessage : SnackbarMessage ? ): SnackbarHostState {
92
102
val snackbarHostState = remember { SnackbarHostState () }
93
103
val snackbarMessageText = snackbarMessage?.let {
94
104
stringResource(id = snackbarMessage.messageResId)
95
- }
105
+ } ? : return snackbarHostState
106
+
96
107
val dispatcher = LocalSnackbarDispatcher .current
97
- LaunchedEffect (snackbarMessage) {
98
- if (snackbarMessageText == null ) return @LaunchedEffect
99
- launch {
100
- snackbarHostState.showSnackbar(
101
- message = snackbarMessageText,
102
- duration = snackbarMessage.duration,
103
- )
104
- if (isActive) {
108
+ LaunchedEffect (snackbarMessageText) {
109
+ // If the message wasn't already displayed, do it now, and mark it as displayed
110
+ // This will prevent the message from appearing in any other active SnackbarHosts
111
+ if (snackbarMessage.isDisplayed.getAndSet(true ) == false ) {
112
+ try {
113
+ snackbarHostState.showSnackbar(
114
+ message = snackbarMessageText,
115
+ duration = snackbarMessage.duration,
116
+ )
117
+ // The snackbar item was displayed and dismissed, clear its message
118
+ dispatcher.clear()
119
+ } catch (e: CancellationException ) {
120
+ // The snackbar was being displayed when the coroutine was cancelled,
121
+ // so we need to clear its message
105
122
dispatcher.clear()
123
+ throw e
106
124
}
107
125
}
108
126
}
109
127
return snackbarHostState
110
128
}
111
129
130
+ /* *
131
+ * A message to be displayed in a [Snackbar].
132
+ * @param messageResId The message to be displayed.
133
+ * @param duration The duration of the message. The default value is [SnackbarDuration.Short].
134
+ * @param actionResId The action text to be displayed. The default value is `null`.
135
+ * @param isDisplayed Used to track if the current message is already displayed or not.
136
+ * @param action The action to be performed when the action is clicked.
137
+ */
112
138
data class SnackbarMessage (
113
139
@StringRes val messageResId : Int ,
114
140
val duration : SnackbarDuration = SnackbarDuration .Short ,
115
141
@StringRes val actionResId : Int? = null ,
142
+ val isDisplayed : AtomicBoolean = AtomicBoolean (false),
116
143
val action : () -> Unit = {},
117
144
)
0 commit comments