Fix snackbar durations.

This commit is contained in:
Alex Hart
2026-01-05 12:38:44 -04:00
committed by jeffrey-signal
parent bb21363ca8
commit 305c32cfc5
13 changed files with 144 additions and 29 deletions

View File

@@ -43,7 +43,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
@@ -83,6 +82,7 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.signal.core.ui.compose.Snackbars
import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getSerializableCompat import org.signal.core.util.getSerializableCompat
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
@@ -325,7 +325,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
mainNavigationViewModel.snackbarRegistry.emit( mainNavigationViewModel.snackbarRegistry.emit(
SnackbarState( SnackbarState(
message = getString(R.string.CallQualitySheet__thanks_for_your_feedback), message = getString(R.string.CallQualitySheet__thanks_for_your_feedback),
duration = SnackbarDuration.Short, duration = Snackbars.Duration.SHORT,
hostKey = MainSnackbarHostKey.Chat, hostKey = MainSnackbarHostKey.Chat,
fallbackKey = MainSnackbarHostKey.MainChrome fallbackKey = MainSnackbarHostKey.MainChrome
) )
@@ -903,7 +903,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
mainNavigationViewModel.snackbarRegistry.emit( mainNavigationViewModel.snackbarRegistry.emit(
SnackbarState( SnackbarState(
message = getString(R.string.VerifyBackupKey__backup_key_correct), message = getString(R.string.VerifyBackupKey__backup_key_correct),
duration = SnackbarDuration.Short, duration = Snackbars.Duration.SHORT,
hostKey = MainSnackbarHostKey.MainChrome hostKey = MainSnackbarHostKey.MainChrome
) )
) )

View File

@@ -7,7 +7,6 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.compose.material3.SnackbarDuration
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@@ -22,6 +21,7 @@ import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.kotlin.Flowables import io.reactivex.rxjava3.kotlin.Flowables
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.signal.core.ui.compose.Snackbars
import org.signal.core.util.DimensionUnit import org.signal.core.util.DimensionUnit
import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo import org.signal.core.util.concurrent.addTo
@@ -458,7 +458,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
mainNavigationViewModel.snackbarRegistry.emit( mainNavigationViewModel.snackbarRegistry.emit(
SnackbarState( SnackbarState(
message = snackbarMessage, message = snackbarMessage,
duration = SnackbarDuration.Short, duration = Snackbars.Duration.SHORT,
hostKey = MainSnackbarHostKey.MainChrome hostKey = MainSnackbarHostKey.MainChrome
) )
) )

View File

@@ -13,7 +13,6 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.signal.core.util.AppUtil import org.signal.core.util.AppUtil
import org.signal.core.util.ThreadUtil import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.concurrent.SignalExecutors
@@ -33,6 +32,8 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsRepository import org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.snackbars.SnackbarState
import org.thoughtcrime.securesms.components.snackbars.makeSnackbar
import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.JobDatabase import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.database.LocalMetricsDatabase import org.thoughtcrime.securesms.database.LocalMetricsDatabase
@@ -100,7 +101,11 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
setFragmentResultListener(CallQualityBottomSheetFragment.REQUEST_KEY) { _, bundle -> setFragmentResultListener(CallQualityBottomSheetFragment.REQUEST_KEY) { _, bundle ->
if (bundle.getBoolean(CallQualityBottomSheetFragment.REQUEST_KEY, false)) { if (bundle.getBoolean(CallQualityBottomSheetFragment.REQUEST_KEY, false)) {
Snackbar.make(requireView(), R.string.CallQualitySheet__thanks_for_your_feedback, Snackbar.LENGTH_SHORT).show() makeSnackbar(
SnackbarState(
message = getString(R.string.CallQualitySheet__thanks_for_your_feedback)
)
)
} }
} }
} }

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -31,7 +30,9 @@ import org.signal.core.ui.compose.Dividers
import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.Texts import org.signal.core.ui.compose.Texts
import org.signal.core.ui.compose.showSnackbar
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
@@ -73,7 +74,7 @@ class PhoneNumberPrivacySettingsFragment : ComposeFragment() {
lifecycleScope.launch { lifecycleScope.launch {
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
message = snackbarMessage, message = snackbarMessage,
duration = SnackbarDuration.Short duration = Snackbars.Duration.SHORT
) )
} }
} }

