Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-26 14:46:01 +04:00
parent 5dddcb48c3
commit fd9d66462e
5 changed files with 335 additions and 21 deletions

View file

@ -66,9 +66,6 @@ internal fun RootContent(
},
)
EventMessageEffect(
messageHandler = uiDependencies.eventMessageHandler,
snackbarHostState = snackbarHostState,
)
EventMessageEffect()
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.routing.component.impl
import androidx.compose.material3.SnackbarDuration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@ -99,10 +98,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
val message = SnackbarMessage(
message = text,
duration = when (length) {
Snackbar.LENGTH_SHORT -> SnackbarDuration.Short
Snackbar.LENGTH_LONG -> SnackbarDuration.Long
Snackbar.LENGTH_INDEFINITE -> SnackbarDuration.Indefinite
else -> SnackbarDuration.Short
Snackbar.LENGTH_SHORT -> SnackbarMessage.Duration.Short
Snackbar.LENGTH_LONG -> SnackbarMessage.Duration.Long
Snackbar.LENGTH_INDEFINITE -> SnackbarMessage.Duration.Indefinite
else -> SnackbarMessage.Duration.Short
},
actionLabel = buttonTitle,
action = action,

View file

@ -1,11 +1,14 @@
package com.tangem.core.ui.message
import androidx.compose.material3.SnackbarDuration
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.ui.UiMessage
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage.Duration
/**
* Event that is used to show a message in the UI.
@ -19,16 +22,40 @@ sealed interface EventMessage : UiMessage
* Shows a snackbar.
*
* @param message The message to show.
* @param duration The duration of the snackbar.
* @param actionLabel The label of the action button.
* @param action The action to perform when the action button is clicked.
* @param duration The duration of the snackbar. Optional, [Duration.Short] by default.
* @param onDismissRequest The action to perform when the snackbar is dismissed. Optional, empty lambda by default.
* @param actionLabel The label of the action button. Optional, `null` by default.
* @param action The action to perform when the action button is clicked. Optional, `null` by default.
* */
data class SnackbarMessage(
val message: TextReference,
val duration: SnackbarDuration = SnackbarDuration.Short,
val duration: Duration = Duration.Short,
val onDismissRequest: () -> Unit = {},
val actionLabel: TextReference? = null,
val action: (() -> Unit)? = null,
) : EventMessage
) : EventMessage {
/**
* The duration of the snackbar.
* */
enum class Duration {
/**
* Shows the snackbar for a short period of time.
* */
Short,
/**
* Shows the snackbar for a long period of time.
* */
Long,
/**
* Shows the snackbar indefinitely, until dismissed or action performed.
* */
Indefinite,
}
}
/**
* Shows a [content] in the UI.
@ -44,4 +71,170 @@ data class ContentMessage(val content: Content) : EventMessage {
@Composable
operator fun invoke(onDismiss: () -> Unit)
}
}
/**
* Shows an alert dialog.
*
* @param message The message to show.
* @param title The title of the dialog. Optional, `null` by default.
* @param firstAction The first dialog action to perform.
* @param secondAction The second dialog action to perform. Optional, `null` by default.
* Performing the second action always dismisses the dialog.
* @param isDismissable If `false` then dialog can not be dismissed by back button click or by outside click.
* @param dismissOnFirstAction If `true` then dialog is dismissed when first action is performed,
* ignoring lambda passed to [EventMessageAction.onClick].
* `true` by default.
* @param onDismissRequest The action to perform when the dialog is dismissed.
* */
data class DialogMessage(
val message: TextReference,
val title: TextReference? = null,
val firstAction: EventMessageAction,
val secondAction: EventMessageAction? = null,
val isDismissable: Boolean = true,
val dismissOnFirstAction: Boolean = true,
val onDismissRequest: () -> Unit = {},
) : EventMessage {
companion object {
/**
* Builder for [DialogMessage].
*
* @param message The message to show.
* @param title The title of the dialog. Optional, `null` by default.
* @param isDismissable If `false` then dialog can not be dismissed by back button click or by outside click.
* `true` by default.
* @param dismissOnFirstAction If `true` then dialog is dismissed when first action is performed,
* ignoring lambda passed to [EventMessageAction.onClick].
* `true` by default.
* @param onDismissRequest The action to perform when the dialog is dismissed.
* @param firstActionBuilder The builder for the first action.
* By default, it builds an action with title "OK".
* @param secondActionBuilder The builder for the second action. Optional, `null` by default.
* Performing the second action always dismisses the dialog.
*
* @return [DialogMessage] with the specified parameters.
* */
operator fun invoke(
message: TextReference,
title: TextReference? = null,
isDismissable: Boolean = true,
dismissOnFirstAction: Boolean = true,
onDismissRequest: () -> Unit = {},
firstActionBuilder: EventMessageAction.BuilderScope.() -> EventMessageAction = { okAction() },
secondActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null,
): DialogMessage {
val buttonsScope = EventMessageAction.BuilderScope(onDismissRequest)
return DialogMessage(
message = message,
title = title,
isDismissable = isDismissable,
dismissOnFirstAction = dismissOnFirstAction,
onDismissRequest = onDismissRequest,
firstAction = firstActionBuilder(buttonsScope),
secondAction = secondActionBuilder?.invoke(buttonsScope),
)
}
}
}
/**
* Shows a bottom sheet.
*
* @param iconResId The icon to show in the bottom sheet.
* Optional, `null` by default.
* @param title The title of the bottom sheet. Optional, `null` by default.
* @param message The message to show in the bottom sheet.
* @param firstAction The first action to perform. Optional, `null` by default.
* @param secondAction The second action to perform. Optional, `null` by default.
* @param onDismissRequest The action to perform when the bottom sheet is dismissed.
* */
data class BottomSheetMessage(
@DrawableRes val iconResId: Int? = null,
val title: TextReference? = null,
val message: TextReference,
val firstAction: EventMessageAction? = null,
val secondAction: EventMessageAction? = null,
val onDismissRequest: () -> Unit = {},
) : EventMessage {
companion object {
/**
* Builder for [BottomSheetMessage].
*
* @param iconResId The icon to show in the bottom sheet. Optional, `null` by default.
* @param title The title of the bottom sheet. Optional, `null` by default.
* @param message The message to show in the bottom sheet.
* @param onDismissRequest The action to perform when the bottom sheet is dismissed.
* @param firstActionBuilder The builder for the first action. Optional, `null` by default.
* @param secondActionBuilder The builder for the second action. Optional, `null` by default.
* */
operator fun invoke(
@DrawableRes iconResId: Int?,
title: TextReference?,
message: TextReference,
onDismissRequest: () -> Unit = {},
firstActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null,
secondActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null,
): BottomSheetMessage {
val buttonsScope = EventMessageAction.BuilderScope(onDismissRequest)
return BottomSheetMessage(
iconResId = iconResId,
title = title,
message = message,
onDismissRequest = onDismissRequest,
firstAction = firstActionBuilder?.invoke(buttonsScope),
secondAction = secondActionBuilder?.invoke(buttonsScope),
)
}
}
}
/**
* Represents an action button in the dialog.
*
* @param title The title of the action.
* @param warning If `true` then the action is highlighted as a warning.
* @param enabled If `false` then the action is disabled.
* @param onClick The action to perform when the button is clicked.
* */
data class EventMessageAction(
val title: TextReference,
val warning: Boolean = false,
val enabled: Boolean = true,
val onClick: () -> Unit,
) {
/**
* Scope for building buttons for the message.
*
* @param onDismissRequest The action to perform when the dialog is dismissed.
* */
class BuilderScope internal constructor(val onDismissRequest: () -> Unit) {
/**
* Builds an action with title "OK".
*
* @param onClick The action to perform when the button is clicked. By default, it dismisses the message.
* */
fun okAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction(
title = resourceReference(id = R.string.common_ok),
onClick = onClick,
)
/**
* Builds an action with title "Cancel".
*
* @param onClick The action to perform when the button is clicked. By default, it dismisses the message.
* */
fun cancelAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction(
title = resourceReference(id = R.string.common_cancel),
onClick = onClick,
)
}
}

View file

@ -1,26 +1,49 @@
package com.tangem.core.ui.message
import android.content.Context
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalEventMessageHandler
import com.tangem.core.ui.res.LocalSnackbarHostState
@Composable
fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: SnackbarHostState) {
fun EventMessageEffect(
messageHandler: EventMessageHandler = LocalEventMessageHandler.current,
snackbarHostState: SnackbarHostState = LocalSnackbarHostState.current,
onShowSnackbar: suspend (SnackbarMessage, Context) -> Unit = { message, context ->
showSnackbar(snackbarHostState, message, context)
},
) {
val messageEvent by messageHandler.collectAsState()
val context = LocalContext.current
var contentMessage: ContentMessage? by remember { mutableStateOf(value = null) }
var dialogMessage: DialogMessage? by remember { mutableStateOf(value = null) }
var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) }
EventEffect(event = messageEvent) { message ->
when (message) {
is SnackbarMessage -> {
onShowSnackbar(message, context)
}
is ContentMessage -> {
contentMessage = message
}
is SnackbarMessage -> {
showSnackbar(snackbarHostState, message, context)
is DialogMessage -> {
dialogMessage = message
}
is BottomSheetMessage -> {
bottomSheetMessage = message
}
}
}
@ -28,16 +51,110 @@ fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: S
contentMessage?.content?.invoke(
onDismiss = { contentMessage = null },
)
dialogMessage?.let { message ->
MessageDialog(
message = message,
onDismissRequest = {
dialogMessage = null
message.onDismissRequest()
},
)
}
bottomSheetMessage?.let { message ->
MessageBottomSheet(
message = message,
onDismissRequest = {
bottomSheetMessage = null
message.onDismissRequest()
},
)
}
}
@Composable
private fun MessageBottomSheet(message: BottomSheetMessage, onDismissRequest: () -> Unit) {
val config = TangemBottomSheetConfig(
isShow = true,
content = MessageBottomSheetUM(
iconResId = message.iconResId,
title = message.title,
message = message.message,
primaryAction = message.firstAction?.let { action ->
MessageBottomSheetUM.ActionUM(
text = action.title,
enabled = action.enabled,
onClick = {
action.onClick()
onDismissRequest()
},
)
},
secondaryAction = message.secondAction?.let { action ->
MessageBottomSheetUM.ActionUM(
text = action.title,
enabled = action.enabled,
onClick = {
action.onClick()
onDismissRequest()
},
)
},
),
onDismissRequest = onDismissRequest,
)
MessageBottomSheet(config)
}
@Composable
private fun MessageDialog(message: DialogMessage, onDismissRequest: () -> Unit) {
BasicDialog(
message = message.message.resolveReference(),
title = message.title?.resolveReference(),
confirmButton = message.firstAction.let { action ->
DialogButtonUM(
title = action.title.resolveReference(),
warning = action.warning,
enabled = action.enabled,
onClick = {
action.onClick()
if (message.dismissOnFirstAction) {
onDismissRequest()
}
},
)
},
dismissButton = message.secondAction?.let { action ->
DialogButtonUM(
title = action.title.resolveReference(),
warning = action.warning,
enabled = action.enabled,
onClick = {
action.onClick()
onDismissRequest()
},
)
},
onDismissDialog = onDismissRequest,
)
}
private suspend fun showSnackbar(snackbarHostState: SnackbarHostState, message: SnackbarMessage, context: Context) {
val result = snackbarHostState.showSnackbar(
message = message.message.resolveReference(context.resources),
actionLabel = message.actionLabel?.resolveReference(context.resources),
duration = message.duration,
duration = when (message.duration) {
SnackbarMessage.Duration.Short -> SnackbarDuration.Short
SnackbarMessage.Duration.Long -> SnackbarDuration.Long
SnackbarMessage.Duration.Indefinite -> SnackbarDuration.Indefinite
},
)
if (result == SnackbarResult.ActionPerformed) {
message.action?.invoke()
when (result) {
SnackbarResult.Dismissed -> message.onDismissRequest()
SnackbarResult.ActionPerformed -> message.action?.invoke()
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.haptic.DefaultHapticManager
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.windowsize.WindowSize
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.domain.apptheme.model.AppThemeMode
@ -39,6 +40,7 @@ fun TangemTheme(
windowSize = windowSize,
vibratorHapticManager = uiDependencies.vibratorHapticManager,
snackbarHostState = uiDependencies.globalSnackbarHostState,
eventMessageHandler = uiDependencies.eventMessageHandler,
overrideSystemBarColors = overrideSystemBarColors,
typography = typography,
dimens = dimens,
@ -53,6 +55,7 @@ fun TangemTheme(
typography: TangemTypography = TangemTheme.typography,
dimens: TangemDimens = TangemTheme.dimens,
vibratorHapticManager: VibratorHapticManager? = null,
eventMessageHandler: EventMessageHandler = remember { EventMessageHandler() },
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
overrideSystemBarColors: Boolean = true,
content: @Composable () -> Unit,
@ -93,6 +96,7 @@ fun TangemTheme(
LocalIsInDarkTheme provides isDark,
LocalHapticManager provides hapticManager,
LocalSnackbarHostState provides snackbarHostState,
LocalEventMessageHandler provides eventMessageHandler,
LocalWindowSize provides windowSize,
) {
CompositionLocalProvider(
@ -285,6 +289,10 @@ val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
error("No SnackbarHostState provided")
}
val LocalEventMessageHandler = staticCompositionLocalOf<EventMessageHandler> {
error("No EventMessageHandler provided")
}
val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
error("No WindowSize provided")
}