View File

@@ -5,9 +5,12 @@
package org.thoughtcrime.securesms.components.snackbars package org.thoughtcrime.securesms.components.snackbars
import androidx.compose.material3.SnackbarDuration
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.Snackbars
fun Fragment.makeSnackbar(state: SnackbarState) { fun Fragment.makeSnackbar(state: SnackbarState) {
if (view == null) { if (view == null) {
@@ -18,9 +21,10 @@ fun Fragment.makeSnackbar(state: SnackbarState) {
requireView(), requireView(),
state.message, state.message,
when (state.duration) { when (state.duration) {
SnackbarDuration.Short -> Snackbar.LENGTH_SHORT Snackbars.Duration.SHORT -> Snackbar.LENGTH_SHORT
SnackbarDuration.Long -> Snackbar.LENGTH_LONG Snackbars.Duration.LONG -> Snackbar.LENGTH_LONG
SnackbarDuration.Indefinite -> Snackbar.LENGTH_INDEFINITE Snackbars.Duration.INDEFINITE -> Snackbar.LENGTH_INDEFINITE
else -> Snackbar.LENGTH_INDEFINITE
} }
) )
@@ -30,4 +34,11 @@ fun Fragment.makeSnackbar(state: SnackbarState) {
} }
snackbar.show() snackbar.show()
if (state.duration is Snackbars.Duration.Custom) {
viewLifecycleOwner.lifecycleScope.launch {
delay(state.duration.duration)
snackbar.dismiss()
}
}
} }

View File

@@ -6,7 +6,7 @@
package org.thoughtcrime.securesms.components.snackbars package org.thoughtcrime.securesms.components.snackbars
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.compose.material3.SnackbarDuration import org.signal.core.ui.compose.Snackbars
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
/** /**
@@ -16,15 +16,15 @@ import org.thoughtcrime.securesms.R
* @property actionState Optional action button configuration. * @property actionState Optional action button configuration.
* @property showProgress Whether to show a progress indicator in the snackbar. * @property showProgress Whether to show a progress indicator in the snackbar.
* @property duration How long the snackbar should be displayed. * @property duration How long the snackbar should be displayed.
* @property hostKey The target host where this snackbar should be displayed. * @property hostKey The target host where this snackbar should be displayed. Defaults to [SnackbarHostKey.Global]
* @property fallbackKey Optional host to fallback upon if the host key is not registered. Defaults to the Global key. * @property fallbackKey Optional host to fallback upon if the host key is not registered. Defaults to the Global key.
*/ */
data class SnackbarState( data class SnackbarState(
val message: String, val message: String,
val actionState: ActionState? = null, val actionState: ActionState? = null,
val showProgress: Boolean = false, val showProgress: Boolean = false,
val duration: SnackbarDuration = SnackbarDuration.Long, val duration: Snackbars.Duration = Snackbars.Duration.SHORT,
val hostKey: SnackbarHostKey, val hostKey: SnackbarHostKey = SnackbarHostKey.Global,
val fallbackKey: SnackbarHostKey? = SnackbarHostKey.Global val fallbackKey: SnackbarHostKey? = SnackbarHostKey.Global
) { ) {
/** /**

View File

@@ -26,7 +26,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.compose.material3.SnackbarDuration; import org.signal.core.ui.compose.Snackbars;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.signal.core.util.concurrent.LifecycleDisposable; import org.signal.core.util.concurrent.LifecycleDisposable;
@@ -152,7 +152,7 @@ public class ConversationListArchiveFragment extends ConversationListFragment
} }
), ),
false, false,
SnackbarDuration.Long, Snackbars.Duration.LONG,
MainSnackbarHostKey.MainChrome.INSTANCE, MainSnackbarHostKey.MainChrome.INSTANCE,
null null
)); ));

View File

@@ -42,7 +42,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.PluralsRes; import androidx.annotation.PluralsRes;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.compose.material3.SnackbarDuration; import org.signal.core.ui.compose.Snackbars;
import androidx.compose.ui.platform.ComposeView; import androidx.compose.ui.platform.ComposeView;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@@ -991,7 +991,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
} }
), ),
showProgress, showProgress,
SnackbarDuration.Long, Snackbars.Duration.LONG,
MainSnackbarHostKey.MainChrome.INSTANCE, MainSnackbarHostKey.MainChrome.INSTANCE,
null null
)); ));
@@ -1075,7 +1075,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS), getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
null, null,
false, false,
SnackbarDuration.Long, Snackbars.Duration.LONG,
MainSnackbarHostKey.MainChrome.INSTANCE, MainSnackbarHostKey.MainChrome.INSTANCE,
null null
)); ));
@@ -1441,7 +1441,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
} }
), ),
false, false,
SnackbarDuration.Long, Snackbars.Duration.LONG,
MainSnackbarHostKey.MainChrome.INSTANCE, MainSnackbarHostKey.MainChrome.INSTANCE,
null null
)); ));

View File

@@ -24,6 +24,7 @@ import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Snackbars import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.showSnackbar
import org.thoughtcrime.securesms.components.snackbars.SnackbarHostKey import org.thoughtcrime.securesms.components.snackbars.SnackbarHostKey
import org.thoughtcrime.securesms.components.snackbars.SnackbarState import org.thoughtcrime.securesms.components.snackbars.SnackbarState
import org.thoughtcrime.securesms.components.snackbars.rememberSnackbarState import org.thoughtcrime.securesms.components.snackbars.rememberSnackbarState

View File

@@ -9,7 +9,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -34,6 +33,7 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.Snackbars import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.showSnackbar
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment
import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.ComposeFragment
@@ -146,7 +146,7 @@ class AdvancedPinSettingsFragment : ComposeFragment() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.snackbarHostState.showSnackbar( viewModel.snackbarHostState.showSnackbar(
message = getString(R.string.ApplicationPreferencesActivity_pin_disabled), message = getString(R.string.ApplicationPreferencesActivity_pin_disabled),
duration = SnackbarDuration.Long duration = Snackbars.Duration.LONG
) )
} }
} }

View File

@@ -31,7 +31,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
@@ -78,6 +77,7 @@ import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent
import org.signal.core.ui.compose.copied.androidx.compose.DraggableItem import org.signal.core.ui.compose.copied.androidx.compose.DraggableItem
import org.signal.core.ui.compose.copied.androidx.compose.dragContainer import org.signal.core.ui.compose.copied.androidx.compose.dragContainer
import org.signal.core.ui.compose.copied.androidx.compose.rememberDragDropState import org.signal.core.ui.compose.copied.androidx.compose.rememberDragDropState
import org.signal.core.ui.compose.showSnackbar
import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.menu.ActionItem import org.thoughtcrime.securesms.components.menu.ActionItem
@@ -625,7 +625,7 @@ private fun SnackbarHost(
if (snackbarMessage != null) { if (snackbarMessage != null) {
val result = hostState.showSnackbar( val result = hostState.showSnackbar(
message = snackbarMessage, message = snackbarMessage,
duration = SnackbarDuration.Short, duration = Snackbars.Duration.SHORT,
withDismissAction = false withDismissAction = false
) )

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.snackbars package org.thoughtcrime.securesms.components.snackbars
import androidx.compose.material3.SnackbarDuration
import androidx.core.util.Consumer import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -10,6 +9,7 @@ import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.signal.core.ui.compose.Snackbars
class SnackbarStateConsumerRegistryTest { class SnackbarStateConsumerRegistryTest {
@@ -256,7 +256,7 @@ class SnackbarStateConsumerRegistryTest {
message = "Test message", message = "Test message",
hostKey = hostKey, hostKey = hostKey,
fallbackKey = fallbackKey, fallbackKey = fallbackKey,
duration = SnackbarDuration.Short duration = Snackbars.Duration.SHORT
) )
} }

View File

@@ -10,10 +10,16 @@ import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.SnackbarVisuals import androidx.compose.material3.SnackbarVisuals
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.theme.LocalSnackbarColors import org.signal.core.ui.compose.theme.LocalSnackbarColors
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/** /**
* Properly themed Snackbars. Since these use internal color state, we need to * Properly themed Snackbars. Since these use internal color state, we need to
@@ -21,6 +27,48 @@ import org.signal.core.ui.compose.theme.LocalSnackbarColors
* allow for quick and easy access to the proper theming for snackbars. * allow for quick and easy access to the proper theming for snackbars.
*/ */
object Snackbars { object Snackbars {
/**
* Snackbar duration that allows specifying exact durations or using predefined
* durations that match view-based snackbar timings.
*
* Compose's default [SnackbarDuration] values are significantly longer than view-based snackbars:
* - Compose Short = 4000ms vs View-based LENGTH_SHORT = 1500ms
* - Compose Long = 10000ms vs View-based LENGTH_LONG = 2750ms
*
* This sealed class provides durations that match view-based behavior for consistency.
*/
sealed class Duration {
abstract val duration: kotlin.time.Duration?
companion object {
/** 1500ms - matches view-based Snackbar.LENGTH_SHORT */
@JvmField
val SHORT: Duration = Short
/** 2750ms - matches view-based Snackbar.LENGTH_LONG */
@JvmField
val LONG: Duration = Long
@JvmField
val INDEFINITE: Duration = Indefinite
}
private data object Short : Duration() {
override val duration: kotlin.time.Duration = 1500.milliseconds
}
private data object Long : Duration() {
override val duration: kotlin.time.Duration = 2750.milliseconds
}
private data object Indefinite : Duration() {
override val duration: kotlin.time.Duration? = null
}
data class Custom(override val duration: kotlin.time.Duration) : Duration()
}
@Composable @Composable
fun Host(snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) { fun Host(snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) {
SnackbarHost(hostState = snackbarHostState, modifier = modifier) { SnackbarHost(hostState = snackbarHostState, modifier = modifier) {
@@ -42,6 +90,55 @@ object Snackbars {
} }
} }
/**
* Shows a snackbar with custom duration support.
*
* Unlike the standard [SnackbarHostState.showSnackbar] which only supports Compose's
* [SnackbarDuration] (Short=4s, Long=10s, Indefinite), this function allows specifying
* exact durations via [Snackbars.Duration], including durations that match view-based snackbars.
*
* @param message The message to display in the snackbar.
* @param actionLabel Optional action label to show. If null, no action button is shown.
* @param withDismissAction Whether to show a dismiss action (X button).
* @param duration The duration to show the snackbar. Defaults to [Snackbars.Duration.SHORT] (1500ms).
* @return [SnackbarResult] indicating whether the snackbar was dismissed or the action was performed.
*/
suspend fun SnackbarHostState.showSnackbar(
message: String,
actionLabel: String? = null,
withDismissAction: Boolean = false,
duration: Snackbars.Duration = Snackbars.Duration.SHORT
): SnackbarResult {
val durationValue = duration.duration
return if (durationValue == null) {
showSnackbar(
message = message,
actionLabel = actionLabel,
withDismissAction = withDismissAction,
duration = SnackbarDuration.Indefinite
)
} else {
coroutineScope {
val dismissJob = launch {
delay(durationValue)
currentSnackbarData?.dismiss()
}
val result = showSnackbar(
message = message,
actionLabel = actionLabel,
withDismissAction = withDismissAction,
duration = SnackbarDuration.Indefinite
)
dismissJob.cancel()
result
}
}
}
@DayNightPreviews @DayNightPreviews
@Composable @Composable
private fun SnackbarPreview() { private fun SnackbarPreview() {