From c2a51d1661b2853cf9e60617f61c030cd4d6ccc0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 19:48:18 +0100 Subject: [PATCH 01/21] refactor: replace ToastEventBus with injectable Toaster - Add ToastText sealed interface for type-safe messages - Create Toaster singleton with convenience APIs - Move ToastType to top-level with @Stable annotation - Migrate all ViewModels and Repos to use Toaster - Delete ToastEventBus in favor of DI pattern Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 8 +- .../main/java/to/bitkit/models/ToastText.kt | 21 +++ .../java/to/bitkit/repositories/BackupRepo.kt | 7 +- .../to/bitkit/repositories/CurrencyRepo.kt | 7 +- app/src/main/java/to/bitkit/ui/ContentView.kt | 4 +- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 4 +- .../bitkit/ui/components/IsOnlineTracker.kt | 6 +- .../java/to/bitkit/ui/components/ToastView.kt | 21 +-- .../recovery/RecoveryMnemonicViewModel.kt | 12 +- .../ui/screens/recovery/RecoveryViewModel.kt | 24 +-- .../ui/screens/scanner/QrScanningScreen.kt | 6 +- .../ui/screens/settings/DevSettingsScreen.kt | 24 +-- .../screens/transfer/SavingsProgressScreen.kt | 6 +- .../transfer/SpendingAdvancedScreen.kt | 4 +- .../external/ExternalFeeCustomScreen.kt | 16 +- .../external/ExternalNodeViewModel.kt | 30 ++-- .../external/LnurlChannelViewModel.kt | 14 +- .../wallets/activity/ActivityDetailScreen.kt | 12 +- .../wallets/activity/ActivityExploreScreen.kt | 4 +- .../screens/wallets/send/SendAmountScreen.kt | 4 +- .../send/SendCoinSelectionViewModel.kt | 5 +- .../screens/wallets/send/SendFeeViewModel.kt | 16 +- .../wallets/send/SendRecipientScreen.kt | 6 +- .../ui/settings/BlocktankRegtestScreen.kt | 16 +- .../to/bitkit/ui/settings/SettingsScreen.kt | 4 +- .../settings/advanced/AddressViewerScreen.kt | 4 +- .../settings/advanced/ElectrumConfigScreen.kt | 6 +- .../advanced/ElectrumConfigViewModel.kt | 10 +- .../ui/settings/advanced/RgsServerScreen.kt | 6 +- .../backups/BackupNavSheetViewModel.kt | 16 +- .../ui/settings/backups/ShowMnemonicScreen.kt | 12 +- .../settings/lightning/ChannelDetailScreen.kt | 4 +- .../LightningConnectionsViewModel.kt | 27 ++-- .../bitkit/ui/shared/toast/ToastEventBus.kt | 34 ----- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 144 ++++++++++++++++++ .../java/to/bitkit/ui/sheets/BackupSheet.kt | 1 + .../java/to/bitkit/viewmodels/AppViewModel.kt | 50 +++--- .../bitkit/viewmodels/DevSettingsViewModel.kt | 29 ++-- .../to/bitkit/viewmodels/LdkDebugViewModel.kt | 105 +++---------- .../to/bitkit/viewmodels/TransferViewModel.kt | 30 ++-- .../to/bitkit/viewmodels/WalletViewModel.kt | 35 ++--- .../bitkit/repositories/CurrencyRepoTest.kt | 3 + .../java/to/bitkit/ui/WalletViewModelTest.kt | 5 + .../wallets/send/SendFeeViewModelTest.kt | 4 +- .../viewmodels/AmountInputViewModelTest.kt | 4 + 45 files changed, 417 insertions(+), 393 deletions(-) create mode 100644 app/src/main/java/to/bitkit/models/ToastText.kt delete mode 100644 app/src/main/java/to/bitkit/ui/shared/toast/ToastEventBus.kt create mode 100644 app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index a4dc00d99..b9688108e 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -1,5 +1,11 @@ package to.bitkit.models +import androidx.compose.runtime.Stable + +@Stable +enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } + +@Stable data class Toast( val type: ToastType, val title: String, @@ -8,8 +14,6 @@ data class Toast( val visibilityTime: Long = VISIBILITY_TIME_DEFAULT, val testTag: String? = null, ) { - enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } - companion object { const val VISIBILITY_TIME_DEFAULT = 3000L } diff --git a/app/src/main/java/to/bitkit/models/ToastText.kt b/app/src/main/java/to/bitkit/models/ToastText.kt new file mode 100644 index 000000000..bf73e651e --- /dev/null +++ b/app/src/main/java/to/bitkit/models/ToastText.kt @@ -0,0 +1,21 @@ +package to.bitkit.models + +import android.content.Context +import androidx.annotation.StringRes +import androidx.compose.runtime.Stable + +@Stable +sealed interface ToastText { + @JvmInline + @Stable + value class Resource(@StringRes val resId: Int) : ToastText + + @JvmInline + @Stable + value class Literal(val value: String) : ToastText +} + +fun ToastText.asString(context: Context): String = when (this) { + is ToastText.Resource -> context.getString(resId) + is ToastText.Literal -> value +} diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index 9088a0e88..e53ddfcde 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -42,11 +42,10 @@ import to.bitkit.models.BackupItemStatus import to.bitkit.models.BlocktankBackupV1 import to.bitkit.models.MetadataBackupV1 import to.bitkit.models.SettingsBackupV1 -import to.bitkit.models.Toast import to.bitkit.models.WalletBackupV1 import to.bitkit.models.WidgetsBackupV1 import to.bitkit.services.LightningService -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import to.bitkit.utils.jsonLogOf import java.util.concurrent.ConcurrentHashMap @@ -86,6 +85,7 @@ class BackupRepo @Inject constructor( private val lightningService: LightningService, private val clock: Clock, private val db: AppDb, + private val toaster: Toaster, ) { private val scope = CoroutineScope(ioDispatcher + SupervisorJob()) @@ -373,8 +373,7 @@ class BackupRepo @Inject constructor( lastNotificationTime = currentTime scope.launch { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.settings__backup__failed_title), description = context.getString(R.string.settings__backup__failed_message).formatPlural( mapOf("interval" to (BACKUP_CHECK_INTERVAL / MINUTE_IN_MS)) diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index 57d347abc..1456fcd11 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -29,11 +29,10 @@ import to.bitkit.models.FxRate import to.bitkit.models.PrimaryDisplay import to.bitkit.models.SATS_IN_BTC import to.bitkit.models.STUB_RATE -import to.bitkit.models.Toast import to.bitkit.models.asBtc import to.bitkit.models.formatCurrency import to.bitkit.services.CurrencyService -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import java.math.BigDecimal import java.math.RoundingMode @@ -53,6 +52,7 @@ class CurrencyRepo @Inject constructor( private val cacheStore: CacheStore, private val clock: Clock, @Named("enablePolling") private val enablePolling: Boolean, + private val toaster: Toaster, ) : AmountInputHandler { private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob()) private val _currencyState = MutableStateFlow(CurrencyState()) @@ -92,8 +92,7 @@ class CurrencyRepo @Inject constructor( .distinctUntilChanged() .collect { isStale -> if (isStale) { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = "Rates currently unavailable", description = "An error has occurred. Please try again later." ) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 96188dbf9..762bb8ee8 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -46,7 +46,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import to.bitkit.env.Env import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.WidgetType import to.bitkit.ui.Routes.ExternalConnection import to.bitkit.ui.components.AuthCheckScreen @@ -628,7 +628,7 @@ private fun RootNavHost( toastException = { appViewModel.toast(it) }, toast = { title, description -> appViewModel.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = title, description = description ) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 5e81df27d..55096ff34 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -46,7 +46,7 @@ import to.bitkit.ext.createChannelDetails import to.bitkit.ext.formatToString import to.bitkit.ext.uri import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.LightningState import to.bitkit.ui.components.BodyM @@ -92,7 +92,7 @@ fun NodeInfoScreen( onDisconnectPeer = { wallet.disconnectPeer(it) }, onCopy = { text -> app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), description = text ) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 0ef916d3f..ae21265d2 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.repositories.ConnectivityState import to.bitkit.viewmodels.AppViewModel @@ -31,7 +31,7 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.other__connection_back_title), description = context.getString(R.string.other__connection_back_msg), ) @@ -39,7 +39,7 @@ fun IsOnlineTracker( ConnectivityState.DISCONNECTED -> { app.toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__connection_issue), description = context.getString(R.string.other__connection_issue_explain), ) diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index aef3abf9d..4632001bc 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -53,6 +53,7 @@ import dev.chrisbanes.haze.rememberHazeState import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -322,7 +323,7 @@ private fun ToastViewPreview() { ) { ToastView( toast = Toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = "You're still offline", description = "Check your connection to keep using Bitkit.", autoHide = true, @@ -331,7 +332,7 @@ private fun ToastViewPreview() { ) ToastView( toast = Toast( - type = Toast.ToastType.LIGHTNING, + type = ToastType.LIGHTNING, title = "Instant Payments Ready", description = "You can now pay anyone, anywhere, instantly.", autoHide = true, @@ -340,7 +341,7 @@ private fun ToastViewPreview() { ) ToastView( toast = Toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "You're Back Online!", description = "Successfully reconnected to the Internet.", autoHide = true, @@ -349,7 +350,7 @@ private fun ToastViewPreview() { ) ToastView( toast = Toast( - type = Toast.ToastType.INFO, + type = ToastType.INFO, title = "General Message", description = "Used for neutral content to inform the user.", autoHide = false, @@ -358,7 +359,7 @@ private fun ToastViewPreview() { ) ToastView( toast = Toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Error Toast", description = "This is a toast message.", autoHide = true, @@ -372,9 +373,9 @@ private fun ToastViewPreview() { @ReadOnlyComposable @Composable private fun Toast.tintColor(): Color = when (type) { - Toast.ToastType.SUCCESS -> Colors.Green - Toast.ToastType.INFO -> Colors.Blue - Toast.ToastType.LIGHTNING -> Colors.Purple - Toast.ToastType.WARNING -> Colors.Brand - Toast.ToastType.ERROR -> Colors.Red + ToastType.SUCCESS -> Colors.Green + ToastType.INFO -> Colors.Blue + ToastType.LIGHTNING -> Colors.Purple + ToastType.WARNING -> Colors.Brand + ToastType.ERROR -> Colors.Red } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index fd53d23a9..a9785d623 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -12,8 +12,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.keychain.Keychain -import to.bitkit.models.Toast -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -21,6 +20,7 @@ import javax.inject.Inject class RecoveryMnemonicViewModel @Inject constructor( @ApplicationContext private val context: Context, private val keychain: Keychain, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(RecoveryMnemonicUiState()) @@ -42,11 +42,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.security__mnemonic_load_error), - description = context.getString(R.string.security__mnemonic_load_error), - ) + toaster.error(R.string.security__mnemonic_load_error) return@launch } @@ -66,7 +62,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - ToastEventBus.send(e) + toaster.error(e) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index c0f19f378..98061ef81 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -17,11 +17,10 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.env.Env -import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo import to.bitkit.repositories.WalletRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -32,6 +31,7 @@ class RecoveryViewModel @Inject constructor( private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, private val settingsStore: SettingsStore, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(RecoveryUiState()) @@ -73,11 +73,7 @@ class RecoveryViewModel @Inject constructor( isExportingLogs = false, ) } - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.common__error), - description = context.getString(R.string.other__logs_export_error), - ) + toaster.error(R.string.common__error, R.string.other__logs_export_error) } ) } @@ -98,11 +94,7 @@ class RecoveryViewModel @Inject constructor( }.onFailure { fallbackError -> Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.common__error), - description = context.getString(R.string.settings__support__link_error), - ) + toaster.error(R.string.common__error, R.string.settings__support__link_error) } } } @@ -119,13 +111,9 @@ class RecoveryViewModel @Inject constructor( fun wipeWallet() { viewModelScope.launch { walletRepo.wipeWallet().onFailure { error -> - ToastEventBus.send(error) + toaster.error(error) }.onSuccess { - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.security__wiped_title), - description = context.getString(R.string.security__wiped_message), - ) + toaster.success(R.string.security__wiped_title, R.string.security__wiped_message) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 84d7c76fd..7aad87e56 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -65,7 +65,7 @@ import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.getClipboardText import to.bitkit.ext.startActivityAppSettings -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton @@ -150,7 +150,7 @@ fun QrScanningScreen( val error = requireNotNull(result.exceptionOrNull()) Logger.error("Failed to scan QR code", error) app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), description = context.getString(R.string.other__qr_error_text), ) @@ -256,7 +256,7 @@ private fun handlePaste( val clipboard = context.getClipboardText()?.trim() if (clipboard.isNullOrBlank()) { app.toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.wallet__send_clipboard_empty_title), description = context.getString(R.string.wallet__send_clipboard_empty_text), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index ff033c2ce..c93bc2338 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -14,7 +14,7 @@ import androidx.navigation.NavController import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.activityListViewModel import to.bitkit.ui.appViewModel @@ -78,63 +78,63 @@ fun DevSettingsScreen( title = "Reset Settings State", onClick = { settings.reset() - app.toast(type = Toast.ToastType.SUCCESS, title = "Settings state reset") + app.toast(type = ToastType.SUCCESS, title = "Settings state reset") } ) SettingsTextButtonRow( title = "Reset All Activities", onClick = { activity.removeAllActivities() - app.toast(type = Toast.ToastType.SUCCESS, title = "Activities removed") + app.toast(type = ToastType.SUCCESS, title = "Activities removed") } ) SettingsTextButtonRow( title = "Reset Backup State", onClick = { viewModel.resetBackupState() - app.toast(type = Toast.ToastType.SUCCESS, title = "Backup state reset") + app.toast(type = ToastType.SUCCESS, title = "Backup state reset") } ) SettingsTextButtonRow( title = "Reset Widgets State", onClick = { viewModel.resetWidgetsState() - app.toast(type = Toast.ToastType.SUCCESS, title = "Widgets state reset") + app.toast(type = ToastType.SUCCESS, title = "Widgets state reset") } ) SettingsTextButtonRow( title = "Refresh Currency Rates", onClick = { viewModel.refreshCurrencyRates() - app.toast(type = Toast.ToastType.SUCCESS, title = "Currency rates refreshed") + app.toast(type = ToastType.SUCCESS, title = "Currency rates refreshed") } ) SettingsTextButtonRow( title = "Reset App Database", onClick = { viewModel.resetDatabase() - app.toast(type = Toast.ToastType.SUCCESS, title = "Database state reset") + app.toast(type = ToastType.SUCCESS, title = "Database state reset") } ) SettingsTextButtonRow( title = "Reset Blocktank State", onClick = { viewModel.resetBlocktankState() - app.toast(type = Toast.ToastType.SUCCESS, title = "Blocktank state reset") + app.toast(type = ToastType.SUCCESS, title = "Blocktank state reset") } ) SettingsTextButtonRow( title = "Reset Cache Store", onClick = { viewModel.resetCacheStore() - app.toast(type = Toast.ToastType.SUCCESS, title = "Cache store reset") + app.toast(type = ToastType.SUCCESS, title = "Cache store reset") } ) SettingsTextButtonRow( title = "Wipe App", onClick = { viewModel.wipeWallet() - app.toast(type = Toast.ToastType.SUCCESS, title = "Wallet wiped") + app.toast(type = ToastType.SUCCESS, title = "Wallet wiped") } ) @@ -145,14 +145,14 @@ fun DevSettingsScreen( onClick = { val count = 100 activity.generateRandomTestData(count) - app.toast(type = Toast.ToastType.SUCCESS, title = "Generated $count test activities") + app.toast(type = ToastType.SUCCESS, title = "Generated $count test activities") } ) SettingsTextButtonRow( "Fake New BG Receive", onClick = { viewModel.fakeBgReceive() - app.toast(type = Toast.ToastType.INFO, title = "Restart app to see the payment received sheet") + app.toast(type = ToastType.INFO, title = "Restart app to see the payment received sheet") } ) SettingsTextButtonRow( diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 3b3521c2e..1b2269d82 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton @@ -71,7 +71,7 @@ fun SavingsProgressScreen( if (nonTrustedChannels.isEmpty()) { // All channels are trusted peers - show error and navigate back immediately app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.lightning__close_error), description = context.getString(R.string.lightning__close_error_msg), ) @@ -82,7 +82,7 @@ fun SavingsProgressScreen( onGiveUp = { app.showSheet(Sheet.ForceTransfer) }, onTransferUnavailable = { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.lightning__close_error), description = context.getString(R.string.lightning__close_error_msg), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index 7e92fad64..1511ef44a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -26,7 +26,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ext.mockOrder -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.appViewModel @@ -91,7 +91,7 @@ fun SpendingAdvancedScreen( is TransferEffect.ToastError -> { isLoading = false app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = effect.title, description = effect.description, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt index f2cd73f25..4981a7958 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.BITCOIN_SYMBOL -import to.bitkit.models.Toast import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display @@ -39,7 +38,6 @@ import to.bitkit.ui.currencyViewModel import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -52,7 +50,6 @@ fun ExternalFeeCustomScreen( val uiState by viewModel.uiState.collectAsState() val currency = currencyViewModel ?: return val scope = rememberCoroutineScope() - val context = LocalContext.current var input by remember { @@ -88,18 +85,11 @@ fun ExternalFeeCustomScreen( } }, onContinue = { - val feeRate = input.toUIntOrNull() ?: 0u - if (feeRate == 0u) { - scope.launch { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = context.getString(R.string.wallet__min_possible_fee_rate), - description = context.getString(R.string.wallet__min_possible_fee_rate_msg), - ) + scope.launch { + if (viewModel.validateCustomFeeRate()) { + onBack() } - return@Content } - onBack() }, onBack = onBack, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index efd087df3..63859b1da 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -20,7 +20,6 @@ import to.bitkit.data.SettingsStore import to.bitkit.ext.WatchResult import to.bitkit.ext.of import to.bitkit.ext.watchUntil -import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType import to.bitkit.models.formatToModernDisplay @@ -28,7 +27,7 @@ import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.UiState -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -40,6 +39,7 @@ class ExternalNodeViewModel @Inject constructor( private val lightningRepo: LightningRepo, private val settingsStore: SettingsStore, private val transferRepo: to.bitkit.repositories.TransferRepo, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() @@ -73,11 +73,7 @@ class ExternalNodeViewModel @Inject constructor( _uiState.update { it.copy(peer = peer) } setEffect(SideEffect.ConnectionSuccess) } else { - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__error_add_title), - description = context.getString(R.string.lightning__error_add), - ) + toaster.error(R.string.lightning__error_add_title, R.string.lightning__error_add) } } } @@ -89,10 +85,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__error_add_uri), - ) + toaster.error(R.string.lightning__error_add_uri) } } } @@ -102,8 +95,7 @@ class ExternalNodeViewModel @Inject constructor( if (sats > maxAmount) { viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.lightning__spending_amount__error_max__title), description = context.getString(R.string.lightning__spending_amount__error_max__description) .replace("{amount}", maxAmount.formatToModernDisplay()), @@ -134,6 +126,15 @@ class ExternalNodeViewModel @Inject constructor( updateNetworkFee() } + suspend fun validateCustomFeeRate(): Boolean { + val feeRate = _uiState.value.customFeeRate ?: 0u + if (feeRate == 0u) { + toaster.info(R.string.wallet__min_possible_fee_rate, R.string.wallet__min_possible_fee_rate_msg) + return false + } + return true + } + private fun updateNetworkFee() { viewModelScope.launch { val amountSats = _uiState.value.amount.sats @@ -180,8 +181,7 @@ class ExternalNodeViewModel @Inject constructor( }.onFailure { e -> val error = e.message.orEmpty() Logger.warn("Error opening channel with peer: '${_uiState.value.peer}': '$error'") - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.lightning__error_channel_purchase), description = context.getString(R.string.lightning__error_channel_setup_msg) .replace("{raw}", error), diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 52401cc90..9a38a8edb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -12,16 +12,16 @@ import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.ext.of -import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo import to.bitkit.ui.Routes -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import javax.inject.Inject @HiltViewModel class LnurlChannelViewModel @Inject constructor( @ApplicationContext private val context: Context, private val lightningRepo: LightningRepo, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(LnurlChannelUiState()) @@ -68,10 +68,9 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.other__lnurl_channel_success_title), - description = context.getString(R.string.other__lnurl_channel_success_msg_no_peer), + toaster.success( + R.string.other__lnurl_channel_success_title, + R.string.other__lnurl_channel_success_msg_no_peer, ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> @@ -83,8 +82,7 @@ class LnurlChannelViewModel @Inject constructor( } suspend fun errorToast(error: Throwable) { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.other__lnurl_channel_error), description = error.message ?: "Unknown error", ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index 6bc8520b6..b0ca70f3e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -58,7 +58,7 @@ import to.bitkit.ext.toActivityItemDate import to.bitkit.ext.toActivityItemTime import to.bitkit.ext.totalValue import to.bitkit.models.FeeRate.Companion.getFeeShortDescription -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel @@ -228,7 +228,7 @@ fun ActivityDetailScreen( boostTxDoesExist = boostTxDoesExist, onCopy = { text -> app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = copyToastTitle, description = text.ellipsisMiddle(40) ) @@ -251,7 +251,7 @@ fun ActivityDetailScreen( item = it, onSuccess = { app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.wallet__boost_success_title), description = context.getString(R.string.wallet__boost_success_msg), testTag = "BoostSuccessToast" @@ -261,7 +261,7 @@ fun ActivityDetailScreen( }, onFailure = { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__boost_error_title), description = context.getString(R.string.wallet__boost_error_msg), testTag = "BoostFailureToast" @@ -270,14 +270,14 @@ fun ActivityDetailScreen( }, onMaxFee = { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__send_fee_error), description = context.getString(R.string.wallet__send_fee_error_max) ) }, onMinFee = { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__send_fee_error), description = context.getString(R.string.wallet__send_fee_error_min) ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index f6b8e16f0..130a11fc2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -45,7 +45,7 @@ import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BalanceHeaderView @@ -167,7 +167,7 @@ fun ActivityExploreScreen( boostTxDoesExist = boostTxDoesExist, onCopy = { text -> app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = toastMessage, description = text.ellipsisMiddle(40), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 292aeaca3..60a356589 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -32,7 +32,7 @@ import to.bitkit.ext.maxWithdrawableSat import to.bitkit.models.BalanceState import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.safe import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalBalances @@ -109,7 +109,7 @@ fun SendAmountScreen( onClickMax = { maxSats -> if (uiState.lnurl == null) { app?.toast( - type = Toast.ToastType.INFO, + type = ToastType.INFO, title = context.getString(R.string.wallet__send_max_spending__title), description = context.getString(R.string.wallet__send_max_spending__description) ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt index 851027e26..b3cfcb9b6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt @@ -16,7 +16,7 @@ import to.bitkit.env.Defaults import to.bitkit.ext.rawId import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.LightningRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -25,6 +25,7 @@ class SendCoinSelectionViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, private val activityRepo: ActivityRepo, + private val toaster: Toaster, ) : ViewModel() { companion object { private const val TAG = "SendCoinSelectionViewModel" @@ -67,7 +68,7 @@ class SendCoinSelectionViewModel @Inject constructor( } }.onFailure { Logger.error("Failed to load UTXOs for coin selection", it, context = TAG) - ToastEventBus.send(Exception("Failed to load UTXOs: ${it.message}")) + toaster.error("Failed to load UTXOs: ${it.message}") } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index cdd4fac8f..8e12bda98 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -12,13 +12,12 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.models.FeeRate -import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.ui.components.KEY_DELETE -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.SendUiState import javax.inject.Inject @@ -32,6 +31,7 @@ class SendFeeViewModel @Inject constructor( private val currencyRepo: CurrencyRepo, private val walletRepo: WalletRepo, @ApplicationContext private val context: Context, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(SendFeeUiState()) val uiState = _uiState.asStateFlow() @@ -105,20 +105,12 @@ class SendFeeViewModel @Inject constructor( // TODO update to use minimum instead of slow when using mempool api val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = context.getString(R.string.wallet__min_possible_fee_rate), - description = context.getString(R.string.wallet__min_possible_fee_rate_msg) - ) + toaster.info(R.string.wallet__min_possible_fee_rate, R.string.wallet__min_possible_fee_rate_msg) return false } if (satsPerVByte > maxSatsPerVByte) { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = context.getString(R.string.wallet__max_possible_fee_rate), - description = context.getString(R.string.wallet__max_possible_fee_rate_msg) - ) + toaster.info(R.string.wallet__max_possible_fee_rate, R.string.wallet__max_possible_fee_rate_msg) return false } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index 26e487a9d..3ff163ade 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -59,7 +59,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import to.bitkit.R import to.bitkit.ext.startActivityAppSettings -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB @@ -140,7 +140,7 @@ fun SendRecipientScreen( val error = requireNotNull(result.exceptionOrNull()) Logger.error("Scan failed", error, context = TAG) app?.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), description = context.getString(R.string.other__qr_error_text), ) @@ -174,7 +174,7 @@ fun SendRecipientScreen( }.onFailure { Logger.error("Camera initialization failed", it, context = TAG) app?.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), description = context.getString(R.string.other__camera_init_error) .replace("{message}", it.message.orEmpty()) diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index fcc6ff45c..4965889e5 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -28,7 +28,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption @@ -113,14 +113,14 @@ fun BlocktankRegtestScreen( val txId = viewModel.regtestDeposit(depositAddress, sats) Logger.debug("Deposit successful with txId: $txId") app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "Success", description = "Deposit successful. TxID: $txId", ) }.onFailure { Logger.error("Deposit failed", it) app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Failed to deposit", description = it.message.orEmpty(), ) @@ -159,14 +159,14 @@ fun BlocktankRegtestScreen( viewModel.regtestMine(count) Logger.debug("Successfully mined $count blocks") app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "Success", description = "Successfully mined $count blocks", ) }.onFailure { Logger.error("Mining failed", it) app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Failed to mine", description = it.message.orEmpty(), ) @@ -211,14 +211,14 @@ fun BlocktankRegtestScreen( val paymentId = viewModel.regtestPay(paymentInvoice, amount) Logger.debug("Payment successful with ID: $paymentId") app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "Success", description = "Payment successful. ID: $paymentId", ) }.onFailure { Logger.error("Payment failed", it) app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Failed to pay invoice from LND", description = it.message.orEmpty(), ) @@ -278,7 +278,7 @@ fun BlocktankRegtestScreen( ) Logger.debug("Channel closed successfully with txId: $closingTxId") app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "Success", description = "Channel closed. Closing TxID: $closingTxId" ) diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index f08ac96af..1893dc944 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SettingsButtonRow @@ -76,7 +76,7 @@ fun SettingsScreen( haptic.performHapticFeedback(HapticFeedbackType.LongPress) app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString( if (newValue) { R.string.settings__dev_enabled_title diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 09353a86c..e65a58bd9 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -30,7 +30,7 @@ import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyS @@ -78,7 +78,7 @@ fun AddressViewerScreen( onCopy = { text -> context.setClipboardText(text) app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), description = text, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 72531e42f..1a46b44db 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up @@ -75,7 +75,7 @@ fun ElectrumConfigScreen( uiState.connectionResult?.let { result -> if (result.isSuccess) { app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.settings__es__server_updated_title), description = context.getString(R.string.settings__es__server_updated_message) .replace("{host}", uiState.host) @@ -84,7 +84,7 @@ fun ElectrumConfigScreen( ) } else { app.toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.settings__es__server_error), description = context.getString(R.string.settings__es__server_error_description), testTag = "ElectrumErrorToast", diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index ed41850f1..a7c25036d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -23,10 +23,9 @@ import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServer import to.bitkit.models.ElectrumServerPeer import to.bitkit.models.MAX_VALID_PORT -import to.bitkit.models.Toast import to.bitkit.models.getDefaultPort import to.bitkit.repositories.LightningRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import javax.inject.Inject @HiltViewModel @@ -35,6 +34,7 @@ class ElectrumConfigViewModel @Inject constructor( @ApplicationContext private val context: Context, private val settingsStore: SettingsStore, private val lightningRepo: LightningRepo, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(ElectrumConfigUiState()) @@ -246,8 +246,7 @@ class ElectrumConfigViewModel @Inject constructor( viewModelScope.launch { val validationError = validateInput() if (validationError != null) { - ToastEventBus.send( - type = Toast.ToastType.WARNING, + toaster.warning( title = context.getString(R.string.settings__es__error_peer), description = validationError, ) @@ -268,8 +267,7 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput(host, port) if (validationError != null) { - ToastEventBus.send( - type = Toast.ToastType.WARNING, + toaster.warning( title = context.getString(R.string.settings__es__error_peer), description = validationError, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index cf74847db..28d9d08d0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up @@ -68,14 +68,14 @@ fun RgsServerScreen( uiState.connectionResult?.let { result -> if (result.isSuccess) { app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.settings__rgs__update_success_title), description = context.getString(R.string.settings__rgs__update_success_description), testTag = "RgsUpdatedToast", ) } else { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__ldk_start_error_title), description = result.exceptionOrNull()?.message ?: "Unknown error", testTag = "RgsErrorToast", diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 3c41f83b5..385d8dd30 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -19,11 +19,10 @@ import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.models.BackupCategory import to.bitkit.models.HealthState -import to.bitkit.models.Toast import to.bitkit.repositories.HealthRepo import to.bitkit.ui.settings.backups.BackupContract.SideEffect import to.bitkit.ui.settings.backups.BackupContract.UiState -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -35,6 +34,7 @@ class BackupNavSheetViewModel @Inject constructor( private val keychain: Keychain, private val healthRepo: HealthRepo, private val cacheStore: CacheStore, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) @@ -86,11 +86,7 @@ class BackupNavSheetViewModel @Inject constructor( } }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.security__mnemonic_error), - description = context.getString(R.string.security__mnemonic_error_description), - ) + toaster.warning(R.string.security__mnemonic_error, R.string.security__mnemonic_error_description) } } @@ -154,6 +150,12 @@ class BackupNavSheetViewModel @Inject constructor( fun resetState() { _uiState.update { UiState() } } + + fun onMnemonicCopied() { + viewModelScope.launch { + toaster.success(R.string.common__copied, R.string.security__mnemonic_copied) + } + } } interface BackupContract { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 0468834aa..843441ce0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -41,7 +41,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ext.setClipboardText -import to.bitkit.models.Toast import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BottomSheetPreview @@ -51,7 +50,6 @@ import to.bitkit.ui.components.SheetSize import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.effects.BlockScreenshots import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -63,24 +61,18 @@ fun ShowMnemonicScreen( uiState: BackupContract.UiState, onRevealClick: () -> Unit, onContinueClick: () -> Unit, + onMnemonicCopied: () -> Unit, ) { BlockScreenshots() val context = LocalContext.current - val scope = rememberCoroutineScope() ShowMnemonicContent( mnemonic = uiState.bip39Mnemonic, showMnemonic = uiState.showMnemonic, onRevealClick = onRevealClick, onCopyClick = { context.setClipboardText(uiState.bip39Mnemonic) - scope.launch { - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - description = context.getString(R.string.security__mnemonic_copied), - ) - } + onMnemonicCopied() }, onContinueClick = onContinueClick, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 04795bd4d..fffa115db 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -55,7 +55,7 @@ import to.bitkit.ext.DatePattern import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up @@ -129,7 +129,7 @@ fun ChannelDetailScreen( onCopyText = { text -> context.setClipboardText(text) app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), description = text, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 94174c2d6..a1324dd09 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -31,13 +31,12 @@ import to.bitkit.ext.calculateRemoteBalance import to.bitkit.ext.createChannelDetails import to.bitkit.ext.filterOpen import to.bitkit.ext.filterPending -import to.bitkit.models.Toast import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo import to.bitkit.repositories.WalletRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -51,6 +50,7 @@ class LightningConnectionsViewModel @Inject constructor( private val logsRepo: LogsRepo, private val walletRepo: WalletRepo, private val activityRepo: ActivityRepo, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(LightningConnectionsUiState()) @@ -338,11 +338,10 @@ class LightningConnectionsViewModel @Inject constructor( viewModelScope.launch { logsRepo.zipLogsForSharing() .onSuccess { uri -> onReady(uri) } - .onFailure { err -> - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.lightning__error_logs), - description = context.getString(R.string.lightning__error_logs_description), + .onFailure { + toaster.warning( + R.string.lightning__error_logs, + R.string.lightning__error_logs_description, ) } } @@ -454,10 +453,9 @@ class LightningConnectionsViewModel @Inject constructor( onSuccess = { walletRepo.syncNodeAndWallet() - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.lightning__close_success_title), - description = context.getString(R.string.lightning__close_success_msg), + toaster.success( + R.string.lightning__close_success_title, + R.string.lightning__close_success_msg, ) _closeConnectionUiState.update { @@ -470,10 +468,9 @@ class LightningConnectionsViewModel @Inject constructor( onFailure = { error -> Logger.error("Failed to close channel", e = error, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.lightning__close_error), - description = context.getString(R.string.lightning__close_error_msg), + toaster.warning( + R.string.lightning__close_error, + R.string.lightning__close_error_msg, ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastEventBus.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastEventBus.kt deleted file mode 100644 index 5613a4265..000000000 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastEventBus.kt +++ /dev/null @@ -1,34 +0,0 @@ -package to.bitkit.ui.shared.toast - -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import to.bitkit.models.Toast - -object ToastEventBus { - private val _events = MutableSharedFlow(extraBufferCapacity = 1) - val events = _events.asSharedFlow() - - suspend fun send( - type: Toast.ToastType, - title: String, - description: String? = null, - autoHide: Boolean = true, - visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, - ) { - _events.emit( - Toast(type, title, description, autoHide, visibilityTime) - ) - } - - suspend fun send(error: Throwable) { - _events.emit( - Toast( - type = Toast.ToastType.ERROR, - title = "Error", - description = error.message ?: "Unknown error", - autoHide = true, - visibilityTime = Toast.VISIBILITY_TIME_DEFAULT, - ) - ) - } -} diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt new file mode 100644 index 000000000..096b381f6 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -0,0 +1,144 @@ +package to.bitkit.ui.shared.toast + +import android.content.Context +import androidx.annotation.StringRes +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import to.bitkit.R +import to.bitkit.models.Toast +import to.bitkit.models.ToastType +import javax.inject.Inject +import javax.inject.Singleton + +@Suppress("TooManyFunctions") +@Singleton +class Toaster @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val _events = MutableSharedFlow(extraBufferCapacity = 1) + val events: SharedFlow = _events.asSharedFlow() + + @Suppress("LongParameterList") + private suspend fun emit( + type: ToastType, + title: String, + description: String? = null, + autoHide: Boolean = true, + visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, + testTag: String? = null, + ) { + _events.emit( + Toast( + type = type, + title = title, + description = description, + autoHide = autoHide, + visibilityTime = visibilityTime, + testTag = testTag, + ) + ) + } + + // region Success + suspend fun success( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.SUCCESS, title, description, testTag = testTag) + + suspend fun success( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.SUCCESS, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + // endregion + + // region Info + suspend fun info( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.INFO, title, description, testTag = testTag) + + suspend fun info( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.INFO, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + // endregion + + // region Lightning + suspend fun lightning( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.LIGHTNING, title, description, testTag = testTag) + + suspend fun lightning( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.LIGHTNING, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + // endregion + + // region Warning + suspend fun warning( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.WARNING, title, description, testTag = testTag) + + suspend fun warning( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.WARNING, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + // endregion + + // region Error + suspend fun error( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.ERROR, title, description, testTag = testTag) + + suspend fun error( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.ERROR, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + + suspend fun error(throwable: Throwable) = emit( + type = ToastType.ERROR, + title = context.getString(R.string.common__error), + description = throwable.message ?: context.getString(R.string.common__error_body), + ) + // endregion +} diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt index 0ccaf924b..4d774bf30 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt @@ -97,6 +97,7 @@ fun BackupSheet( uiState = uiState, onRevealClick = viewModel::onRevealMnemonic, onContinueClick = viewModel::onShowMnemonicContinue, + onMnemonicCopied = viewModel::onMnemonicCopied, ) } composableWithDefaultTransitions { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 94c9a8cb2..d148c609b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -82,6 +82,7 @@ import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.TransactionSpeed import to.bitkit.models.safe import to.bitkit.models.toActivityFilter @@ -102,8 +103,8 @@ import to.bitkit.services.AppUpdaterService import to.bitkit.services.MigrationService import to.bitkit.ui.Routes import to.bitkit.ui.components.Sheet -import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.shared.toast.ToastQueueManager +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.TRANSITION_SCREEN_MS import to.bitkit.utils.Logger @@ -127,6 +128,7 @@ class AppViewModel @Inject constructor( healthRepo: HealthRepo, toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, + toaster: Toaster, @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val keychain: Keychain, @@ -230,7 +232,7 @@ class AppViewModel @Inject constructor( init { viewModelScope.launch { - ToastEventBus.events.collect { + toaster.events.collect { toast(it.type, it.title, it.description, it.autoHide, it.visibilityTime) } } @@ -449,7 +451,7 @@ class AppViewModel @Inject constructor( delay(MIGRATION_AUTH_RESET_DELAY_MS) resetIsAuthenticatedStateInternal() toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Migration Warning", description = "Migration completed but node restart failed. Please restart the app." ) @@ -533,7 +535,7 @@ class AppViewModel @Inject constructor( return } toast( - type = Toast.ToastType.LIGHTNING, + type = ToastType.LIGHTNING, title = context.getString(R.string.lightning__channel_opened_title), description = context.getString(R.string.lightning__channel_opened_msg), testTag = "SpendingBalanceReadyToast", @@ -543,7 +545,7 @@ class AppViewModel @Inject constructor( private suspend fun notifyTransactionRemoved(event: Event.OnchainTransactionEvicted) { if (activityRepo.wasTransactionReplaced(event.txid)) return toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.wallet__toast_transaction_removed_title), description = context.getString(R.string.wallet__toast_transaction_removed_description), testTag = "TransactionRemovedToast", @@ -558,7 +560,7 @@ class AppViewModel @Inject constructor( } private fun notifyTransactionUnconfirmed() = toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.wallet__toast_transaction_unconfirmed_title), description = context.getString(R.string.wallet__toast_transaction_unconfirmed_description), testTag = "TransactionUnconfirmedToast", @@ -567,7 +569,7 @@ class AppViewModel @Inject constructor( private suspend fun notifyTransactionReplaced(event: Event.OnchainTransactionReplaced) { val isReceive = activityRepo.isReceivedTransaction(event.txid) toast( - type = Toast.ToastType.INFO, + type = ToastType.INFO, title = when (isReceive) { true -> R.string.wallet__toast_received_transaction_replaced_title else -> R.string.wallet__toast_transaction_replaced_title @@ -584,7 +586,7 @@ class AppViewModel @Inject constructor( } private fun notifyPaymentFailed() = toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__toast_payment_failed_title), description = context.getString(R.string.wallet__toast_payment_failed_description), testTag = "PaymentFailedToast", @@ -775,7 +777,7 @@ class AppViewModel @Inject constructor( val minSendable = lnurl.data.minSendableSat() if (_sendUiState.value.amount < minSendable) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__lnurl_pay__error_min__title), description = context.getString(R.string.wallet__lnurl_pay__error_min__description) .replace("{amount}", minSendable.toString()), @@ -830,7 +832,7 @@ class AppViewModel @Inject constructor( val data = context.getClipboardText()?.trim() if (data.isNullOrBlank()) { toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.wallet__send_clipboard_empty_title), description = context.getString(R.string.wallet__send_clipboard_empty_text), ) @@ -875,7 +877,7 @@ class AppViewModel @Inject constructor( else -> { Logger.warn("Unhandled scan data: $scan", context = TAG) toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__scan_err_decoding), description = context.getString(R.string.other__scan_err_interpret_title), ) @@ -892,7 +894,7 @@ class AppViewModel @Inject constructor( ?.takeIf { invoice -> if (invoice.isExpired) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__scan_err_decoding), description = context.getString(R.string.other__scan__error__expired), ) @@ -961,7 +963,7 @@ class AppViewModel @Inject constructor( private suspend fun onScanLightning(invoice: LightningInvoice, scanResult: String) { if (invoice.isExpired) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__scan_err_decoding), description = context.getString(R.string.other__scan__error__expired), ) @@ -973,7 +975,7 @@ class AppViewModel @Inject constructor( if (!lightningRepo.canSend(invoice.amountSatoshis)) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__error_insufficient_funds_title), description = context.getString(R.string.wallet__error_insufficient_funds_msg) ) @@ -1017,7 +1019,7 @@ class AppViewModel @Inject constructor( if (!lightningRepo.canSend(minSendable)) { toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_pay_error), description = context.getString(R.string.other__lnurl_pay_error_no_capacity), ) @@ -1065,7 +1067,7 @@ class AppViewModel @Inject constructor( if (minWithdrawable > maxWithdrawable) { toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_withdr_error), description = context.getString(R.string.other__lnurl_withdr_error_minmax) ) @@ -1114,14 +1116,14 @@ class AppViewModel @Inject constructor( domain = domain, ).onFailure { toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_auth_error), description = context.getString(R.string.other__lnurl_auth_error_msg) .replace("{raw}", it.message?.takeIf { m -> m.isNotBlank() } ?: it.javaClass.simpleName), ) }.onSuccess { toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.other__lnurl_auth_success_title), description = when (domain.isNotBlank()) { true -> context.getString(R.string.other__lnurl_auth_success_msg_domain) @@ -1153,7 +1155,7 @@ class AppViewModel @Inject constructor( // val appNetwork = Env.network.toCoreNetworkType() // if (network != appNetwork) { // toast( - // type = Toast.ToastType.WARNING, + // type = ToastType.WARNING, // title = context.getString(R.string.other__qr_error_network_header), // description = context.getString(R.string.other__qr_error_network_text) // .replace("{selectedNetwork}", appNetwork.name) @@ -1358,7 +1360,7 @@ class AppViewModel @Inject constructor( }.onFailure { e -> Logger.error(msg = "Error sending onchain payment", e = e, context = TAG) toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__error_sending_title), description = e.message ?: context.getString(R.string.common__error_body) ) @@ -1450,7 +1452,7 @@ class AppViewModel @Inject constructor( paymentRequest = invoice ).onSuccess { toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.other__lnurl_withdr_success_title), description = context.getString(R.string.other__lnurl_withdr_success_msg), ) @@ -1793,7 +1795,7 @@ class AppViewModel @Inject constructor( val currentToast: StateFlow = toastManager.currentToast fun toast( - type: Toast.ToastType, + type: ToastType, title: String, description: String? = null, autoHide: Boolean = true, @@ -1814,7 +1816,7 @@ class AppViewModel @Inject constructor( fun toast(error: Throwable) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.common__error), description = error.message ?: context.getString(R.string.common__error_body) ) @@ -1867,7 +1869,7 @@ class AppViewModel @Inject constructor( if (newAttempts <= 0) { toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.security__wiped_title), description = context.getString(R.string.security__wiped_message), ) diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index 30a5bd849..11dae996b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -18,13 +18,12 @@ import to.bitkit.env.Env import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType -import to.bitkit.models.Toast import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo import to.bitkit.repositories.WalletRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -41,29 +40,26 @@ class DevSettingsViewModel @Inject constructor( private val cacheStore: CacheStore, private val blocktankRepo: BlocktankRepo, private val appDb: AppDb, + private val toaster: Toaster, ) : ViewModel() { fun openChannel() = viewModelScope.launch { val peer = lightningRepo.getPeers()?.firstOrNull() if (peer == null) { - ToastEventBus.send(type = Toast.ToastType.WARNING, title = "No peer connected") + toaster.warning("No peer connected") return@launch } lightningRepo.openChannel(peer, 50_000u, 25_000u) - .onSuccess { - ToastEventBus.send(type = Toast.ToastType.INFO, title = "Channel pending") - } - .onFailure { ToastEventBus.send(it) } + .onSuccess { toaster.info("Channel pending") } + .onFailure { toaster.error(it) } } fun registerForNotifications() = viewModelScope.launch { lightningRepo.registerForNotifications() - .onSuccess { - ToastEventBus.send(type = Toast.ToastType.INFO, title = "Registered for notifications") - } - .onFailure { ToastEventBus.send(it) } + .onSuccess { toaster.info("Registered for notifications") } + .onFailure { toaster.error(it) } } fun testLspNotification() = viewModelScope.launch { @@ -74,9 +70,9 @@ class DevSettingsViewModel @Inject constructor( notificationType = "incomingHtlc", customUrl = Env.blocktankNotificationApiUrl, ) - ToastEventBus.send(type = Toast.ToastType.INFO, title = "LSP notification sent to this device") + toaster.info("LSP notification sent to this device") }.onFailure { - ToastEventBus.send(type = Toast.ToastType.WARNING, title = "Error testing LSP notification") + toaster.warning("Error testing LSP notification") } } @@ -103,10 +99,9 @@ class DevSettingsViewModel @Inject constructor( logsRepo.zipLogsForSharing() .onSuccess { uri -> onReady(uri) } .onFailure { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.lightning__error_logs), - description = context.getString(R.string.lightning__error_logs_description), + toaster.warning( + R.string.lightning__error_logs, + R.string.lightning__error_logs_description, ) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt index d8aced251..1ade94f06 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt @@ -17,10 +17,9 @@ import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.data.backup.VssBackupClient import to.bitkit.di.BgDispatcher import to.bitkit.ext.of -import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo import to.bitkit.services.NetworkGraphInfo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import java.io.File import javax.inject.Inject @@ -31,6 +30,7 @@ class LdkDebugViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, private val vssBackupClient: VssBackupClient, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(LdkDebugUiState()) @@ -43,12 +43,7 @@ class LdkDebugViewModel @Inject constructor( fun addPeer() { val uri = _uiState.value.nodeUri.trim() if (uri.isEmpty()) { - viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = "Please enter a node URI", - ) - } + viewModelScope.launch { toaster.warning("Please enter a node URI") } return } connectPeer(uri) @@ -60,12 +55,7 @@ class LdkDebugViewModel @Inject constructor( val pastedUri = clipData?.getItemAt(0)?.text?.toString()?.trim() if (pastedUri.isNullOrEmpty()) { - viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = "Clipboard is empty", - ) - } + viewModelScope.launch { toaster.warning("Clipboard is empty") } return } @@ -81,26 +71,15 @@ class LdkDebugViewModel @Inject constructor( lightningRepo.connectPeer(peer) }.onSuccess { result -> result.onSuccess { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Peer connected", - ) + toaster.info("Peer connected") _uiState.update { it.copy(nodeUri = "") } }.onFailure { e -> Logger.error("Failed to connect peer", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to connect peer", - description = e.message, - ) + toaster.error("Failed to connect peer", e.message) } }.onFailure { e -> Logger.error("Failed to parse peer URI", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Invalid node URI format", - description = e.message, - ) + toaster.error("Invalid node URI format", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -118,15 +97,9 @@ class LdkDebugViewModel @Inject constructor( context = TAG ) _uiState.update { it.copy(networkGraphInfo = info) } - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Network graph info logged", - ) + toaster.info("Network graph info logged") } else { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = "Failed to get network graph info", - ) + toaster.warning("Failed to get network graph info") } } } @@ -137,18 +110,11 @@ class LdkDebugViewModel @Inject constructor( val outputDir = context.cacheDir.absolutePath lightningRepo.exportNetworkGraphToFile(outputDir).onSuccess { file -> Logger.info("Network graph exported to: ${file.absolutePath}", context = TAG) - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Network graph exported", - ) + toaster.info("Network graph exported") onFileReady(file) }.onFailure { e -> Logger.error("Failed to export network graph", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to export network graph", - description = e.message, - ) + toaster.error("Failed to export network graph", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -160,17 +126,10 @@ class LdkDebugViewModel @Inject constructor( vssBackupClient.listKeys().onSuccess { keys -> Logger.info("VSS keys: ${keys.size}", context = TAG) _uiState.update { it.copy(vssKeys = keys) } - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Found ${keys.size} VSS key(s)", - ) + toaster.info("Found ${keys.size} VSS key(s)") }.onFailure { e -> Logger.error("Failed to list VSS keys", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to list VSS keys", - description = e.message, - ) + toaster.error("Failed to list VSS keys", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -182,17 +141,10 @@ class LdkDebugViewModel @Inject constructor( vssBackupClient.deleteAllKeys().onSuccess { deletedCount -> Logger.info("Deleted $deletedCount VSS keys", context = TAG) _uiState.update { it.copy(vssKeys = emptyList()) } - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Deleted $deletedCount VSS key(s)", - ) + toaster.info("Deleted $deletedCount VSS key(s)") }.onFailure { e -> Logger.error("Failed to delete VSS keys", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to delete VSS keys", - description = e.message, - ) + toaster.error("Failed to delete VSS keys", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -208,24 +160,14 @@ class LdkDebugViewModel @Inject constructor( _uiState.update { state -> state.copy(vssKeys = state.vssKeys.filter { it.key != key }) } - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Deleted key: $key", - ) + toaster.info("Deleted key: $key") } else { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = "Key not found: $key", - ) + toaster.warning("Key not found: $key") } } .onFailure { e -> Logger.error("Failed to delete VSS key: $key", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to delete key", - description = e.message, - ) + toaster.error("Failed to delete key", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -237,18 +179,11 @@ class LdkDebugViewModel @Inject constructor( lightningRepo.restartNode() .onSuccess { Logger.info("Node restarted successfully", context = TAG) - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Node restarted", - ) + toaster.info("Node restarted") } .onFailure { e -> Logger.error("Failed to restart node", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to restart node", - description = e.message, - ) + toaster.error("Failed to restart node", e.message) } _uiState.update { it.copy(isLoading = false) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 9a83e0472..9c0e72d41 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -29,7 +29,6 @@ import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.ext.amountOnClose -import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType import to.bitkit.models.safe @@ -37,7 +36,7 @@ import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.TransferRepo import to.bitkit.repositories.WalletRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject import kotlin.math.min @@ -62,6 +61,7 @@ class TransferViewModel @Inject constructor( private val cacheStore: CacheStore, private val transferRepo: TransferRepo, private val clock: Clock, + private val toaster: Toaster, ) : ViewModel() { private val _spendingUiState = MutableStateFlow(TransferToSpendingUiState()) val spendingUiState = _spendingUiState.asStateFlow() @@ -213,7 +213,7 @@ class TransferViewModel @Inject constructor( launch { watchOrder(order.id) } } .onFailure { error -> - ToastEventBus.send(error) + toaster.error(error) } } } @@ -458,10 +458,9 @@ class TransferViewModel @Inject constructor( if (nonTrustedChannels.isEmpty()) { channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__force_failed_title), - description = context.getString(R.string.lightning__force_failed_msg) + toaster.error( + R.string.lightning__force_failed_title, + R.string.lightning__force_failed_msg ) return@runCatching } @@ -483,25 +482,22 @@ class TransferViewModel @Inject constructor( val initMsg = context.getString(R.string.lightning__force_init_msg) val skippedMsg = context.getString(R.string.lightning__force_channels_skipped) val description = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg - ToastEventBus.send( - type = Toast.ToastType.LIGHTNING, + toaster.lightning( title = context.getString(R.string.lightning__force_init_title), description = description, ) } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__force_failed_title), - description = context.getString(R.string.lightning__force_failed_msg) + toaster.error( + R.string.lightning__force_failed_title, + R.string.lightning__force_failed_msg ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__force_failed_title), - description = context.getString(R.string.lightning__force_failed_msg) + toaster.error( + R.string.lightning__force_failed_title, + R.string.lightning__force_failed_msg ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 471fce9d5..04436af6e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -26,7 +26,6 @@ import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher -import to.bitkit.models.Toast import to.bitkit.repositories.BackupRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo @@ -35,7 +34,7 @@ import to.bitkit.repositories.SyncSource import to.bitkit.repositories.WalletRepo import to.bitkit.services.MigrationService import to.bitkit.ui.onboarding.LOADING_MS -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import to.bitkit.utils.isTxSyncTimeout import javax.inject.Inject @@ -54,6 +53,7 @@ class WalletViewModel @Inject constructor( private val backupRepo: BackupRepo, private val blocktankRepo: BlocktankRepo, private val migrationService: MigrationService, + private val toaster: Toaster, ) : ViewModel() { companion object { private const val TAG = "WalletViewModel" @@ -126,8 +126,7 @@ class WalletViewModel @Inject constructor( Logger.error("RN migration failed", it, context = TAG) migrationService.markMigrationChecked() migrationService.setShowingMigrationLoading(false) - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = "Migration Failed", description = "Please restore your wallet manually using your recovery phrase" ) @@ -255,7 +254,7 @@ class WalletViewModel @Inject constructor( .onFailure { Logger.error("Node startup error", it, context = TAG) if (it !is RecoveryModeError) { - ToastEventBus.send(it) + toaster.error(it) } } } @@ -267,7 +266,7 @@ class WalletViewModel @Inject constructor( lightningRepo.stop() .onFailure { Logger.error("Node stop error", it) - ToastEventBus.send(it) + toaster.error(it) } } } @@ -277,7 +276,7 @@ class WalletViewModel @Inject constructor( .onFailure { Logger.error("Failed to refresh state: ${it.message}", it) if (it is CancellationException || it.isTxSyncTimeout()) return@onFailure - ToastEventBus.send(it) + toaster.error(it) } } @@ -301,15 +300,10 @@ class WalletViewModel @Inject constructor( viewModelScope.launch { lightningRepo.disconnectPeer(peer) .onSuccess { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = context.getString(R.string.common__success), - description = context.getString(R.string.wallet__peer_disconnected) - ) + toaster.info(R.string.common__success, R.string.wallet__peer_disconnected) } .onFailure { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.common__error), description = it.message ?: context.getString(R.string.common__error_body) ) @@ -319,8 +313,7 @@ class WalletViewModel @Inject constructor( fun updateBip21Invoice(amountSats: ULong? = walletState.value.bip21AmountSats) = viewModelScope.launch { walletRepo.updateBip21Invoice(amountSats).onFailure { error -> - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.wallet__error_invoice_update), description = error.message ?: context.getString(R.string.common__error_body) ) @@ -335,7 +328,7 @@ class WalletViewModel @Inject constructor( fun wipeWallet() = viewModelScope.launch(bgDispatcher) { walletRepo.wipeWallet().onFailure { - ToastEventBus.send(it) + toaster.error(it) } } @@ -346,7 +339,7 @@ class WalletViewModel @Inject constructor( backupRepo.scheduleFullBackup() } .onFailure { - ToastEventBus.send(it) + toaster.error(it) } } @@ -358,7 +351,7 @@ class WalletViewModel @Inject constructor( mnemonic = mnemonic, bip39Passphrase = bip39Passphrase, ).onFailure { - ToastEventBus.send(it) + toaster.error(it) } } @@ -366,13 +359,13 @@ class WalletViewModel @Inject constructor( fun addTagToSelected(newTag: String) = viewModelScope.launch { walletRepo.addTagToSelected(newTag).onFailure { - ToastEventBus.send(it) + toaster.error(it) } } fun removeTag(tag: String) = viewModelScope.launch { walletRepo.removeTag(tag).onFailure { - ToastEventBus.send(it) + toaster.error(it) } } diff --git a/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt index 8cbdcdf19..3a247f567 100644 --- a/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt @@ -17,6 +17,7 @@ import to.bitkit.models.FxRate import to.bitkit.models.PrimaryDisplay import to.bitkit.services.CurrencyService import to.bitkit.test.BaseUnitTest +import to.bitkit.ui.shared.toast.Toaster import java.math.BigDecimal import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -33,6 +34,7 @@ class CurrencyRepoTest : BaseUnitTest() { private val settingsStore = mock() private val cacheStore = mock() private val clock = mock() + private val toaster = mock() private lateinit var sut: CurrencyRepo @@ -75,6 +77,7 @@ class CurrencyRepoTest : BaseUnitTest() { currencyService = currencyService, settingsStore = settingsStore, cacheStore = cacheStore, + toaster = toaster, enablePolling = false, clock = clock ) diff --git a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt index e80b3f74b..5b6c73a5b 100644 --- a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt @@ -27,6 +27,7 @@ import to.bitkit.repositories.WalletRepo import to.bitkit.repositories.WalletState import to.bitkit.services.MigrationService import to.bitkit.test.BaseUnitTest +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.RestoreState import to.bitkit.viewmodels.WalletViewModel @@ -41,6 +42,7 @@ class WalletViewModelTest : BaseUnitTest() { private val backupRepo = mock() private val blocktankRepo = mock() private val migrationService = mock() + private val toaster = mock() private val lightningState = MutableStateFlow(LightningState()) private val walletState = MutableStateFlow(WalletState()) @@ -63,6 +65,7 @@ class WalletViewModelTest : BaseUnitTest() { backupRepo = backupRepo, blocktankRepo = blocktankRepo, migrationService = migrationService, + toaster = toaster, ) } @@ -247,6 +250,7 @@ class WalletViewModelTest : BaseUnitTest() { backupRepo = backupRepo, blocktankRepo = blocktankRepo, migrationService = migrationService, + toaster = toaster, ) assertEquals(RestoreState.Initial, testSut.restoreState.value) @@ -287,6 +291,7 @@ class WalletViewModelTest : BaseUnitTest() { backupRepo = backupRepo, blocktankRepo = blocktankRepo, migrationService = migrationService, + toaster = toaster, ) // Trigger restore to put state in non-idle diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt index 703926a78..36597e925 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt @@ -17,6 +17,7 @@ import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.test.BaseUnitTest import to.bitkit.ui.components.KEY_DELETE +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.SendUiState import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -28,6 +29,7 @@ class SendFeeViewModelTest : BaseUnitTest() { private val currencyRepo: CurrencyRepo = mock() private val walletRepo: WalletRepo = mock() private val context: Context = mock() + private val toaster: Toaster = mock() private val balance = 100_000uL private val fee = 1_000uL @@ -42,7 +44,7 @@ class SendFeeViewModelTest : BaseUnitTest() { whenever(walletRepo.balanceState) .thenReturn(MutableStateFlow(BalanceState(totalOnchainSats = balance))) - sut = SendFeeViewModel(lightningRepo, currencyRepo, walletRepo, context) + sut = SendFeeViewModel(lightningRepo, currencyRepo, walletRepo, context, toaster) } @Test diff --git a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt index e36008926..04b7d9502 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt @@ -28,6 +28,7 @@ import to.bitkit.ui.components.KEY_000 import to.bitkit.ui.components.KEY_DECIMAL import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPadType +import to.bitkit.ui.shared.toast.Toaster import kotlin.time.Clock import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime @@ -42,6 +43,7 @@ class AmountInputViewModelTest : BaseUnitTest() { private val settingsStore = mock() private val cacheStore = mock() private val clock = mock() + private val toaster = mock() @Suppress("SpellCheckingInspection") private val testRates = listOf( @@ -68,6 +70,7 @@ class AmountInputViewModelTest : BaseUnitTest() { currencyService = currencyService, settingsStore = settingsStore, cacheStore = cacheStore, + toaster = toaster, enablePolling = false, clock = clock, ) @@ -819,6 +822,7 @@ class AmountInputViewModelTest : BaseUnitTest() { currencyService = currencyService, settingsStore = settingsStore, cacheStore = cacheStore, + toaster = toaster, enablePolling = false, clock = clock, ) From 2557f16085bc4f43594894127b24c823fea5d2bf Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:33:46 +0100 Subject: [PATCH 02/21] refactor: rename toast description param to body Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 8 +-- .../java/to/bitkit/repositories/BackupRepo.kt | 2 +- .../to/bitkit/repositories/CurrencyRepo.kt | 2 +- app/src/main/java/to/bitkit/ui/ContentView.kt | 4 +- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 2 +- .../bitkit/ui/components/IsOnlineTracker.kt | 4 +- .../java/to/bitkit/ui/components/ToastView.kt | 14 +++--- .../ui/screens/scanner/QrScanningScreen.kt | 4 +- .../screens/transfer/SavingsProgressScreen.kt | 4 +- .../transfer/SpendingAdvancedScreen.kt | 2 +- .../screens/transfer/SpendingAmountScreen.kt | 2 +- .../external/ExternalNodeViewModel.kt | 4 +- .../external/LnurlChannelViewModel.kt | 2 +- .../wallets/activity/ActivityDetailScreen.kt | 10 ++-- .../wallets/activity/ActivityExploreScreen.kt | 2 +- .../screens/wallets/send/SendAmountScreen.kt | 2 +- .../wallets/send/SendRecipientScreen.kt | 4 +- .../ui/settings/BlocktankRegtestScreen.kt | 14 +++--- .../to/bitkit/ui/settings/SettingsScreen.kt | 2 +- .../settings/advanced/AddressViewerScreen.kt | 2 +- .../settings/advanced/ElectrumConfigScreen.kt | 4 +- .../advanced/ElectrumConfigViewModel.kt | 4 +- .../ui/settings/advanced/RgsServerScreen.kt | 4 +- .../settings/lightning/ChannelDetailScreen.kt | 2 +- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 46 ++++++++--------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 50 +++++++++---------- .../to/bitkit/viewmodels/TransferViewModel.kt | 8 +-- .../to/bitkit/viewmodels/WalletViewModel.kt | 6 +-- 28 files changed, 107 insertions(+), 107 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index b9688108e..968695d49 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -2,14 +2,11 @@ package to.bitkit.models import androidx.compose.runtime.Stable -@Stable -enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } - @Stable data class Toast( val type: ToastType, val title: String, - val description: String? = null, + val body: String? = null, val autoHide: Boolean, val visibilityTime: Long = VISIBILITY_TIME_DEFAULT, val testTag: String? = null, @@ -18,3 +15,6 @@ data class Toast( const val VISIBILITY_TIME_DEFAULT = 3000L } } + +@Stable +enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index e53ddfcde..cb1476ecf 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -375,7 +375,7 @@ class BackupRepo @Inject constructor( scope.launch { toaster.error( title = context.getString(R.string.settings__backup__failed_title), - description = context.getString(R.string.settings__backup__failed_message).formatPlural( + body = context.getString(R.string.settings__backup__failed_message).formatPlural( mapOf("interval" to (BACKUP_CHECK_INTERVAL / MINUTE_IN_MS)) ), ) diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index 1456fcd11..68db7f2c2 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -94,7 +94,7 @@ class CurrencyRepo @Inject constructor( if (isStale) { toaster.error( title = "Rates currently unavailable", - description = "An error has occurred. Please try again later." + body = "An error has occurred. Please try again later." ) } } diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 762bb8ee8..c65396113 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -626,11 +626,11 @@ private fun RootNavHost( onBackClick = { navController.popBackStack() }, onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, toastException = { appViewModel.toast(it) }, - toast = { title, description -> + toast = { title, body -> appViewModel.toast( type = ToastType.ERROR, title = title, - description = description + body = body ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 55096ff34..8c394873b 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -94,7 +94,7 @@ fun NodeInfoScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), - description = text + body = text ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index ae21265d2..83264d790 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -33,7 +33,7 @@ fun IsOnlineTracker( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.other__connection_back_title), - description = context.getString(R.string.other__connection_back_msg), + body = context.getString(R.string.other__connection_back_msg), ) } @@ -41,7 +41,7 @@ fun IsOnlineTracker( app.toast( type = ToastType.WARNING, title = context.getString(R.string.other__connection_issue), - description = context.getString(R.string.other__connection_issue_explain), + body = context.getString(R.string.other__connection_issue_explain), ) } diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index 4632001bc..bdf2cf38c 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -226,9 +226,9 @@ fun ToastView( text = toast.title, color = tintColor, ) - toast.description?.let { description -> + toast.body?.let { body -> Caption( - text = description, + text = body, color = Colors.White ) } @@ -325,7 +325,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.WARNING, title = "You're still offline", - description = "Check your connection to keep using Bitkit.", + body = "Check your connection to keep using Bitkit.", autoHide = true, ), onDismiss = {}, @@ -334,7 +334,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.LIGHTNING, title = "Instant Payments Ready", - description = "You can now pay anyone, anywhere, instantly.", + body = "You can now pay anyone, anywhere, instantly.", autoHide = true, ), onDismiss = {}, @@ -343,7 +343,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.SUCCESS, title = "You're Back Online!", - description = "Successfully reconnected to the Internet.", + body = "Successfully reconnected to the Internet.", autoHide = true, ), onDismiss = {}, @@ -352,7 +352,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.INFO, title = "General Message", - description = "Used for neutral content to inform the user.", + body = "Used for neutral content to inform the user.", autoHide = false, ), onDismiss = {}, @@ -361,7 +361,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.ERROR, title = "Error Toast", - description = "This is a toast message.", + body = "This is a toast message.", autoHide = true, ), onDismiss = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 7aad87e56..264ee2353 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -152,7 +152,7 @@ fun QrScanningScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), - description = context.getString(R.string.other__qr_error_text), + body = context.getString(R.string.other__qr_error_text), ) } } @@ -258,7 +258,7 @@ private fun handlePaste( app.toast( type = ToastType.WARNING, title = context.getString(R.string.wallet__send_clipboard_empty_title), - description = context.getString(R.string.wallet__send_clipboard_empty_text), + body = context.getString(R.string.wallet__send_clipboard_empty_text), ) } setScanResult(clipboard) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 1b2269d82..320066d63 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -73,7 +73,7 @@ fun SavingsProgressScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.lightning__close_error), - description = context.getString(R.string.lightning__close_error_msg), + body = context.getString(R.string.lightning__close_error_msg), ) onTransferUnavailable() } else { @@ -84,7 +84,7 @@ fun SavingsProgressScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.lightning__close_error), - description = context.getString(R.string.lightning__close_error_msg), + body = context.getString(R.string.lightning__close_error_msg), ) onTransferUnavailable() }, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index 1511ef44a..5d76b4d1f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -93,7 +93,7 @@ fun SpendingAdvancedScreen( app.toast( type = ToastType.ERROR, title = effect.title, - description = effect.description, + body = effect.body, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt index d313421c2..7e015b9cc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt @@ -72,7 +72,7 @@ fun SpendingAmountScreen( viewModel.transferEffects.collect { effect -> when (effect) { TransferEffect.OnOrderCreated -> onOrderCreated() - is TransferEffect.ToastError -> toast(effect.title, effect.description) + is TransferEffect.ToastError -> toast(effect.title, effect.body) is TransferEffect.ToastException -> toastException(effect.e) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 63859b1da..71134034c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -97,7 +97,7 @@ class ExternalNodeViewModel @Inject constructor( viewModelScope.launch { toaster.error( title = context.getString(R.string.lightning__spending_amount__error_max__title), - description = context.getString(R.string.lightning__spending_amount__error_max__description) + body = context.getString(R.string.lightning__spending_amount__error_max__description) .replace("{amount}", maxAmount.formatToModernDisplay()), ) } @@ -183,7 +183,7 @@ class ExternalNodeViewModel @Inject constructor( Logger.warn("Error opening channel with peer: '${_uiState.value.peer}': '$error'") toaster.error( title = context.getString(R.string.lightning__error_channel_purchase), - description = context.getString(R.string.lightning__error_channel_setup_msg) + body = context.getString(R.string.lightning__error_channel_setup_msg) .replace("{raw}", error), ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 9a38a8edb..19b5e6576 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -84,7 +84,7 @@ class LnurlChannelViewModel @Inject constructor( suspend fun errorToast(error: Throwable) { toaster.error( title = context.getString(R.string.other__lnurl_channel_error), - description = error.message ?: "Unknown error", + body = error.message ?: "Unknown error", ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index b0ca70f3e..9ef1f4663 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -230,7 +230,7 @@ fun ActivityDetailScreen( app.toast( type = ToastType.SUCCESS, title = copyToastTitle, - description = text.ellipsisMiddle(40) + body = text.ellipsisMiddle(40) ) }, feeRates = feeRates, @@ -253,7 +253,7 @@ fun ActivityDetailScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.wallet__boost_success_title), - description = context.getString(R.string.wallet__boost_success_msg), + body = context.getString(R.string.wallet__boost_success_msg), testTag = "BoostSuccessToast" ) listViewModel.resync() @@ -263,7 +263,7 @@ fun ActivityDetailScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__boost_error_title), - description = context.getString(R.string.wallet__boost_error_msg), + body = context.getString(R.string.wallet__boost_error_msg), testTag = "BoostFailureToast" ) detailViewModel.onDismissBoostSheet() @@ -272,14 +272,14 @@ fun ActivityDetailScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__send_fee_error), - description = context.getString(R.string.wallet__send_fee_error_max) + body = context.getString(R.string.wallet__send_fee_error_max) ) }, onMinFee = { app.toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__send_fee_error), - description = context.getString(R.string.wallet__send_fee_error_min) + body = context.getString(R.string.wallet__send_fee_error_min) ) } ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 130a11fc2..7de2fbf5a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -169,7 +169,7 @@ fun ActivityExploreScreen( app.toast( type = ToastType.SUCCESS, title = toastMessage, - description = text.ellipsisMiddle(40), + body = text.ellipsisMiddle(40), ) }, onClickExplore = { txid -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 60a356589..a857fac93 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -111,7 +111,7 @@ fun SendAmountScreen( app?.toast( type = ToastType.INFO, title = context.getString(R.string.wallet__send_max_spending__title), - description = context.getString(R.string.wallet__send_max_spending__description) + body = context.getString(R.string.wallet__send_max_spending__description) ) } amountInputViewModel.setSats(maxSats, currencies) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index 3ff163ade..dcec61ea4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -142,7 +142,7 @@ fun SendRecipientScreen( app?.toast( type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), - description = context.getString(R.string.other__qr_error_text), + body = context.getString(R.string.other__qr_error_text), ) } } @@ -176,7 +176,7 @@ fun SendRecipientScreen( app?.toast( type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), - description = context.getString(R.string.other__camera_init_error) + body = context.getString(R.string.other__camera_init_error) .replace("{message}", it.message.orEmpty()) ) isCameraInitialized = false diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index 4965889e5..b0d3406d4 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -115,14 +115,14 @@ fun BlocktankRegtestScreen( app.toast( type = ToastType.SUCCESS, title = "Success", - description = "Deposit successful. TxID: $txId", + body = "Deposit successful. TxID: $txId", ) }.onFailure { Logger.error("Deposit failed", it) app.toast( type = ToastType.ERROR, title = "Failed to deposit", - description = it.message.orEmpty(), + body = it.message.orEmpty(), ) } @@ -161,14 +161,14 @@ fun BlocktankRegtestScreen( app.toast( type = ToastType.SUCCESS, title = "Success", - description = "Successfully mined $count blocks", + body = "Successfully mined $count blocks", ) }.onFailure { Logger.error("Mining failed", it) app.toast( type = ToastType.ERROR, title = "Failed to mine", - description = it.message.orEmpty(), + body = it.message.orEmpty(), ) } isMining = false @@ -213,14 +213,14 @@ fun BlocktankRegtestScreen( app.toast( type = ToastType.SUCCESS, title = "Success", - description = "Payment successful. ID: $paymentId", + body = "Payment successful. ID: $paymentId", ) }.onFailure { Logger.error("Payment failed", it) app.toast( type = ToastType.ERROR, title = "Failed to pay invoice from LND", - description = it.message.orEmpty(), + body = it.message.orEmpty(), ) } } @@ -280,7 +280,7 @@ fun BlocktankRegtestScreen( app.toast( type = ToastType.SUCCESS, title = "Success", - description = "Channel closed. Closing TxID: $closingTxId" + body = "Channel closed. Closing TxID: $closingTxId" ) }.onFailure { Logger.error("Channel close failed", it) diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index 1893dc944..e57e4e180 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -84,7 +84,7 @@ fun SettingsScreen( R.string.settings__dev_disabled_title } ), - description = context.getString( + body = context.getString( if (newValue) { R.string.settings__dev_enabled_message } else { diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index e65a58bd9..6754fea54 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -80,7 +80,7 @@ fun AddressViewerScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), - description = text, + body = text, ) } ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 1a46b44db..1a06e5556 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -77,7 +77,7 @@ fun ElectrumConfigScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.settings__es__server_updated_title), - description = context.getString(R.string.settings__es__server_updated_message) + body = context.getString(R.string.settings__es__server_updated_message) .replace("{host}", uiState.host) .replace("{port}", uiState.port), testTag = "ElectrumUpdatedToast", @@ -86,7 +86,7 @@ fun ElectrumConfigScreen( app.toast( type = ToastType.WARNING, title = context.getString(R.string.settings__es__server_error), - description = context.getString(R.string.settings__es__server_error_description), + body = context.getString(R.string.settings__es__server_error_description), testTag = "ElectrumErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index a7c25036d..70b456f10 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -248,7 +248,7 @@ class ElectrumConfigViewModel @Inject constructor( if (validationError != null) { toaster.warning( title = context.getString(R.string.settings__es__error_peer), - description = validationError, + body = validationError, ) } else { connectToServer() @@ -269,7 +269,7 @@ class ElectrumConfigViewModel @Inject constructor( if (validationError != null) { toaster.warning( title = context.getString(R.string.settings__es__error_peer), - description = validationError, + body = validationError, ) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 28d9d08d0..5072b5f15 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -70,14 +70,14 @@ fun RgsServerScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.settings__rgs__update_success_title), - description = context.getString(R.string.settings__rgs__update_success_description), + body = context.getString(R.string.settings__rgs__update_success_description), testTag = "RgsUpdatedToast", ) } else { app.toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__ldk_start_error_title), - description = result.exceptionOrNull()?.message ?: "Unknown error", + body = result.exceptionOrNull()?.message ?: "Unknown error", testTag = "RgsErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index fffa115db..2cd13a0fb 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -131,7 +131,7 @@ fun ChannelDetailScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), - description = text, + body = text, ) }, onOpenUrl = { txId -> diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 096b381f6..08029289a 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -24,7 +24,7 @@ class Toaster @Inject constructor( private suspend fun emit( type: ToastType, title: String, - description: String? = null, + body: String? = null, autoHide: Boolean = true, visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, testTag: String? = null, @@ -33,7 +33,7 @@ class Toaster @Inject constructor( Toast( type = type, title = title, - description = description, + body = body, autoHide = autoHide, visibilityTime = visibilityTime, testTag = testTag, @@ -44,18 +44,18 @@ class Toaster @Inject constructor( // region Success suspend fun success( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.SUCCESS, title, description, testTag = testTag) + ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) suspend fun success( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.SUCCESS, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) // endregion @@ -63,18 +63,18 @@ class Toaster @Inject constructor( // region Info suspend fun info( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.INFO, title, description, testTag = testTag) + ) = emit(ToastType.INFO, title, body, testTag = testTag) suspend fun info( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.INFO, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) // endregion @@ -82,18 +82,18 @@ class Toaster @Inject constructor( // region Lightning suspend fun lightning( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.LIGHTNING, title, description, testTag = testTag) + ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) suspend fun lightning( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.LIGHTNING, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) // endregion @@ -101,18 +101,18 @@ class Toaster @Inject constructor( // region Warning suspend fun warning( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.WARNING, title, description, testTag = testTag) + ) = emit(ToastType.WARNING, title, body, testTag = testTag) suspend fun warning( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.WARNING, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) // endregion @@ -120,25 +120,25 @@ class Toaster @Inject constructor( // region Error suspend fun error( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.ERROR, title, description, testTag = testTag) + ) = emit(ToastType.ERROR, title, body, testTag = testTag) suspend fun error( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.ERROR, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, title = context.getString(R.string.common__error), - description = throwable.message ?: context.getString(R.string.common__error_body), + body = throwable.message ?: context.getString(R.string.common__error_body), ) // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index d148c609b..8324db0c3 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -233,7 +233,7 @@ class AppViewModel @Inject constructor( init { viewModelScope.launch { toaster.events.collect { - toast(it.type, it.title, it.description, it.autoHide, it.visibilityTime) + toast(it.type, it.title, it.body, it.autoHide, it.visibilityTime) } } viewModelScope.launch { @@ -453,7 +453,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = "Migration Warning", - description = "Migration completed but node restart failed. Please restart the app." + body = "Migration completed but node restart failed. Please restart the app." ) } @@ -537,7 +537,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.LIGHTNING, title = context.getString(R.string.lightning__channel_opened_title), - description = context.getString(R.string.lightning__channel_opened_msg), + body = context.getString(R.string.lightning__channel_opened_msg), testTag = "SpendingBalanceReadyToast", ) } @@ -547,7 +547,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.wallet__toast_transaction_removed_title), - description = context.getString(R.string.wallet__toast_transaction_removed_description), + body = context.getString(R.string.wallet__toast_transaction_removed_description), testTag = "TransactionRemovedToast", ) } @@ -562,7 +562,7 @@ class AppViewModel @Inject constructor( private fun notifyTransactionUnconfirmed() = toast( type = ToastType.WARNING, title = context.getString(R.string.wallet__toast_transaction_unconfirmed_title), - description = context.getString(R.string.wallet__toast_transaction_unconfirmed_description), + body = context.getString(R.string.wallet__toast_transaction_unconfirmed_description), testTag = "TransactionUnconfirmedToast", ) @@ -574,7 +574,7 @@ class AppViewModel @Inject constructor( true -> R.string.wallet__toast_received_transaction_replaced_title else -> R.string.wallet__toast_transaction_replaced_title }.let { context.getString(it) }, - description = when (isReceive) { + body = when (isReceive) { true -> R.string.wallet__toast_received_transaction_replaced_description else -> R.string.wallet__toast_transaction_replaced_description }.let { context.getString(it) }, @@ -588,7 +588,7 @@ class AppViewModel @Inject constructor( private fun notifyPaymentFailed() = toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__toast_payment_failed_title), - description = context.getString(R.string.wallet__toast_payment_failed_description), + body = context.getString(R.string.wallet__toast_payment_failed_description), testTag = "PaymentFailedToast", ) @@ -779,7 +779,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__lnurl_pay__error_min__title), - description = context.getString(R.string.wallet__lnurl_pay__error_min__description) + body = context.getString(R.string.wallet__lnurl_pay__error_min__description) .replace("{amount}", minSendable.toString()), testTag = "LnurlPayAmountTooLowToast", ) @@ -834,7 +834,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.wallet__send_clipboard_empty_title), - description = context.getString(R.string.wallet__send_clipboard_empty_text), + body = context.getString(R.string.wallet__send_clipboard_empty_text), ) return } @@ -879,7 +879,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.other__scan_err_decoding), - description = context.getString(R.string.other__scan_err_interpret_title), + body = context.getString(R.string.other__scan_err_interpret_title), ) } } @@ -896,7 +896,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.other__scan_err_decoding), - description = context.getString(R.string.other__scan__error__expired), + body = context.getString(R.string.other__scan__error__expired), ) Logger.debug( @@ -965,7 +965,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.other__scan_err_decoding), - description = context.getString(R.string.other__scan__error__expired), + body = context.getString(R.string.other__scan__error__expired), ) return } @@ -977,7 +977,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__error_insufficient_funds_title), - description = context.getString(R.string.wallet__error_insufficient_funds_msg) + body = context.getString(R.string.wallet__error_insufficient_funds_msg) ) return } @@ -1021,7 +1021,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_pay_error), - description = context.getString(R.string.other__lnurl_pay_error_no_capacity), + body = context.getString(R.string.other__lnurl_pay_error_no_capacity), ) return } @@ -1069,7 +1069,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_withdr_error), - description = context.getString(R.string.other__lnurl_withdr_error_minmax) + body = context.getString(R.string.other__lnurl_withdr_error_minmax) ) return } @@ -1118,14 +1118,14 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_auth_error), - description = context.getString(R.string.other__lnurl_auth_error_msg) + body = context.getString(R.string.other__lnurl_auth_error_msg) .replace("{raw}", it.message?.takeIf { m -> m.isNotBlank() } ?: it.javaClass.simpleName), ) }.onSuccess { toast( type = ToastType.SUCCESS, title = context.getString(R.string.other__lnurl_auth_success_title), - description = when (domain.isNotBlank()) { + body = when (domain.isNotBlank()) { true -> context.getString(R.string.other__lnurl_auth_success_msg_domain) .replace("{domain}", domain) @@ -1157,7 +1157,7 @@ class AppViewModel @Inject constructor( // toast( // type = ToastType.WARNING, // title = context.getString(R.string.other__qr_error_network_header), - // description = context.getString(R.string.other__qr_error_network_text) + // body = context.getString(R.string.other__qr_error_network_text) // .replace("{selectedNetwork}", appNetwork.name) // .replace("{dataNetwork}", network.name), // ) @@ -1362,7 +1362,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__error_sending_title), - description = e.message ?: context.getString(R.string.common__error_body) + body = e.message ?: context.getString(R.string.common__error_body) ) hideSheet() } @@ -1454,7 +1454,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.SUCCESS, title = context.getString(R.string.other__lnurl_withdr_success_title), - description = context.getString(R.string.other__lnurl_withdr_success_msg), + body = context.getString(R.string.other__lnurl_withdr_success_msg), ) hideSheet() _sendUiState.update { it.copy(isLoading = false) } @@ -1797,7 +1797,7 @@ class AppViewModel @Inject constructor( fun toast( type: ToastType, title: String, - description: String? = null, + body: String? = null, autoHide: Boolean = true, visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, testTag: String? = null, @@ -1806,7 +1806,7 @@ class AppViewModel @Inject constructor( Toast( type = type, title = title, - description = description, + body = body, autoHide = autoHide, visibilityTime = visibilityTime, testTag = testTag, @@ -1818,7 +1818,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.common__error), - description = error.message ?: context.getString(R.string.common__error_body) + body = error.message ?: context.getString(R.string.common__error_body) ) } @@ -1826,7 +1826,7 @@ class AppViewModel @Inject constructor( toast( type = toast.type, title = toast.title, - description = toast.description, + body = toast.body, autoHide = toast.autoHide, visibilityTime = toast.visibilityTime ) @@ -1871,7 +1871,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.SUCCESS, title = context.getString(R.string.security__wiped_title), - description = context.getString(R.string.security__wiped_message), + body = context.getString(R.string.security__wiped_message), ) delay(250) // small delay for UI feedback mainScreenEffect(MainScreenEffect.WipeWallet) diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 9c0e72d41..215821433 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -93,7 +93,7 @@ class TransferViewModel @Inject constructor( setTransferEffect( TransferEffect.ToastError( title = context.getString(R.string.lightning__spending_amount__error_max__title), - description = context.getString( + body = context.getString( R.string.lightning__spending_amount__error_max__description_zero ), ) @@ -481,10 +481,10 @@ class TransferViewModel @Inject constructor( Logger.info("Force close initiated successfully for all channels", context = TAG) val initMsg = context.getString(R.string.lightning__force_init_msg) val skippedMsg = context.getString(R.string.lightning__force_channels_skipped) - val description = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg + val bodyText = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg toaster.lightning( title = context.getString(R.string.lightning__force_init_title), - description = description, + body = bodyText, ) } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) @@ -566,6 +566,6 @@ data class TransferValues( sealed interface TransferEffect { data object OnOrderCreated : TransferEffect data class ToastException(val e: Throwable) : TransferEffect - data class ToastError(val title: String, val description: String) : TransferEffect + data class ToastError(val title: String, val body: String) : TransferEffect } // endregion diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 04436af6e..ffa54790d 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -128,7 +128,7 @@ class WalletViewModel @Inject constructor( migrationService.setShowingMigrationLoading(false) toaster.error( title = "Migration Failed", - description = "Please restore your wallet manually using your recovery phrase" + body = "Please restore your wallet manually using your recovery phrase" ) } } @@ -305,7 +305,7 @@ class WalletViewModel @Inject constructor( .onFailure { toaster.error( title = context.getString(R.string.common__error), - description = it.message ?: context.getString(R.string.common__error_body) + body = it.message ?: context.getString(R.string.common__error_body) ) } } @@ -315,7 +315,7 @@ class WalletViewModel @Inject constructor( walletRepo.updateBip21Invoice(amountSats).onFailure { error -> toaster.error( title = context.getString(R.string.wallet__error_invoice_update), - description = error.message ?: context.getString(R.string.common__error_body) + body = error.message ?: context.getString(R.string.common__error_body) ) } } From 26092e3ebe1ffa7abd468756abfda87f5390ac62 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:36:13 +0100 Subject: [PATCH 03/21] refactor: use Duration for toast visibility Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 6 ++++-- .../java/to/bitkit/ui/shared/toast/ToastQueueManager.kt | 7 ++++--- app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt | 5 +++-- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 9 +++++---- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index 968695d49..ca9c7eb1e 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -1,6 +1,8 @@ package to.bitkit.models import androidx.compose.runtime.Stable +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds @Stable data class Toast( @@ -8,11 +10,11 @@ data class Toast( val title: String, val body: String? = null, val autoHide: Boolean, - val visibilityTime: Long = VISIBILITY_TIME_DEFAULT, + val duration: Duration = DURATION_DEFAULT, val testTag: String? = null, ) { companion object { - const val VISIBILITY_TIME_DEFAULT = 3000L + val DURATION_DEFAULT: Duration = 3.seconds } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt index 94af44aa3..f8beb6b29 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.models.Toast +import kotlin.time.Duration private const val MAX_QUEUE_SIZE = 5 @@ -81,7 +82,7 @@ class ToastQueueManager(private val scope: CoroutineScope) { if (isPaused && toast != null) { isPaused = false if (toast.autoHide) { - startTimer(toast.visibilityTime) + startTimer(toast.duration) } } } @@ -108,11 +109,11 @@ class ToastQueueManager(private val scope: CoroutineScope) { // Start auto-hide timer if enabled if (nextToast.autoHide) { - startTimer(nextToast.visibilityTime) + startTimer(nextToast.duration) } } - private fun startTimer(duration: Long) { + private fun startTimer(duration: Duration) { cancelTimer() timerJob = scope.launch { delay(duration) diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 08029289a..16fe33773 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -11,6 +11,7 @@ import to.bitkit.models.Toast import to.bitkit.models.ToastType import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration @Suppress("TooManyFunctions") @Singleton @@ -26,7 +27,7 @@ class Toaster @Inject constructor( title: String, body: String? = null, autoHide: Boolean = true, - visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, + duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) { _events.emit( @@ -35,7 +36,7 @@ class Toaster @Inject constructor( title = title, body = body, autoHide = autoHide, - visibilityTime = visibilityTime, + duration = duration, testTag = testTag, ) ) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 8324db0c3..b5acc7735 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -46,6 +46,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout +import kotlin.time.Duration import org.lightningdevkit.ldknode.ChannelDataMigration import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId @@ -233,7 +234,7 @@ class AppViewModel @Inject constructor( init { viewModelScope.launch { toaster.events.collect { - toast(it.type, it.title, it.body, it.autoHide, it.visibilityTime) + toast(it.type, it.title, it.body, it.autoHide, it.duration) } } viewModelScope.launch { @@ -1799,7 +1800,7 @@ class AppViewModel @Inject constructor( title: String, body: String? = null, autoHide: Boolean = true, - visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, + duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) { toastManager.enqueue( @@ -1808,7 +1809,7 @@ class AppViewModel @Inject constructor( title = title, body = body, autoHide = autoHide, - visibilityTime = visibilityTime, + duration = duration, testTag = testTag, ) ) @@ -1828,7 +1829,7 @@ class AppViewModel @Inject constructor( title = toast.title, body = toast.body, autoHide = toast.autoHide, - visibilityTime = toast.visibilityTime + duration = toast.duration ) } From e3679ad6ad4ad81dd8245c548da2fbf6f04e0cc9 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:41:44 +0100 Subject: [PATCH 04/21] refactor: remove Context from Toaster Co-Authored-By: Claude Opus 4.5 --- .../recovery/RecoveryMnemonicViewModel.kt | 2 +- .../ui/screens/recovery/RecoveryViewModel.kt | 15 +++- .../external/ExternalNodeViewModel.kt | 12 ++- .../external/LnurlChannelViewModel.kt | 4 +- .../screens/wallets/send/SendFeeViewModel.kt | 10 ++- .../backups/BackupNavSheetViewModel.kt | 10 ++- .../LightningConnectionsViewModel.kt | 12 +-- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 78 +------------------ .../bitkit/viewmodels/DevSettingsViewModel.kt | 4 +- .../to/bitkit/viewmodels/TransferViewModel.kt | 12 +-- .../to/bitkit/viewmodels/WalletViewModel.kt | 5 +- 11 files changed, 61 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index a9785d623..48fcd3816 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -42,7 +42,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - toaster.error(R.string.security__mnemonic_load_error) + toaster.error(context.getString(R.string.security__mnemonic_load_error)) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index 98061ef81..d229d64d3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -73,7 +73,10 @@ class RecoveryViewModel @Inject constructor( isExportingLogs = false, ) } - toaster.error(R.string.common__error, R.string.other__logs_export_error) + toaster.error( + context.getString(R.string.common__error), + context.getString(R.string.other__logs_export_error), + ) } ) } @@ -94,7 +97,10 @@ class RecoveryViewModel @Inject constructor( }.onFailure { fallbackError -> Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { - toaster.error(R.string.common__error, R.string.settings__support__link_error) + toaster.error( + context.getString(R.string.common__error), + context.getString(R.string.settings__support__link_error), + ) } } } @@ -113,7 +119,10 @@ class RecoveryViewModel @Inject constructor( walletRepo.wipeWallet().onFailure { error -> toaster.error(error) }.onSuccess { - toaster.success(R.string.security__wiped_title, R.string.security__wiped_message) + toaster.success( + context.getString(R.string.security__wiped_title), + context.getString(R.string.security__wiped_message), + ) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 71134034c..ed1fcea4e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -73,7 +73,10 @@ class ExternalNodeViewModel @Inject constructor( _uiState.update { it.copy(peer = peer) } setEffect(SideEffect.ConnectionSuccess) } else { - toaster.error(R.string.lightning__error_add_title, R.string.lightning__error_add) + toaster.error( + context.getString(R.string.lightning__error_add_title), + context.getString(R.string.lightning__error_add), + ) } } } @@ -85,7 +88,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - toaster.error(R.string.lightning__error_add_uri) + toaster.error(context.getString(R.string.lightning__error_add_uri)) } } } @@ -129,7 +132,10 @@ class ExternalNodeViewModel @Inject constructor( suspend fun validateCustomFeeRate(): Boolean { val feeRate = _uiState.value.customFeeRate ?: 0u if (feeRate == 0u) { - toaster.info(R.string.wallet__min_possible_fee_rate, R.string.wallet__min_possible_fee_rate_msg) + toaster.info( + context.getString(R.string.wallet__min_possible_fee_rate), + context.getString(R.string.wallet__min_possible_fee_rate_msg), + ) return false } return true diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 19b5e6576..f60d580fb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -69,8 +69,8 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { toaster.success( - R.string.other__lnurl_channel_success_title, - R.string.other__lnurl_channel_success_msg_no_peer, + context.getString(R.string.other__lnurl_channel_success_title), + context.getString(R.string.other__lnurl_channel_success_msg_no_peer), ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index 8e12bda98..417a8eef9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -105,12 +105,18 @@ class SendFeeViewModel @Inject constructor( // TODO update to use minimum instead of slow when using mempool api val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { - toaster.info(R.string.wallet__min_possible_fee_rate, R.string.wallet__min_possible_fee_rate_msg) + toaster.info( + context.getString(R.string.wallet__min_possible_fee_rate), + context.getString(R.string.wallet__min_possible_fee_rate_msg), + ) return false } if (satsPerVByte > maxSatsPerVByte) { - toaster.info(R.string.wallet__max_possible_fee_rate, R.string.wallet__max_possible_fee_rate_msg) + toaster.info( + context.getString(R.string.wallet__max_possible_fee_rate), + context.getString(R.string.wallet__max_possible_fee_rate_msg), + ) return false } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 385d8dd30..8bb0947f3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -86,7 +86,10 @@ class BackupNavSheetViewModel @Inject constructor( } }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) - toaster.warning(R.string.security__mnemonic_error, R.string.security__mnemonic_error_description) + toaster.warning( + context.getString(R.string.security__mnemonic_error), + context.getString(R.string.security__mnemonic_error_description), + ) } } @@ -153,7 +156,10 @@ class BackupNavSheetViewModel @Inject constructor( fun onMnemonicCopied() { viewModelScope.launch { - toaster.success(R.string.common__copied, R.string.security__mnemonic_copied) + toaster.success( + context.getString(R.string.common__copied), + context.getString(R.string.security__mnemonic_copied), + ) } } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index a1324dd09..0126fbf6b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -340,8 +340,8 @@ class LightningConnectionsViewModel @Inject constructor( .onSuccess { uri -> onReady(uri) } .onFailure { toaster.warning( - R.string.lightning__error_logs, - R.string.lightning__error_logs_description, + context.getString(R.string.lightning__error_logs), + context.getString(R.string.lightning__error_logs_description), ) } } @@ -454,8 +454,8 @@ class LightningConnectionsViewModel @Inject constructor( walletRepo.syncNodeAndWallet() toaster.success( - R.string.lightning__close_success_title, - R.string.lightning__close_success_msg, + context.getString(R.string.lightning__close_success_title), + context.getString(R.string.lightning__close_success_msg), ) _closeConnectionUiState.update { @@ -469,8 +469,8 @@ class LightningConnectionsViewModel @Inject constructor( Logger.error("Failed to close channel", e = error, context = TAG) toaster.warning( - R.string.lightning__close_error, - R.string.lightning__close_error_msg, + context.getString(R.string.lightning__close_error), + context.getString(R.string.lightning__close_error_msg), ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 16fe33773..62db3dce7 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -1,23 +1,16 @@ package to.bitkit.ui.shared.toast -import android.content.Context -import androidx.annotation.StringRes -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.models.ToastType import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration -@Suppress("TooManyFunctions") @Singleton -class Toaster @Inject constructor( - @ApplicationContext private val context: Context, -) { +class Toaster @Inject constructor() { private val _events = MutableSharedFlow(extraBufferCapacity = 1) val events: SharedFlow = _events.asSharedFlow() @@ -42,104 +35,39 @@ class Toaster @Inject constructor( ) } - // region Success suspend fun success( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) - suspend fun success( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.SUCCESS, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - // endregion - - // region Info suspend fun info( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.INFO, title, body, testTag = testTag) - suspend fun info( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.INFO, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - // endregion - - // region Lightning suspend fun lightning( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) - suspend fun lightning( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.LIGHTNING, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - // endregion - - // region Warning suspend fun warning( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.WARNING, title, body, testTag = testTag) - suspend fun warning( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.WARNING, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - // endregion - - // region Error suspend fun error( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.ERROR, title, body, testTag = testTag) - suspend fun error( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.ERROR, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, - title = context.getString(R.string.common__error), - body = throwable.message ?: context.getString(R.string.common__error_body), + title = "Error", + body = throwable.message ?: "An unknown error occurred", ) - // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index 11dae996b..c5e0a4ad2 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -100,8 +100,8 @@ class DevSettingsViewModel @Inject constructor( .onSuccess { uri -> onReady(uri) } .onFailure { toaster.warning( - R.string.lightning__error_logs, - R.string.lightning__error_logs_description, + context.getString(R.string.lightning__error_logs), + context.getString(R.string.lightning__error_logs_description), ) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 215821433..02af58076 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -459,8 +459,8 @@ class TransferViewModel @Inject constructor( channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) toaster.error( - R.string.lightning__force_failed_title, - R.string.lightning__force_failed_msg + context.getString(R.string.lightning__force_failed_title), + context.getString(R.string.lightning__force_failed_msg), ) return@runCatching } @@ -489,15 +489,15 @@ class TransferViewModel @Inject constructor( } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) toaster.error( - R.string.lightning__force_failed_title, - R.string.lightning__force_failed_msg + context.getString(R.string.lightning__force_failed_title), + context.getString(R.string.lightning__force_failed_msg), ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) toaster.error( - R.string.lightning__force_failed_title, - R.string.lightning__force_failed_msg + context.getString(R.string.lightning__force_failed_title), + context.getString(R.string.lightning__force_failed_msg), ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index ffa54790d..9fc2a6d38 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -300,7 +300,10 @@ class WalletViewModel @Inject constructor( viewModelScope.launch { lightningRepo.disconnectPeer(peer) .onSuccess { - toaster.info(R.string.common__success, R.string.wallet__peer_disconnected) + toaster.info( + context.getString(R.string.common__success), + context.getString(R.string.wallet__peer_disconnected), + ) } .onFailure { toaster.error( From 32ad8c590e51d31cbbd27ca143ddd88712559435 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:43:59 +0100 Subject: [PATCH 05/21] refactor: rename ToastQueueManager to ToastQueue Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/di/ViewModelModule.kt | 6 +++--- .../{ToastQueueManager.kt => ToastQueue.kt} | 8 ++++---- .../java/to/bitkit/viewmodels/AppViewModel.kt | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) rename app/src/main/java/to/bitkit/ui/shared/toast/{ToastQueueManager.kt => ToastQueue.kt} (92%) diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index d0f531515..f4517100d 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -6,7 +6,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope -import to.bitkit.ui.shared.toast.ToastQueueManager +import to.bitkit.ui.shared.toast.ToastQueue import javax.inject.Singleton @Module @@ -19,7 +19,7 @@ object ViewModelModule { } @Provides - fun provideToastManagerProvider(): (CoroutineScope) -> ToastQueueManager { - return ::ToastQueueManager + fun provideToastQueueProvider(): (CoroutineScope) -> ToastQueue { + return ::ToastQueue } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt similarity index 92% rename from app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt rename to app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt index f8beb6b29..673a413ed 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt @@ -14,10 +14,10 @@ import kotlin.time.Duration private const val MAX_QUEUE_SIZE = 5 /** - * Manages a queue of toasts to display sequentially. + * A queue for displaying toasts sequentially. * - * This ensures that toasts are shown one at a time without premature cancellation. - * When a toast is displayed, it waits for its full visibility duration before + * Ensures toasts are shown one at a time without premature cancellation. + * When a toast is displayed, it waits for its full duration before * showing the next toast in the queue. * * Features: @@ -27,7 +27,7 @@ private const val MAX_QUEUE_SIZE = 5 * - Auto-advance to next toast on completion * - Max queue size with FIFO overflow handling */ -class ToastQueueManager(private val scope: CoroutineScope) { +class ToastQueue(private val scope: CoroutineScope) { // Public state exposed to UI private val _currentToast = MutableStateFlow(null) val currentToast: StateFlow = _currentToast.asStateFlow() diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index b5acc7735..e50ac8d26 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -104,7 +104,7 @@ import to.bitkit.services.AppUpdaterService import to.bitkit.services.MigrationService import to.bitkit.ui.Routes import to.bitkit.ui.components.Sheet -import to.bitkit.ui.shared.toast.ToastQueueManager +import to.bitkit.ui.shared.toast.ToastQueue import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.TRANSITION_SCREEN_MS @@ -127,7 +127,7 @@ import kotlin.time.ExperimentalTime class AppViewModel @Inject constructor( connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, - toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager, + toastQueueProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueue, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, toaster: Toaster, @ApplicationContext private val context: Context, @@ -1792,8 +1792,8 @@ class AppViewModel @Inject constructor( // endregion // region Toasts - private val toastManager = toastManagerProvider(viewModelScope) - val currentToast: StateFlow = toastManager.currentToast + private val toastQueue = toastQueueProvider(viewModelScope) + val currentToast: StateFlow = toastQueue.currentToast fun toast( type: ToastType, @@ -1803,7 +1803,7 @@ class AppViewModel @Inject constructor( duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) { - toastManager.enqueue( + toastQueue.enqueue( Toast( type = type, title = title, @@ -1833,11 +1833,11 @@ class AppViewModel @Inject constructor( ) } - fun hideToast() = toastManager.dismissCurrentToast() + fun hideToast() = toastQueue.dismissCurrentToast() - fun pauseToast() = toastManager.pauseCurrentToast() + fun pauseToast() = toastQueue.pauseCurrentToast() - fun resumeToast() = toastManager.resumeCurrentToast() + fun resumeToast() = toastQueue.resumeCurrentToast() // endregion // region security From 301e073215667525c8176019474cf5eb6e72bcbe Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:45:37 +0100 Subject: [PATCH 06/21] refactor: make ToastQueue inherit BaseCoroutineScope Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/di/ViewModelModule.kt | 4 ++-- app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt | 7 ++++--- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index f4517100d..a7d6da319 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -5,7 +5,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineDispatcher import to.bitkit.ui.shared.toast.ToastQueue import javax.inject.Singleton @@ -19,7 +19,7 @@ object ViewModelModule { } @Provides - fun provideToastQueueProvider(): (CoroutineScope) -> ToastQueue { + fun provideToastQueueProvider(): (CoroutineDispatcher) -> ToastQueue { return ::ToastQueue } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt index 673a413ed..499721a52 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt @@ -1,6 +1,6 @@ package to.bitkit.ui.shared.toast -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import to.bitkit.async.BaseCoroutineScope import to.bitkit.models.Toast import kotlin.time.Duration @@ -27,7 +28,7 @@ private const val MAX_QUEUE_SIZE = 5 * - Auto-advance to next toast on completion * - Max queue size with FIFO overflow handling */ -class ToastQueue(private val scope: CoroutineScope) { +class ToastQueue(dispatcher: CoroutineDispatcher) : BaseCoroutineScope(dispatcher) { // Public state exposed to UI private val _currentToast = MutableStateFlow(null) val currentToast: StateFlow = _currentToast.asStateFlow() @@ -115,7 +116,7 @@ class ToastQueue(private val scope: CoroutineScope) { private fun startTimer(duration: Duration) { cancelTimer() - timerJob = scope.launch { + timerJob = launch { delay(duration) if (!isPaused) { _currentToast.value = null diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index e50ac8d26..4d672f47c 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -28,6 +28,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -127,7 +128,7 @@ import kotlin.time.ExperimentalTime class AppViewModel @Inject constructor( connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, - toastQueueProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueue, + toastQueueProvider: @JvmSuppressWildcards (CoroutineDispatcher) -> ToastQueue, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, toaster: Toaster, @ApplicationContext private val context: Context, @@ -1792,7 +1793,7 @@ class AppViewModel @Inject constructor( // endregion // region Toasts - private val toastQueue = toastQueueProvider(viewModelScope) + private val toastQueue = toastQueueProvider(Dispatchers.Main.immediate) val currentToast: StateFlow = toastQueue.currentToast fun toast( From 0e3e7f5cc6e9d7223e8f08ea72da01c10a5af866 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:46:01 +0100 Subject: [PATCH 07/21] docs: add *Manager anti-pattern rule Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index e7a0b0da0..20c73500c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -210,6 +210,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - PREFER to use one-liners with `run {}` when applicable, e.g. `override fun someCall(value: String) = run { this.value = value }` - ALWAYS add imports instead of inline fully-qualified names - PREFER to place `@Suppress()` annotations at the narrowest possible scope +- NEVER use `*Manager` suffix for classes, PREFER narrow-scope constructs that do not tend to grow into unmaintainable god objects ### Architecture Guidelines From 96ec53c91000aadf21d2ef3c1a5021b4df69ea0b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:48:46 +0100 Subject: [PATCH 08/21] refactor: expose Toaster via CompositionLocal Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/ContentView.kt | 1 + app/src/main/java/to/bitkit/ui/Locals.kt | 5 +++++ app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index c65396113..9d9b77290 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -360,6 +360,7 @@ fun ContentView( LocalTransferViewModel provides transferViewModel, LocalSettingsViewModel provides settingsViewModel, LocalBackupsViewModel provides backupsViewModel, + LocalToaster provides appViewModel.toaster, LocalDrawerState provides drawerState, LocalBalances provides balance, LocalCurrencies provides currencies, diff --git a/app/src/main/java/to/bitkit/ui/Locals.kt b/app/src/main/java/to/bitkit/ui/Locals.kt index 2843e38c4..dd1f02f7d 100644 --- a/app/src/main/java/to/bitkit/ui/Locals.kt +++ b/app/src/main/java/to/bitkit/ui/Locals.kt @@ -14,6 +14,7 @@ import to.bitkit.viewmodels.CurrencyViewModel import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel +import to.bitkit.ui.shared.toast.Toaster // Locals val LocalBalances = compositionLocalOf { BalanceState() } @@ -29,6 +30,7 @@ val LocalActivityListViewModel = staticCompositionLocalOf { null } val LocalSettingsViewModel = staticCompositionLocalOf { null } val LocalBackupsViewModel = staticCompositionLocalOf { null } +val LocalToaster = staticCompositionLocalOf { null } val appViewModel: AppViewModel? @Composable get() = LocalAppViewModel.current @@ -56,3 +58,6 @@ val backupsViewModel: BackupsViewModel? val drawerState: DrawerState? @Composable get() = LocalDrawerState.current + +val toaster: Toaster? + @Composable get() = LocalToaster.current diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 4d672f47c..001563bc3 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -130,7 +130,7 @@ class AppViewModel @Inject constructor( healthRepo: HealthRepo, toastQueueProvider: @JvmSuppressWildcards (CoroutineDispatcher) -> ToastQueue, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, - toaster: Toaster, + val toaster: Toaster, @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val keychain: Keychain, From f644b0a54cc0b5b98f5c68f4526d6b06db613a42 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:50:57 +0100 Subject: [PATCH 09/21] refactor: use LocalToaster in composables Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/ui/components/IsOnlineTracker.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 83264d790..9e208724c 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -8,8 +8,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R -import to.bitkit.models.ToastType import to.bitkit.repositories.ConnectivityState +import to.bitkit.ui.toaster import to.bitkit.viewmodels.AppViewModel @Composable @@ -17,6 +17,7 @@ fun IsOnlineTracker( app: AppViewModel, ) { val context = LocalContext.current + val toaster = toaster ?: return val connectivityState by app.isOnline.collectAsStateWithLifecycle(initialValue = ConnectivityState.CONNECTED) val (isFirstEmission, setIsFirstEmission) = remember { mutableStateOf(true) } @@ -30,16 +31,14 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = context.getString(R.string.other__connection_back_title), body = context.getString(R.string.other__connection_back_msg), ) } ConnectivityState.DISCONNECTED -> { - app.toast( - type = ToastType.WARNING, + toaster.warning( title = context.getString(R.string.other__connection_issue), body = context.getString(R.string.other__connection_issue_explain), ) From 20be904ab23b23cf1205ed839b62f115f918bb0b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:52:27 +0100 Subject: [PATCH 10/21] refactor: rename ToastView to ToastContent Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/ui/MainActivity.kt | 4 +- .../java/to/bitkit/ui/components/ToastView.kt | 103 ++++++++---------- 2 files changed, 45 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index db4b23dee..52bd312a1 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -36,7 +36,7 @@ import to.bitkit.androidServices.LightningNodeService.Companion.CHANNEL_ID_NODE import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.ui.components.AuthCheckView import to.bitkit.ui.components.IsOnlineTracker -import to.bitkit.ui.components.ToastOverlay +import to.bitkit.ui.components.ToastHost import to.bitkit.ui.onboarding.CreateWalletWithPassphraseScreen import to.bitkit.ui.onboarding.IntroScreen import to.bitkit.ui.onboarding.OnboardingSlidesScreen @@ -173,7 +173,7 @@ class MainActivity : FragmentActivity() { } val currentToast by appViewModel.currentToast.collectAsStateWithLifecycle() - ToastOverlay( + ToastHost( toast = currentToast, hazeState = hazeState, onDismiss = { appViewModel.hideToast() }, diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index bdf2cf38c..c08b837d1 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -67,9 +67,45 @@ private const val TINT_ALPHA = 0.32f private const val SHADOW_ALPHA = 0.4f private const val ELEVATION_DP = 10 +@Composable +fun ToastHost( + toast: Toast?, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + hazeState: HazeState = rememberHazeState(blurEnabled = true), + onDragStart: () -> Unit = {}, + onDragEnd: () -> Unit = {}, +) { + Box( + contentAlignment = Alignment.TopCenter, + modifier = modifier.fillMaxSize(), + ) { + AnimatedContent( + targetState = toast, + transitionSpec = { + (fadeIn() + slideInVertically { -it }) + .togetherWith(fadeOut() + slideOutVertically { -it }) + .using(SizeTransform(clip = false)) + }, + contentAlignment = Alignment.TopCenter, + label = "toastAnimation", + ) { + if (it != null) { + ToastContent( + toast = it, + onDismiss = onDismiss, + hazeState = hazeState, + onDragStart = onDragStart, + onDragEnd = onDragEnd + ) + } + } + } +} + @OptIn(ExperimentalHazeMaterialsApi::class) @Composable -fun ToastView( +private fun ToastContent( toast: Toast, onDismiss: () -> Unit, modifier: Modifier = Modifier, @@ -261,67 +297,14 @@ fun ToastView( } } -@Composable -private fun ToastHost( - toast: Toast?, - hazeState: HazeState, - onDismiss: () -> Unit, - onDragStart: () -> Unit = {}, - onDragEnd: () -> Unit = {}, -) { - AnimatedContent( - targetState = toast, - transitionSpec = { - (fadeIn() + slideInVertically { -it }) - .togetherWith(fadeOut() + slideOutVertically { -it }) - .using(SizeTransform(clip = false)) - }, - contentAlignment = Alignment.TopCenter, - label = "toastAnimation", - ) { - if (it != null) { - ToastView( - toast = it, - onDismiss = onDismiss, - hazeState = hazeState, - onDragStart = onDragStart, - onDragEnd = onDragEnd - ) - } - } -} - -@Composable -fun ToastOverlay( - toast: Toast?, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, - hazeState: HazeState = rememberHazeState(blurEnabled = true), - onDragStart: () -> Unit = {}, - onDragEnd: () -> Unit = {}, -) { - Box( - contentAlignment = Alignment.TopCenter, - modifier = modifier.fillMaxSize(), - ) { - ToastHost( - toast = toast, - hazeState = hazeState, - onDismiss = onDismiss, - onDragStart = onDragStart, - onDragEnd = onDragEnd - ) - } -} - @Preview(showSystemUi = true) @Composable -private fun ToastViewPreview() { +private fun ToastContentPreview() { AppThemeSurface { ScreenColumn( verticalArrangement = Arrangement.spacedBy(16.dp), ) { - ToastView( + ToastContent( toast = Toast( type = ToastType.WARNING, title = "You're still offline", @@ -330,7 +313,7 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) - ToastView( + ToastContent( toast = Toast( type = ToastType.LIGHTNING, title = "Instant Payments Ready", @@ -339,7 +322,7 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) - ToastView( + ToastContent( toast = Toast( type = ToastType.SUCCESS, title = "You're Back Online!", @@ -348,7 +331,7 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) - ToastView( + ToastContent( toast = Toast( type = ToastType.INFO, title = "General Message", @@ -357,7 +340,7 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) - ToastView( + ToastContent( toast = Toast( type = ToastType.ERROR, title = "Error Toast", From 3a3b96df3f1ab1bfc795f571280632479bf81fd1 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:55:13 +0100 Subject: [PATCH 11/21] test: add ToastQueue behavior tests Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/Locals.kt | 2 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- .../bitkit/ui/shared/toast/ToastQueueTest.kt | 149 ++++++++++++++++++ 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt diff --git a/app/src/main/java/to/bitkit/ui/Locals.kt b/app/src/main/java/to/bitkit/ui/Locals.kt index dd1f02f7d..89688e9eb 100644 --- a/app/src/main/java/to/bitkit/ui/Locals.kt +++ b/app/src/main/java/to/bitkit/ui/Locals.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf import to.bitkit.models.BalanceState import to.bitkit.repositories.CurrencyState +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.ActivityListViewModel import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.BackupsViewModel @@ -14,7 +15,6 @@ import to.bitkit.viewmodels.CurrencyViewModel import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel -import to.bitkit.ui.shared.toast.Toaster // Locals val LocalBalances = compositionLocalOf { BalanceState() } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 001563bc3..1e24dce3f 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -47,7 +47,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import kotlin.time.Duration import org.lightningdevkit.ldknode.ChannelDataMigration import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId @@ -120,6 +119,7 @@ import to.bitkit.utils.timedsheets.sheets.QuickPayTimedSheet import java.math.BigDecimal import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) diff --git a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt new file mode 100644 index 000000000..bdf646c76 --- /dev/null +++ b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt @@ -0,0 +1,149 @@ +package to.bitkit.ui.shared.toast + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import to.bitkit.models.Toast +import to.bitkit.models.ToastType +import to.bitkit.test.BaseUnitTest +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { + private lateinit var sut: ToastQueue + + @Before + fun setUp() { + sut = ToastQueue(testDispatcher) + } + + @Test + fun `enqueue shows toast immediately when queue empty`() = test { + val toast = createToast() + + sut.enqueue(toast) + + assertEquals(toast, sut.currentToast.value) + } + + @Test + fun `enqueue queues toast when another is displayed`() = test { + val toast1 = createToast(title = "First") + val toast2 = createToast(title = "Second") + + sut.enqueue(toast1) + sut.enqueue(toast2) + + assertEquals("Second", sut.currentToast.value?.title) + } + + @Test + fun `dismiss advances to next toast in queue`() = test { + val toast1 = createToast(title = "First", autoHide = false) + val toast2 = createToast(title = "Second", autoHide = false) + + sut.enqueue(toast1) + sut.enqueue(toast2) + + assertEquals("Second", sut.currentToast.value?.title) + + sut.dismissCurrentToast() + + assertNull(sut.currentToast.value) + } + + @Test + fun `auto-hide timer dismisses toast after duration`() = test { + val toast = createToast(autoHide = true) + + sut.enqueue(toast) + + assertEquals(toast, sut.currentToast.value) + + advanceTimeBy(3001) + + assertNull(sut.currentToast.value) + } + + @Test + fun `pause stops auto-hide timer`() = test { + val toast = createToast(autoHide = true) + + sut.enqueue(toast) + advanceTimeBy(1000) + sut.pauseCurrentToast() + advanceTimeBy(5000) + + assertEquals(toast, sut.currentToast.value) + } + + @Test + fun `resume restarts auto-hide timer`() = test { + val toast = createToast(autoHide = true) + + sut.enqueue(toast) + advanceTimeBy(1000) + sut.pauseCurrentToast() + advanceTimeBy(5000) + sut.resumeCurrentToast() + advanceTimeBy(2000) + + assertEquals(toast, sut.currentToast.value) + + advanceTimeBy(1001) + + assertNull(sut.currentToast.value) + } + + @Test + fun `max queue size drops oldest when exceeded`() = test { + val toasts = (1..6).map { createToast(title = "Toast $it") } + + toasts.forEach { sut.enqueue(it) } + + assertEquals("Toast 6", sut.currentToast.value?.title) + } + + @Test + fun `clear removes all toasts and hides current`() = test { + val toast1 = createToast(title = "First", autoHide = false) + val toast2 = createToast(title = "Second", autoHide = false) + + sut.enqueue(toast1) + sut.enqueue(toast2) + sut.clear() + + assertNull(sut.currentToast.value) + } + + @Test + fun `non-auto-hide toast stays until dismissed`() = test { + val toast = createToast(autoHide = false) + + sut.enqueue(toast) + advanceTimeBy(10_000) + + assertEquals(toast, sut.currentToast.value) + + sut.dismissCurrentToast() + + assertNull(sut.currentToast.value) + } + + private fun createToast( + title: String = "Test Toast", + body: String? = null, + type: ToastType = ToastType.INFO, + autoHide: Boolean = true, + ) = Toast( + type = type, + title = title, + body = body, + autoHide = autoHide, + duration = 3.seconds, + ) +} From 6feaede3ee6880c548f1fa8eb5772b79821db7cf Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 22:58:26 +0100 Subject: [PATCH 12/21] refactor: add @StringRes overloads to Toaster Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 4 +- .../main/java/to/bitkit/models/ToastText.kt | 8 ++ .../java/to/bitkit/ui/components/ToastView.kt | 28 ++--- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 105 ++++++++++++++++-- .../java/to/bitkit/viewmodels/AppViewModel.kt | 56 +++++++--- .../bitkit/ui/shared/toast/ToastQueueTest.kt | 11 +- 6 files changed, 167 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index ca9c7eb1e..8dbc764ab 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -7,8 +7,8 @@ import kotlin.time.Duration.Companion.seconds @Stable data class Toast( val type: ToastType, - val title: String, - val body: String? = null, + val title: ToastText, + val body: ToastText? = null, val autoHide: Boolean, val duration: Duration = DURATION_DEFAULT, val testTag: String? = null, diff --git a/app/src/main/java/to/bitkit/models/ToastText.kt b/app/src/main/java/to/bitkit/models/ToastText.kt index bf73e651e..bb9335387 100644 --- a/app/src/main/java/to/bitkit/models/ToastText.kt +++ b/app/src/main/java/to/bitkit/models/ToastText.kt @@ -2,7 +2,9 @@ package to.bitkit.models import android.content.Context import androidx.annotation.StringRes +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.ui.res.stringResource @Stable sealed interface ToastText { @@ -15,6 +17,12 @@ sealed interface ToastText { value class Literal(val value: String) : ToastText } +@Composable +fun ToastText.asString(): String = when (this) { + is ToastText.Resource -> stringResource(resId) + is ToastText.Literal -> value +} + fun ToastText.asString(context: Context): String = when (this) { is ToastText.Resource -> context.getString(resId) is ToastText.Literal -> value diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index c08b837d1..98f89fc0d 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -53,7 +53,9 @@ import dev.chrisbanes.haze.rememberHazeState import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.ToastType +import to.bitkit.models.asString import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -259,12 +261,12 @@ private fun ToastContent( .padding(16.dp) ) { BodyMSB( - text = toast.title, + text = toast.title.asString(), color = tintColor, ) toast.body?.let { body -> Caption( - text = body, + text = body.asString(), color = Colors.White ) } @@ -299,7 +301,7 @@ private fun ToastContent( @Preview(showSystemUi = true) @Composable -private fun ToastContentPreview() { +private fun Preview() { AppThemeSurface { ScreenColumn( verticalArrangement = Arrangement.spacedBy(16.dp), @@ -307,8 +309,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.WARNING, - title = "You're still offline", - body = "Check your connection to keep using Bitkit.", + title = ToastText.Literal("You're still offline"), + body = ToastText.Literal("Check your connection to keep using Bitkit."), autoHide = true, ), onDismiss = {}, @@ -316,8 +318,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.LIGHTNING, - title = "Instant Payments Ready", - body = "You can now pay anyone, anywhere, instantly.", + title = ToastText.Literal("Instant Payments Ready"), + body = ToastText.Literal("You can now pay anyone, anywhere, instantly."), autoHide = true, ), onDismiss = {}, @@ -325,8 +327,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.SUCCESS, - title = "You're Back Online!", - body = "Successfully reconnected to the Internet.", + title = ToastText.Literal("You're Back Online!"), + body = ToastText.Literal("Successfully reconnected to the Internet."), autoHide = true, ), onDismiss = {}, @@ -334,8 +336,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.INFO, - title = "General Message", - body = "Used for neutral content to inform the user.", + title = ToastText.Literal("General Message"), + body = ToastText.Literal("Used for neutral content to inform the user."), autoHide = false, ), onDismiss = {}, @@ -343,8 +345,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.ERROR, - title = "Error Toast", - body = "This is a toast message.", + title = ToastText.Literal("Error Toast"), + body = ToastText.Literal("This is a toast message."), autoHide = true, ), onDismiss = {}, diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 62db3dce7..cae6b7b37 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -1,14 +1,17 @@ package to.bitkit.ui.shared.toast +import androidx.annotation.StringRes import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration +@Suppress("TooManyFunctions") @Singleton class Toaster @Inject constructor() { private val _events = MutableSharedFlow(extraBufferCapacity = 1) @@ -17,8 +20,8 @@ class Toaster @Inject constructor() { @Suppress("LongParameterList") private suspend fun emit( type: ToastType, - title: String, - body: String? = null, + title: ToastText, + body: ToastText? = null, autoHide: Boolean = true, duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, @@ -35,39 +38,123 @@ class Toaster @Inject constructor() { ) } + // region @StringRes overloads + suspend fun success( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.SUCCESS, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag + ) + + suspend fun info( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.INFO, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag, + ) + + suspend fun lightning( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.LIGHTNING, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag + ) + + suspend fun warning( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.WARNING, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag + ) + + suspend fun error( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.ERROR, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag, + ) + // endregion + + // region String literal overloads suspend fun success( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) + ) = emit( + ToastType.SUCCESS, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun info( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.INFO, title, body, testTag = testTag) + ) = emit( + ToastType.INFO, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun lightning( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) + ) = emit( + ToastType.LIGHTNING, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun warning( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.WARNING, title, body, testTag = testTag) + ) = emit( + ToastType.WARNING, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun error( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.ERROR, title, body, testTag = testTag) + ) = emit( + ToastType.ERROR, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, - title = "Error", - body = throwable.message ?: "An unknown error occurred", + title = ToastText.Literal("Error"), + body = ToastText.Literal(throwable.message ?: "An unknown error occurred"), ) + // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 1e24dce3f..3e3c8d9e4 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -83,6 +83,7 @@ import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.models.TransactionSpeed import to.bitkit.models.safe @@ -234,9 +235,7 @@ class AppViewModel @Inject constructor( init { viewModelScope.launch { - toaster.events.collect { - toast(it.type, it.title, it.body, it.autoHide, it.duration) - } + toaster.events.collect { toastQueue.enqueue(it) } } viewModelScope.launch { // Delays are required for auth check on launch functionality @@ -1798,8 +1797,8 @@ class AppViewModel @Inject constructor( fun toast( type: ToastType, - title: String, - body: String? = null, + title: ToastText, + body: ToastText? = null, autoHide: Boolean = true, duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, @@ -1816,23 +1815,48 @@ class AppViewModel @Inject constructor( ) } + fun toast( + type: ToastType, + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + autoHide: Boolean = true, + duration: Duration = Toast.DURATION_DEFAULT, + testTag: String? = null, + ) = toast( + type = type, + title = ToastText.Resource(titleRes), + body = bodyRes?.let { ToastText.Resource(it) }, + autoHide = autoHide, + duration = duration, + testTag = testTag, + ) + + fun toast( + type: ToastType, + title: String, + body: String? = null, + autoHide: Boolean = true, + duration: Duration = Toast.DURATION_DEFAULT, + testTag: String? = null, + ) = toast( + type = type, + title = ToastText.Literal(title), + body = body?.let { ToastText.Literal(it) }, + autoHide = autoHide, + duration = duration, + testTag = testTag, + ) + fun toast(error: Throwable) { toast( type = ToastType.ERROR, - title = context.getString(R.string.common__error), - body = error.message ?: context.getString(R.string.common__error_body) + title = ToastText.Resource(R.string.common__error), + body = error.message?.let { ToastText.Literal(it) } + ?: ToastText.Resource(R.string.common__error_body) ) } - fun toast(toast: Toast) { - toast( - type = toast.type, - title = toast.title, - body = toast.body, - autoHide = toast.autoHide, - duration = toast.duration - ) - } + fun toast(toast: Toast) = toastQueue.enqueue(toast) fun hideToast() = toastQueue.dismissCurrentToast() diff --git a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt index bdf646c76..f341814ec 100644 --- a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt +++ b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt @@ -8,6 +8,7 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.test.BaseUnitTest import kotlin.time.Duration.Companion.seconds @@ -38,7 +39,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { sut.enqueue(toast1) sut.enqueue(toast2) - assertEquals("Second", sut.currentToast.value?.title) + assertEquals(ToastText.Literal("Second"), sut.currentToast.value?.title) } @Test @@ -49,7 +50,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { sut.enqueue(toast1) sut.enqueue(toast2) - assertEquals("Second", sut.currentToast.value?.title) + assertEquals(ToastText.Literal("Second"), sut.currentToast.value?.title) sut.dismissCurrentToast() @@ -105,7 +106,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { toasts.forEach { sut.enqueue(it) } - assertEquals("Toast 6", sut.currentToast.value?.title) + assertEquals(ToastText.Literal("Toast 6"), sut.currentToast.value?.title) } @Test @@ -141,8 +142,8 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { autoHide: Boolean = true, ) = Toast( type = type, - title = title, - body = body, + title = ToastText.Literal(title), + body = body?.let { ToastText.Literal(it) }, autoHide = autoHide, duration = 3.seconds, ) From 1ad36fa80546b8730bd260bfcf70623f69dfba04 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 23:06:14 +0100 Subject: [PATCH 13/21] refactor: use @StringRes in composable toasts Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 6 ++-- .../bitkit/ui/components/IsOnlineTracker.kt | 10 +++---- .../ui/screens/scanner/QrScanningScreen.kt | 8 +++--- .../screens/transfer/SavingsProgressScreen.kt | 10 +++---- .../wallets/activity/ActivityDetailScreen.kt | 17 ++++++----- .../to/bitkit/ui/settings/SettingsScreen.kt | 28 ++++++++----------- .../settings/advanced/AddressViewerScreen.kt | 5 ++-- .../settings/advanced/ElectrumConfigScreen.kt | 15 ++++++---- .../ui/settings/advanced/RgsServerScreen.kt | 11 ++++---- .../settings/lightning/ChannelDetailScreen.kt | 5 ++-- 10 files changed, 55 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 8c394873b..2317fe37a 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -46,6 +46,7 @@ import to.bitkit.ext.createChannelDetails import to.bitkit.ext.formatToString import to.bitkit.ext.uri import to.bitkit.models.NodeLifecycleState +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.LightningState @@ -77,7 +78,6 @@ fun NodeInfoScreen( val wallet = walletViewModel ?: return val app = appViewModel ?: return val settings = settingsViewModel ?: return - val context = LocalContext.current val isRefreshing by wallet.isRefreshing.collectAsStateWithLifecycle() val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle() @@ -93,8 +93,8 @@ fun NodeInfoScreen( onCopy = { text -> app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - body = text + title = ToastText.Resource(R.string.common__copied), + body = ToastText.Literal(text), ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 9e208724c..2b0bcd07c 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.repositories.ConnectivityState @@ -16,7 +15,6 @@ import to.bitkit.viewmodels.AppViewModel fun IsOnlineTracker( app: AppViewModel, ) { - val context = LocalContext.current val toaster = toaster ?: return val connectivityState by app.isOnline.collectAsStateWithLifecycle(initialValue = ConnectivityState.CONNECTED) @@ -32,15 +30,15 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { toaster.success( - title = context.getString(R.string.other__connection_back_title), - body = context.getString(R.string.other__connection_back_msg), + titleRes = R.string.other__connection_back_title, + bodyRes = R.string.other__connection_back_msg, ) } ConnectivityState.DISCONNECTED -> { toaster.warning( - title = context.getString(R.string.other__connection_issue), - body = context.getString(R.string.other__connection_issue_explain), + titleRes = R.string.other__connection_issue, + bodyRes = R.string.other__connection_issue_explain, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 264ee2353..0490be66d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -151,8 +151,8 @@ fun QrScanningScreen( Logger.error("Failed to scan QR code", error) app.toast( type = ToastType.ERROR, - title = context.getString(R.string.other__qr_error_header), - body = context.getString(R.string.other__qr_error_text), + titleRes = R.string.other__qr_error_header, + bodyRes = R.string.other__qr_error_text, ) } } @@ -257,8 +257,8 @@ private fun handlePaste( if (clipboard.isNullOrBlank()) { app.toast( type = ToastType.WARNING, - title = context.getString(R.string.wallet__send_clipboard_empty_title), - body = context.getString(R.string.wallet__send_clipboard_empty_text), + titleRes = R.string.wallet__send_clipboard_empty_title, + bodyRes = R.string.wallet__send_clipboard_empty_text, ) } setScanResult(clipboard) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 320066d63..089241e90 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.keepScreenOn import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -52,7 +51,6 @@ fun SavingsProgressScreen( onContinueClick: () -> Unit = {}, onTransferUnavailable: () -> Unit = {}, ) { - val context = LocalContext.current var progressState by remember { mutableStateOf(SavingsProgressState.PROGRESS) } // Effect to close channels & update UI @@ -72,8 +70,8 @@ fun SavingsProgressScreen( // All channels are trusted peers - show error and navigate back immediately app.toast( type = ToastType.ERROR, - title = context.getString(R.string.lightning__close_error), - body = context.getString(R.string.lightning__close_error_msg), + titleRes = R.string.lightning__close_error, + bodyRes = R.string.lightning__close_error_msg, ) onTransferUnavailable() } else { @@ -83,8 +81,8 @@ fun SavingsProgressScreen( onTransferUnavailable = { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.lightning__close_error), - body = context.getString(R.string.lightning__close_error_msg), + titleRes = R.string.lightning__close_error, + bodyRes = R.string.lightning__close_error_msg, ) onTransferUnavailable() }, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index 9ef1f4663..e0e4abd82 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -194,7 +194,6 @@ fun ActivityDetailScreen( } } - val context = LocalContext.current val blocktankInfo by blocktankViewModel?.info?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(null) } @@ -252,8 +251,8 @@ fun ActivityDetailScreen( onSuccess = { app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.wallet__boost_success_title), - body = context.getString(R.string.wallet__boost_success_msg), + titleRes = R.string.wallet__boost_success_title, + bodyRes = R.string.wallet__boost_success_msg, testTag = "BoostSuccessToast" ) listViewModel.resync() @@ -262,8 +261,8 @@ fun ActivityDetailScreen( onFailure = { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.wallet__boost_error_title), - body = context.getString(R.string.wallet__boost_error_msg), + titleRes = R.string.wallet__boost_error_title, + bodyRes = R.string.wallet__boost_error_msg, testTag = "BoostFailureToast" ) detailViewModel.onDismissBoostSheet() @@ -271,15 +270,15 @@ fun ActivityDetailScreen( onMaxFee = { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.wallet__send_fee_error), - body = context.getString(R.string.wallet__send_fee_error_max) + titleRes = R.string.wallet__send_fee_error, + bodyRes = R.string.wallet__send_fee_error_max, ) }, onMinFee = { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.wallet__send_fee_error), - body = context.getString(R.string.wallet__send_fee_error_min) + titleRes = R.string.wallet__send_fee_error, + bodyRes = R.string.wallet__send_fee_error_min, ) } ) diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index e57e4e180..ac1c19c2d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -54,7 +53,6 @@ fun SettingsScreen( val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle() var enableDevModeTapCount by remember { mutableIntStateOf(0) } val haptic = LocalHapticFeedback.current - val context = LocalContext.current SettingsScreenContent( isDevModeEnabled = isDevModeEnabled, @@ -75,22 +73,20 @@ fun SettingsScreen( settings.setIsDevModeEnabled(newValue) haptic.performHapticFeedback(HapticFeedbackType.LongPress) + val titleRes = if (newValue) { + R.string.settings__dev_enabled_title + } else { + R.string.settings__dev_disabled_title + } + val bodyRes = if (newValue) { + R.string.settings__dev_enabled_message + } else { + R.string.settings__dev_disabled_message + } app.toast( type = ToastType.SUCCESS, - title = context.getString( - if (newValue) { - R.string.settings__dev_enabled_title - } else { - R.string.settings__dev_disabled_title - } - ), - body = context.getString( - if (newValue) { - R.string.settings__dev_enabled_message - } else { - R.string.settings__dev_disabled_message - } - ), + titleRes = titleRes, + bodyRes = bodyRes, ) enableDevModeTapCount = 0 } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 6754fea54..58ff99f8c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -30,6 +30,7 @@ import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.ui.appViewModel @@ -79,8 +80,8 @@ fun AddressViewerScreen( context.setClipboardText(text) app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - body = text, + title = ToastText.Resource(R.string.common__copied), + body = ToastText.Literal(text), ) } ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 1a06e5556..57b675d30 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM @@ -76,17 +77,19 @@ fun ElectrumConfigScreen( if (result.isSuccess) { app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.settings__es__server_updated_title), - body = context.getString(R.string.settings__es__server_updated_message) - .replace("{host}", uiState.host) - .replace("{port}", uiState.port), + title = ToastText.Resource(R.string.settings__es__server_updated_title), + body = ToastText.Literal( + context.getString(R.string.settings__es__server_updated_message) + .replace("{host}", uiState.host) + .replace("{port}", uiState.port) + ), testTag = "ElectrumUpdatedToast", ) } else { app.toast( type = ToastType.WARNING, - title = context.getString(R.string.settings__es__server_error), - body = context.getString(R.string.settings__es__server_error_description), + titleRes = R.string.settings__es__server_error, + bodyRes = R.string.settings__es__server_error_description, testTag = "ElectrumErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 5072b5f15..27962dde2 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -26,6 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM @@ -51,7 +51,6 @@ fun RgsServerScreen( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val app = appViewModel ?: return - val context = LocalContext.current // Handle result from Scanner LaunchedEffect(savedStateHandle) { @@ -69,15 +68,15 @@ fun RgsServerScreen( if (result.isSuccess) { app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.settings__rgs__update_success_title), - body = context.getString(R.string.settings__rgs__update_success_description), + titleRes = R.string.settings__rgs__update_success_title, + bodyRes = R.string.settings__rgs__update_success_description, testTag = "RgsUpdatedToast", ) } else { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.wallet__ldk_start_error_title), - body = result.exceptionOrNull()?.message ?: "Unknown error", + title = ToastText.Resource(R.string.wallet__ldk_start_error_title), + body = ToastText.Literal(result.exceptionOrNull()?.message ?: "Unknown error"), testTag = "RgsErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 2cd13a0fb..ca53e3928 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -55,6 +55,7 @@ import to.bitkit.ext.DatePattern import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel @@ -130,8 +131,8 @@ fun ChannelDetailScreen( context.setClipboardText(text) app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - body = text, + title = ToastText.Resource(R.string.common__copied), + body = ToastText.Literal(text), ) }, onOpenUrl = { txId -> From a9b764968172a2d629e847303a5f978101d4cd9f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 23:14:04 +0100 Subject: [PATCH 14/21] refactor: use @StringRes in ViewModel toasts Co-Authored-By: Claude Opus 4.5 --- .../recovery/RecoveryMnemonicViewModel.kt | 2 +- .../ui/screens/recovery/RecoveryViewModel.kt | 12 +++---- .../external/ExternalNodeViewModel.kt | 27 +++++++++------- .../external/LnurlChannelViewModel.kt | 9 +++--- .../screens/wallets/send/SendFeeViewModel.kt | 8 ++--- .../advanced/ElectrumConfigViewModel.kt | 9 +++--- .../backups/BackupNavSheetViewModel.kt | 8 ++--- .../LightningConnectionsViewModel.kt | 12 +++---- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 32 +++++++++++++++++++ .../to/bitkit/viewmodels/TransferViewModel.kt | 17 +++++----- .../to/bitkit/viewmodels/WalletViewModel.kt | 13 ++++---- 11 files changed, 95 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index 48fcd3816..ea11d4f30 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -42,7 +42,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - toaster.error(context.getString(R.string.security__mnemonic_load_error)) + toaster.error(titleRes = R.string.security__mnemonic_load_error) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index d229d64d3..0612d4dd6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -74,8 +74,8 @@ class RecoveryViewModel @Inject constructor( ) } toaster.error( - context.getString(R.string.common__error), - context.getString(R.string.other__logs_export_error), + titleRes = R.string.common__error, + bodyRes = R.string.other__logs_export_error, ) } ) @@ -98,8 +98,8 @@ class RecoveryViewModel @Inject constructor( Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { toaster.error( - context.getString(R.string.common__error), - context.getString(R.string.settings__support__link_error), + titleRes = R.string.common__error, + bodyRes = R.string.settings__support__link_error, ) } } @@ -120,8 +120,8 @@ class RecoveryViewModel @Inject constructor( toaster.error(error) }.onSuccess { toaster.success( - context.getString(R.string.security__wiped_title), - context.getString(R.string.security__wiped_message), + titleRes = R.string.security__wiped_title, + bodyRes = R.string.security__wiped_message, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index ed1fcea4e..329be3aa7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -20,6 +20,7 @@ import to.bitkit.data.SettingsStore import to.bitkit.ext.WatchResult import to.bitkit.ext.of import to.bitkit.ext.watchUntil +import to.bitkit.models.ToastText import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType import to.bitkit.models.formatToModernDisplay @@ -74,8 +75,8 @@ class ExternalNodeViewModel @Inject constructor( setEffect(SideEffect.ConnectionSuccess) } else { toaster.error( - context.getString(R.string.lightning__error_add_title), - context.getString(R.string.lightning__error_add), + titleRes = R.string.lightning__error_add_title, + bodyRes = R.string.lightning__error_add, ) } } @@ -88,7 +89,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - toaster.error(context.getString(R.string.lightning__error_add_uri)) + toaster.error(titleRes = R.string.lightning__error_add_uri) } } } @@ -99,9 +100,11 @@ class ExternalNodeViewModel @Inject constructor( if (sats > maxAmount) { viewModelScope.launch { toaster.error( - title = context.getString(R.string.lightning__spending_amount__error_max__title), - body = context.getString(R.string.lightning__spending_amount__error_max__description) - .replace("{amount}", maxAmount.formatToModernDisplay()), + title = ToastText.Resource(R.string.lightning__spending_amount__error_max__title), + body = ToastText.Literal( + context.getString(R.string.lightning__spending_amount__error_max__description) + .replace("{amount}", maxAmount.formatToModernDisplay()) + ), ) } return @@ -133,8 +136,8 @@ class ExternalNodeViewModel @Inject constructor( val feeRate = _uiState.value.customFeeRate ?: 0u if (feeRate == 0u) { toaster.info( - context.getString(R.string.wallet__min_possible_fee_rate), - context.getString(R.string.wallet__min_possible_fee_rate_msg), + titleRes = R.string.wallet__min_possible_fee_rate, + bodyRes = R.string.wallet__min_possible_fee_rate_msg, ) return false } @@ -188,9 +191,11 @@ class ExternalNodeViewModel @Inject constructor( val error = e.message.orEmpty() Logger.warn("Error opening channel with peer: '${_uiState.value.peer}': '$error'") toaster.error( - title = context.getString(R.string.lightning__error_channel_purchase), - body = context.getString(R.string.lightning__error_channel_setup_msg) - .replace("{raw}", error), + title = ToastText.Resource(R.string.lightning__error_channel_purchase), + body = ToastText.Literal( + context.getString(R.string.lightning__error_channel_setup_msg) + .replace("{raw}", error) + ), ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index f60d580fb..40ac52756 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.ext.of +import to.bitkit.models.ToastText import to.bitkit.repositories.LightningRepo import to.bitkit.ui.Routes import to.bitkit.ui.shared.toast.Toaster @@ -69,8 +70,8 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { toaster.success( - context.getString(R.string.other__lnurl_channel_success_title), - context.getString(R.string.other__lnurl_channel_success_msg_no_peer), + titleRes = R.string.other__lnurl_channel_success_title, + bodyRes = R.string.other__lnurl_channel_success_msg_no_peer, ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> @@ -83,8 +84,8 @@ class LnurlChannelViewModel @Inject constructor( suspend fun errorToast(error: Throwable) { toaster.error( - title = context.getString(R.string.other__lnurl_channel_error), - body = error.message ?: "Unknown error", + title = ToastText.Resource(R.string.other__lnurl_channel_error), + body = ToastText.Literal(error.message ?: context.getString(R.string.common__error_body)), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index 417a8eef9..3d44046e8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -106,16 +106,16 @@ class SendFeeViewModel @Inject constructor( val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { toaster.info( - context.getString(R.string.wallet__min_possible_fee_rate), - context.getString(R.string.wallet__min_possible_fee_rate_msg), + titleRes = R.string.wallet__min_possible_fee_rate, + bodyRes = R.string.wallet__min_possible_fee_rate_msg, ) return false } if (satsPerVByte > maxSatsPerVByte) { toaster.info( - context.getString(R.string.wallet__max_possible_fee_rate), - context.getString(R.string.wallet__max_possible_fee_rate_msg), + titleRes = R.string.wallet__max_possible_fee_rate, + bodyRes = R.string.wallet__max_possible_fee_rate_msg, ) return false } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index 70b456f10..bb189f8b5 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -23,6 +23,7 @@ import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServer import to.bitkit.models.ElectrumServerPeer import to.bitkit.models.MAX_VALID_PORT +import to.bitkit.models.ToastText import to.bitkit.models.getDefaultPort import to.bitkit.repositories.LightningRepo import to.bitkit.ui.shared.toast.Toaster @@ -247,8 +248,8 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput() if (validationError != null) { toaster.warning( - title = context.getString(R.string.settings__es__error_peer), - body = validationError, + title = ToastText.Resource(R.string.settings__es__error_peer), + body = ToastText.Literal(validationError), ) } else { connectToServer() @@ -268,8 +269,8 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput(host, port) if (validationError != null) { toaster.warning( - title = context.getString(R.string.settings__es__error_peer), - body = validationError, + title = ToastText.Resource(R.string.settings__es__error_peer), + body = ToastText.Literal(validationError), ) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 8bb0947f3..e50f624f3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -87,8 +87,8 @@ class BackupNavSheetViewModel @Inject constructor( }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) toaster.warning( - context.getString(R.string.security__mnemonic_error), - context.getString(R.string.security__mnemonic_error_description), + titleRes = R.string.security__mnemonic_error, + bodyRes = R.string.security__mnemonic_error_description, ) } } @@ -157,8 +157,8 @@ class BackupNavSheetViewModel @Inject constructor( fun onMnemonicCopied() { viewModelScope.launch { toaster.success( - context.getString(R.string.common__copied), - context.getString(R.string.security__mnemonic_copied), + titleRes = R.string.common__copied, + bodyRes = R.string.security__mnemonic_copied, ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 0126fbf6b..52f728917 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -340,8 +340,8 @@ class LightningConnectionsViewModel @Inject constructor( .onSuccess { uri -> onReady(uri) } .onFailure { toaster.warning( - context.getString(R.string.lightning__error_logs), - context.getString(R.string.lightning__error_logs_description), + titleRes = R.string.lightning__error_logs, + bodyRes = R.string.lightning__error_logs_description, ) } } @@ -454,8 +454,8 @@ class LightningConnectionsViewModel @Inject constructor( walletRepo.syncNodeAndWallet() toaster.success( - context.getString(R.string.lightning__close_success_title), - context.getString(R.string.lightning__close_success_msg), + titleRes = R.string.lightning__close_success_title, + bodyRes = R.string.lightning__close_success_msg, ) _closeConnectionUiState.update { @@ -469,8 +469,8 @@ class LightningConnectionsViewModel @Inject constructor( Logger.error("Failed to close channel", e = error, context = TAG) toaster.warning( - context.getString(R.string.lightning__close_error), - context.getString(R.string.lightning__close_error_msg), + titleRes = R.string.lightning__close_error, + bodyRes = R.string.lightning__close_error_msg, ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index cae6b7b37..59d103fc3 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -95,6 +95,38 @@ class Toaster @Inject constructor() { ) // endregion + // region ToastText overloads + suspend fun success( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) + + suspend fun info( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.INFO, title, body, testTag = testTag) + + suspend fun lightning( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) + + suspend fun warning( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.WARNING, title, body, testTag = testTag) + + suspend fun error( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.ERROR, title, body, testTag = testTag) + // endregion + // region String literal overloads suspend fun success( title: String, diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 02af58076..705089029 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -29,6 +29,7 @@ import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.ext.amountOnClose +import to.bitkit.models.ToastText import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType import to.bitkit.models.safe @@ -459,8 +460,8 @@ class TransferViewModel @Inject constructor( channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) toaster.error( - context.getString(R.string.lightning__force_failed_title), - context.getString(R.string.lightning__force_failed_msg), + titleRes = R.string.lightning__force_failed_title, + bodyRes = R.string.lightning__force_failed_msg, ) return@runCatching } @@ -483,21 +484,21 @@ class TransferViewModel @Inject constructor( val skippedMsg = context.getString(R.string.lightning__force_channels_skipped) val bodyText = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg toaster.lightning( - title = context.getString(R.string.lightning__force_init_title), - body = bodyText, + title = ToastText.Resource(R.string.lightning__force_init_title), + body = ToastText.Literal(bodyText), ) } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) toaster.error( - context.getString(R.string.lightning__force_failed_title), - context.getString(R.string.lightning__force_failed_msg), + titleRes = R.string.lightning__force_failed_title, + bodyRes = R.string.lightning__force_failed_msg, ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) toaster.error( - context.getString(R.string.lightning__force_failed_title), - context.getString(R.string.lightning__force_failed_msg), + titleRes = R.string.lightning__force_failed_title, + bodyRes = R.string.lightning__force_failed_msg, ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 9fc2a6d38..22192fa7c 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -26,6 +26,7 @@ import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher +import to.bitkit.models.ToastText import to.bitkit.repositories.BackupRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo @@ -301,14 +302,14 @@ class WalletViewModel @Inject constructor( lightningRepo.disconnectPeer(peer) .onSuccess { toaster.info( - context.getString(R.string.common__success), - context.getString(R.string.wallet__peer_disconnected), + titleRes = R.string.common__success, + bodyRes = R.string.wallet__peer_disconnected, ) } .onFailure { toaster.error( - title = context.getString(R.string.common__error), - body = it.message ?: context.getString(R.string.common__error_body) + title = ToastText.Resource(R.string.common__error), + body = ToastText.Literal(it.message ?: context.getString(R.string.common__error_body)), ) } } @@ -317,8 +318,8 @@ class WalletViewModel @Inject constructor( fun updateBip21Invoice(amountSats: ULong? = walletState.value.bip21AmountSats) = viewModelScope.launch { walletRepo.updateBip21Invoice(amountSats).onFailure { error -> toaster.error( - title = context.getString(R.string.wallet__error_invoice_update), - body = error.message ?: context.getString(R.string.common__error_body) + title = ToastText.Resource(R.string.wallet__error_invoice_update), + body = ToastText.Literal(error.message ?: context.getString(R.string.common__error_body)), ) } } From 84c5eee51b49791e109200b6dd68c4b4450e89fa Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 23:18:25 +0100 Subject: [PATCH 15/21] fix: localize hardcoded toast strings Co-Authored-By: Claude Opus 4.5 --- .../screens/wallets/send/SendCoinSelectionViewModel.kt | 9 ++++++++- app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt | 6 ++++-- .../main/java/to/bitkit/viewmodels/WalletViewModel.kt | 4 ++-- app/src/main/res/values/strings.xml | 3 +++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt index b3cfcb9b6..3f5c1bb07 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt @@ -1,16 +1,19 @@ package to.bitkit.ui.screens.wallets.send +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.Activity.Onchain import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.SpendableUtxo +import to.bitkit.R import to.bitkit.di.BgDispatcher import to.bitkit.env.Defaults import to.bitkit.ext.rawId @@ -22,6 +25,7 @@ import javax.inject.Inject @HiltViewModel class SendCoinSelectionViewModel @Inject constructor( + @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, private val activityRepo: ActivityRepo, @@ -68,7 +72,10 @@ class SendCoinSelectionViewModel @Inject constructor( } }.onFailure { Logger.error("Failed to load UTXOs for coin selection", it, context = TAG) - toaster.error("Failed to load UTXOs: ${it.message}") + toaster.error( + context.getString(R.string.wallet__error_utxo_load) + .replace("{raw}", it.message.orEmpty()) + ) } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 59d103fc3..23370f216 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -4,6 +4,7 @@ import androidx.annotation.StringRes import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.models.ToastText import to.bitkit.models.ToastType @@ -185,8 +186,9 @@ class Toaster @Inject constructor() { suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, - title = ToastText.Literal("Error"), - body = ToastText.Literal(throwable.message ?: "An unknown error occurred"), + title = ToastText.Resource(R.string.common__error), + body = throwable.message?.let { ToastText.Literal(it) } + ?: ToastText.Resource(R.string.common__error_body), ) // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 22192fa7c..56eaca2a2 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -128,8 +128,8 @@ class WalletViewModel @Inject constructor( migrationService.markMigrationChecked() migrationService.setShowingMigrationLoading(false) toaster.error( - title = "Migration Failed", - body = "Please restore your wallet manually using your recovery phrase" + titleRes = R.string.wallet__migration_error_title, + bodyRes = R.string.wallet__migration_error_body, ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54f5d0b3c..415efb3db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1094,6 +1094,7 @@ Please check your transaction info and try again. No transaction is available to broadcast. Error Sending + Failed to load UTXOs: {raw} Apply Clear Select Range @@ -1112,6 +1113,8 @@ Withdraw Bitcoin Fee Exceeds Maximum Limit Lower the custom fee and try again. + Please restore your wallet manually using your recovery phrase + Migration Failed Fee Below Minimum Limit Increase the custom fee and try again. MINIMUM From f50889f04ed997147a8883cf607b8a90d547ced3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:14:57 +0100 Subject: [PATCH 16/21] refactor: prettify Toast and Toaster APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ToastText() factory constructors (invoke operators) - Remove redundant @Stable from ToastType enum and value classes - Rename titleRes/bodyRes → title/body in @StringRes overloads - Rename warning() → warn() across all overloads - Update all call sites for the API changes Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 1 - .../main/java/to/bitkit/models/ToastText.kt | 7 ++- .../bitkit/ui/components/IsOnlineTracker.kt | 10 ++-- .../recovery/RecoveryMnemonicViewModel.kt | 2 +- .../ui/screens/recovery/RecoveryViewModel.kt | 12 ++--- .../ui/screens/scanner/QrScanningScreen.kt | 8 ++-- .../screens/transfer/SavingsProgressScreen.kt | 8 ++-- .../external/ExternalNodeViewModel.kt | 10 ++-- .../external/LnurlChannelViewModel.kt | 4 +- .../wallets/activity/ActivityDetailScreen.kt | 16 +++---- .../screens/wallets/send/SendFeeViewModel.kt | 8 ++-- .../to/bitkit/ui/settings/SettingsScreen.kt | 4 +- .../settings/advanced/ElectrumConfigScreen.kt | 4 +- .../advanced/ElectrumConfigViewModel.kt | 4 +- .../ui/settings/advanced/RgsServerScreen.kt | 4 +- .../backups/BackupNavSheetViewModel.kt | 10 ++-- .../LightningConnectionsViewModel.kt | 16 +++---- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 46 +++++++++---------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 8 ++-- .../bitkit/viewmodels/DevSettingsViewModel.kt | 6 +-- .../to/bitkit/viewmodels/LdkDebugViewModel.kt | 8 ++-- .../to/bitkit/viewmodels/TransferViewModel.kt | 12 ++--- .../to/bitkit/viewmodels/WalletViewModel.kt | 8 ++-- 23 files changed, 109 insertions(+), 107 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index 8dbc764ab..debb58267 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -18,5 +18,4 @@ data class Toast( } } -@Stable enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } diff --git a/app/src/main/java/to/bitkit/models/ToastText.kt b/app/src/main/java/to/bitkit/models/ToastText.kt index bb9335387..fb36e8a26 100644 --- a/app/src/main/java/to/bitkit/models/ToastText.kt +++ b/app/src/main/java/to/bitkit/models/ToastText.kt @@ -9,12 +9,15 @@ import androidx.compose.ui.res.stringResource @Stable sealed interface ToastText { @JvmInline - @Stable value class Resource(@StringRes val resId: Int) : ToastText @JvmInline - @Stable value class Literal(val value: String) : ToastText + + companion object { + operator fun invoke(value: String): ToastText = Literal(value) + operator fun invoke(@StringRes resId: Int): ToastText = Resource(resId) + } } @Composable diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 2b0bcd07c..37a65fcb8 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -30,15 +30,15 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { toaster.success( - titleRes = R.string.other__connection_back_title, - bodyRes = R.string.other__connection_back_msg, + title = R.string.other__connection_back_title, + body = R.string.other__connection_back_msg, ) } ConnectivityState.DISCONNECTED -> { - toaster.warning( - titleRes = R.string.other__connection_issue, - bodyRes = R.string.other__connection_issue_explain, + toaster.warn( + title = R.string.other__connection_issue, + body = R.string.other__connection_issue_explain, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index ea11d4f30..537ddff7b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -42,7 +42,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - toaster.error(titleRes = R.string.security__mnemonic_load_error) + toaster.error(title = R.string.security__mnemonic_load_error) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index 0612d4dd6..df55fd3fc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -74,8 +74,8 @@ class RecoveryViewModel @Inject constructor( ) } toaster.error( - titleRes = R.string.common__error, - bodyRes = R.string.other__logs_export_error, + title = R.string.common__error, + body = R.string.other__logs_export_error, ) } ) @@ -98,8 +98,8 @@ class RecoveryViewModel @Inject constructor( Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { toaster.error( - titleRes = R.string.common__error, - bodyRes = R.string.settings__support__link_error, + title = R.string.common__error, + body = R.string.settings__support__link_error, ) } } @@ -120,8 +120,8 @@ class RecoveryViewModel @Inject constructor( toaster.error(error) }.onSuccess { toaster.success( - titleRes = R.string.security__wiped_title, - bodyRes = R.string.security__wiped_message, + title = R.string.security__wiped_title, + body = R.string.security__wiped_message, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 0490be66d..860d19bea 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -151,8 +151,8 @@ fun QrScanningScreen( Logger.error("Failed to scan QR code", error) app.toast( type = ToastType.ERROR, - titleRes = R.string.other__qr_error_header, - bodyRes = R.string.other__qr_error_text, + title = R.string.other__qr_error_header, + body = R.string.other__qr_error_text, ) } } @@ -257,8 +257,8 @@ private fun handlePaste( if (clipboard.isNullOrBlank()) { app.toast( type = ToastType.WARNING, - titleRes = R.string.wallet__send_clipboard_empty_title, - bodyRes = R.string.wallet__send_clipboard_empty_text, + title = R.string.wallet__send_clipboard_empty_title, + body = R.string.wallet__send_clipboard_empty_text, ) } setScanResult(clipboard) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 089241e90..0114cb4cf 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -70,8 +70,8 @@ fun SavingsProgressScreen( // All channels are trusted peers - show error and navigate back immediately app.toast( type = ToastType.ERROR, - titleRes = R.string.lightning__close_error, - bodyRes = R.string.lightning__close_error_msg, + title = R.string.lightning__close_error, + body = R.string.lightning__close_error_msg, ) onTransferUnavailable() } else { @@ -81,8 +81,8 @@ fun SavingsProgressScreen( onTransferUnavailable = { app.toast( type = ToastType.ERROR, - titleRes = R.string.lightning__close_error, - bodyRes = R.string.lightning__close_error_msg, + title = R.string.lightning__close_error, + body = R.string.lightning__close_error_msg, ) onTransferUnavailable() }, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 329be3aa7..cc3aca148 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -75,8 +75,8 @@ class ExternalNodeViewModel @Inject constructor( setEffect(SideEffect.ConnectionSuccess) } else { toaster.error( - titleRes = R.string.lightning__error_add_title, - bodyRes = R.string.lightning__error_add, + title = R.string.lightning__error_add_title, + body = R.string.lightning__error_add, ) } } @@ -89,7 +89,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - toaster.error(titleRes = R.string.lightning__error_add_uri) + toaster.error(title = R.string.lightning__error_add_uri) } } } @@ -136,8 +136,8 @@ class ExternalNodeViewModel @Inject constructor( val feeRate = _uiState.value.customFeeRate ?: 0u if (feeRate == 0u) { toaster.info( - titleRes = R.string.wallet__min_possible_fee_rate, - bodyRes = R.string.wallet__min_possible_fee_rate_msg, + title = R.string.wallet__min_possible_fee_rate, + body = R.string.wallet__min_possible_fee_rate_msg, ) return false } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 40ac52756..ce08fb56d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -70,8 +70,8 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { toaster.success( - titleRes = R.string.other__lnurl_channel_success_title, - bodyRes = R.string.other__lnurl_channel_success_msg_no_peer, + title = R.string.other__lnurl_channel_success_title, + body = R.string.other__lnurl_channel_success_msg_no_peer, ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index e0e4abd82..e50ef2593 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -251,8 +251,8 @@ fun ActivityDetailScreen( onSuccess = { app.toast( type = ToastType.SUCCESS, - titleRes = R.string.wallet__boost_success_title, - bodyRes = R.string.wallet__boost_success_msg, + title = R.string.wallet__boost_success_title, + body = R.string.wallet__boost_success_msg, testTag = "BoostSuccessToast" ) listViewModel.resync() @@ -261,8 +261,8 @@ fun ActivityDetailScreen( onFailure = { app.toast( type = ToastType.ERROR, - titleRes = R.string.wallet__boost_error_title, - bodyRes = R.string.wallet__boost_error_msg, + title = R.string.wallet__boost_error_title, + body = R.string.wallet__boost_error_msg, testTag = "BoostFailureToast" ) detailViewModel.onDismissBoostSheet() @@ -270,15 +270,15 @@ fun ActivityDetailScreen( onMaxFee = { app.toast( type = ToastType.ERROR, - titleRes = R.string.wallet__send_fee_error, - bodyRes = R.string.wallet__send_fee_error_max, + title = R.string.wallet__send_fee_error, + body = R.string.wallet__send_fee_error_max, ) }, onMinFee = { app.toast( type = ToastType.ERROR, - titleRes = R.string.wallet__send_fee_error, - bodyRes = R.string.wallet__send_fee_error_min, + title = R.string.wallet__send_fee_error, + body = R.string.wallet__send_fee_error_min, ) } ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index 3d44046e8..105bafe93 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -106,16 +106,16 @@ class SendFeeViewModel @Inject constructor( val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { toaster.info( - titleRes = R.string.wallet__min_possible_fee_rate, - bodyRes = R.string.wallet__min_possible_fee_rate_msg, + title = R.string.wallet__min_possible_fee_rate, + body = R.string.wallet__min_possible_fee_rate_msg, ) return false } if (satsPerVByte > maxSatsPerVByte) { toaster.info( - titleRes = R.string.wallet__max_possible_fee_rate, - bodyRes = R.string.wallet__max_possible_fee_rate_msg, + title = R.string.wallet__max_possible_fee_rate, + body = R.string.wallet__max_possible_fee_rate_msg, ) return false } diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index ac1c19c2d..c864549e0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -85,8 +85,8 @@ fun SettingsScreen( } app.toast( type = ToastType.SUCCESS, - titleRes = titleRes, - bodyRes = bodyRes, + title = titleRes, + body = bodyRes, ) enableDevModeTapCount = 0 } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 57b675d30..ae98fc792 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -88,8 +88,8 @@ fun ElectrumConfigScreen( } else { app.toast( type = ToastType.WARNING, - titleRes = R.string.settings__es__server_error, - bodyRes = R.string.settings__es__server_error_description, + title = R.string.settings__es__server_error, + body = R.string.settings__es__server_error_description, testTag = "ElectrumErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index bb189f8b5..a43d52f99 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -247,7 +247,7 @@ class ElectrumConfigViewModel @Inject constructor( viewModelScope.launch { val validationError = validateInput() if (validationError != null) { - toaster.warning( + toaster.warn( title = ToastText.Resource(R.string.settings__es__error_peer), body = ToastText.Literal(validationError), ) @@ -268,7 +268,7 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput(host, port) if (validationError != null) { - toaster.warning( + toaster.warn( title = ToastText.Resource(R.string.settings__es__error_peer), body = ToastText.Literal(validationError), ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 27962dde2..1c1c11f8c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -68,8 +68,8 @@ fun RgsServerScreen( if (result.isSuccess) { app.toast( type = ToastType.SUCCESS, - titleRes = R.string.settings__rgs__update_success_title, - bodyRes = R.string.settings__rgs__update_success_description, + title = R.string.settings__rgs__update_success_title, + body = R.string.settings__rgs__update_success_description, testTag = "RgsUpdatedToast", ) } else { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index e50f624f3..a5e6b80ff 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -86,9 +86,9 @@ class BackupNavSheetViewModel @Inject constructor( } }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) - toaster.warning( - titleRes = R.string.security__mnemonic_error, - bodyRes = R.string.security__mnemonic_error_description, + toaster.warn( + title = R.string.security__mnemonic_error, + body = R.string.security__mnemonic_error_description, ) } } @@ -157,8 +157,8 @@ class BackupNavSheetViewModel @Inject constructor( fun onMnemonicCopied() { viewModelScope.launch { toaster.success( - titleRes = R.string.common__copied, - bodyRes = R.string.security__mnemonic_copied, + title = R.string.common__copied, + body = R.string.security__mnemonic_copied, ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 52f728917..dd5e4c670 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -339,9 +339,9 @@ class LightningConnectionsViewModel @Inject constructor( logsRepo.zipLogsForSharing() .onSuccess { uri -> onReady(uri) } .onFailure { - toaster.warning( - titleRes = R.string.lightning__error_logs, - bodyRes = R.string.lightning__error_logs_description, + toaster.warn( + title = R.string.lightning__error_logs, + body = R.string.lightning__error_logs_description, ) } } @@ -454,8 +454,8 @@ class LightningConnectionsViewModel @Inject constructor( walletRepo.syncNodeAndWallet() toaster.success( - titleRes = R.string.lightning__close_success_title, - bodyRes = R.string.lightning__close_success_msg, + title = R.string.lightning__close_success_title, + body = R.string.lightning__close_success_msg, ) _closeConnectionUiState.update { @@ -468,9 +468,9 @@ class LightningConnectionsViewModel @Inject constructor( onFailure = { error -> Logger.error("Failed to close channel", e = error, context = TAG) - toaster.warning( - titleRes = R.string.lightning__close_error, - bodyRes = R.string.lightning__close_error_msg, + toaster.warn( + title = R.string.lightning__close_error, + body = R.string.lightning__close_error_msg, ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 23370f216..39b40bffa 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -41,57 +41,57 @@ class Toaster @Inject constructor() { // region @StringRes overloads suspend fun success( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.SUCCESS, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag ) suspend fun info( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.INFO, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag, ) suspend fun lightning( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.LIGHTNING, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag ) - suspend fun warning( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + suspend fun warn( + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.WARNING, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag ) suspend fun error( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.ERROR, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag, ) // endregion @@ -115,7 +115,7 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) - suspend fun warning( + suspend fun warn( title: ToastText, body: ToastText? = null, testTag: String? = null, @@ -162,7 +162,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun warning( + suspend fun warn( title: String, body: String? = null, testTag: String? = null, diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 3e3c8d9e4..c31098478 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1817,15 +1817,15 @@ class AppViewModel @Inject constructor( fun toast( type: ToastType, - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, autoHide: Boolean = true, duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) = toast( type = type, - title = ToastText.Resource(titleRes), - body = bodyRes?.let { ToastText.Resource(it) }, + title = ToastText.Resource(title), + body = body?.let { ToastText.Resource(it) }, autoHide = autoHide, duration = duration, testTag = testTag, diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index c5e0a4ad2..3e8733923 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -47,7 +47,7 @@ class DevSettingsViewModel @Inject constructor( val peer = lightningRepo.getPeers()?.firstOrNull() if (peer == null) { - toaster.warning("No peer connected") + toaster.warn("No peer connected") return@launch } @@ -72,7 +72,7 @@ class DevSettingsViewModel @Inject constructor( ) toaster.info("LSP notification sent to this device") }.onFailure { - toaster.warning("Error testing LSP notification") + toaster.warn("Error testing LSP notification") } } @@ -99,7 +99,7 @@ class DevSettingsViewModel @Inject constructor( logsRepo.zipLogsForSharing() .onSuccess { uri -> onReady(uri) } .onFailure { - toaster.warning( + toaster.warn( context.getString(R.string.lightning__error_logs), context.getString(R.string.lightning__error_logs_description), ) diff --git a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt index 1ade94f06..3121e8577 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt @@ -43,7 +43,7 @@ class LdkDebugViewModel @Inject constructor( fun addPeer() { val uri = _uiState.value.nodeUri.trim() if (uri.isEmpty()) { - viewModelScope.launch { toaster.warning("Please enter a node URI") } + viewModelScope.launch { toaster.warn("Please enter a node URI") } return } connectPeer(uri) @@ -55,7 +55,7 @@ class LdkDebugViewModel @Inject constructor( val pastedUri = clipData?.getItemAt(0)?.text?.toString()?.trim() if (pastedUri.isNullOrEmpty()) { - viewModelScope.launch { toaster.warning("Clipboard is empty") } + viewModelScope.launch { toaster.warn("Clipboard is empty") } return } @@ -99,7 +99,7 @@ class LdkDebugViewModel @Inject constructor( _uiState.update { it.copy(networkGraphInfo = info) } toaster.info("Network graph info logged") } else { - toaster.warning("Failed to get network graph info") + toaster.warn("Failed to get network graph info") } } } @@ -162,7 +162,7 @@ class LdkDebugViewModel @Inject constructor( } toaster.info("Deleted key: $key") } else { - toaster.warning("Key not found: $key") + toaster.warn("Key not found: $key") } } .onFailure { e -> diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 705089029..4670bdcd5 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -460,8 +460,8 @@ class TransferViewModel @Inject constructor( channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) toaster.error( - titleRes = R.string.lightning__force_failed_title, - bodyRes = R.string.lightning__force_failed_msg, + title = R.string.lightning__force_failed_title, + body = R.string.lightning__force_failed_msg, ) return@runCatching } @@ -490,15 +490,15 @@ class TransferViewModel @Inject constructor( } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) toaster.error( - titleRes = R.string.lightning__force_failed_title, - bodyRes = R.string.lightning__force_failed_msg, + title = R.string.lightning__force_failed_title, + body = R.string.lightning__force_failed_msg, ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) toaster.error( - titleRes = R.string.lightning__force_failed_title, - bodyRes = R.string.lightning__force_failed_msg, + title = R.string.lightning__force_failed_title, + body = R.string.lightning__force_failed_msg, ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 56eaca2a2..78e7b1921 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -128,8 +128,8 @@ class WalletViewModel @Inject constructor( migrationService.markMigrationChecked() migrationService.setShowingMigrationLoading(false) toaster.error( - titleRes = R.string.wallet__migration_error_title, - bodyRes = R.string.wallet__migration_error_body, + title = R.string.wallet__migration_error_title, + body = R.string.wallet__migration_error_body, ) } } @@ -302,8 +302,8 @@ class WalletViewModel @Inject constructor( lightningRepo.disconnectPeer(peer) .onSuccess { toaster.info( - titleRes = R.string.common__success, - bodyRes = R.string.wallet__peer_disconnected, + title = R.string.common__success, + body = R.string.wallet__peer_disconnected, ) } .onFailure { From bb124a6d874207798cafa0e6867cf69e9d4f8e54 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:17:20 +0100 Subject: [PATCH 17/21] refactor: use ToastText() factory constructor Replace ToastText.Literal() and ToastText.Resource() calls with the cleaner ToastText() factory constructor. Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 4 +- .../java/to/bitkit/ui/components/ToastView.kt | 20 ++++---- .../external/ExternalNodeViewModel.kt | 8 ++-- .../external/LnurlChannelViewModel.kt | 4 +- .../settings/advanced/AddressViewerScreen.kt | 4 +- .../settings/advanced/ElectrumConfigScreen.kt | 4 +- .../advanced/ElectrumConfigViewModel.kt | 8 ++-- .../ui/settings/advanced/RgsServerScreen.kt | 4 +- .../settings/lightning/ChannelDetailScreen.kt | 4 +- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 46 +++++++++---------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 14 +++--- .../to/bitkit/viewmodels/TransferViewModel.kt | 4 +- .../to/bitkit/viewmodels/WalletViewModel.kt | 8 ++-- 13 files changed, 66 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 2317fe37a..719bf5130 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -93,8 +93,8 @@ fun NodeInfoScreen( onCopy = { text -> app.toast( type = ToastType.SUCCESS, - title = ToastText.Resource(R.string.common__copied), - body = ToastText.Literal(text), + title = ToastText(R.string.common__copied), + body = ToastText(text), ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index 98f89fc0d..f4389c030 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -309,8 +309,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.WARNING, - title = ToastText.Literal("You're still offline"), - body = ToastText.Literal("Check your connection to keep using Bitkit."), + title = ToastText("You're still offline"), + body = ToastText("Check your connection to keep using Bitkit."), autoHide = true, ), onDismiss = {}, @@ -318,8 +318,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.LIGHTNING, - title = ToastText.Literal("Instant Payments Ready"), - body = ToastText.Literal("You can now pay anyone, anywhere, instantly."), + title = ToastText("Instant Payments Ready"), + body = ToastText("You can now pay anyone, anywhere, instantly."), autoHide = true, ), onDismiss = {}, @@ -327,8 +327,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.SUCCESS, - title = ToastText.Literal("You're Back Online!"), - body = ToastText.Literal("Successfully reconnected to the Internet."), + title = ToastText("You're Back Online!"), + body = ToastText("Successfully reconnected to the Internet."), autoHide = true, ), onDismiss = {}, @@ -336,8 +336,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.INFO, - title = ToastText.Literal("General Message"), - body = ToastText.Literal("Used for neutral content to inform the user."), + title = ToastText("General Message"), + body = ToastText("Used for neutral content to inform the user."), autoHide = false, ), onDismiss = {}, @@ -345,8 +345,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.ERROR, - title = ToastText.Literal("Error Toast"), - body = ToastText.Literal("This is a toast message."), + title = ToastText("Error Toast"), + body = ToastText("This is a toast message."), autoHide = true, ), onDismiss = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index cc3aca148..1df49579a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -100,8 +100,8 @@ class ExternalNodeViewModel @Inject constructor( if (sats > maxAmount) { viewModelScope.launch { toaster.error( - title = ToastText.Resource(R.string.lightning__spending_amount__error_max__title), - body = ToastText.Literal( + title = ToastText(R.string.lightning__spending_amount__error_max__title), + body = ToastText( context.getString(R.string.lightning__spending_amount__error_max__description) .replace("{amount}", maxAmount.formatToModernDisplay()) ), @@ -191,8 +191,8 @@ class ExternalNodeViewModel @Inject constructor( val error = e.message.orEmpty() Logger.warn("Error opening channel with peer: '${_uiState.value.peer}': '$error'") toaster.error( - title = ToastText.Resource(R.string.lightning__error_channel_purchase), - body = ToastText.Literal( + title = ToastText(R.string.lightning__error_channel_purchase), + body = ToastText( context.getString(R.string.lightning__error_channel_setup_msg) .replace("{raw}", error) ), diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index ce08fb56d..ccb7ef4ad 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -84,8 +84,8 @@ class LnurlChannelViewModel @Inject constructor( suspend fun errorToast(error: Throwable) { toaster.error( - title = ToastText.Resource(R.string.other__lnurl_channel_error), - body = ToastText.Literal(error.message ?: context.getString(R.string.common__error_body)), + title = ToastText(R.string.other__lnurl_channel_error), + body = ToastText(error.message ?: context.getString(R.string.common__error_body)), ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 58ff99f8c..55d8719a7 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -80,8 +80,8 @@ fun AddressViewerScreen( context.setClipboardText(text) app.toast( type = ToastType.SUCCESS, - title = ToastText.Resource(R.string.common__copied), - body = ToastText.Literal(text), + title = ToastText(R.string.common__copied), + body = ToastText(text), ) } ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index ae98fc792..e188903ba 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -77,8 +77,8 @@ fun ElectrumConfigScreen( if (result.isSuccess) { app.toast( type = ToastType.SUCCESS, - title = ToastText.Resource(R.string.settings__es__server_updated_title), - body = ToastText.Literal( + title = ToastText(R.string.settings__es__server_updated_title), + body = ToastText( context.getString(R.string.settings__es__server_updated_message) .replace("{host}", uiState.host) .replace("{port}", uiState.port) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index a43d52f99..c3b53c322 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -248,8 +248,8 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput() if (validationError != null) { toaster.warn( - title = ToastText.Resource(R.string.settings__es__error_peer), - body = ToastText.Literal(validationError), + title = ToastText(R.string.settings__es__error_peer), + body = ToastText(validationError), ) } else { connectToServer() @@ -269,8 +269,8 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput(host, port) if (validationError != null) { toaster.warn( - title = ToastText.Resource(R.string.settings__es__error_peer), - body = ToastText.Literal(validationError), + title = ToastText(R.string.settings__es__error_peer), + body = ToastText(validationError), ) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 1c1c11f8c..b5eeccd4d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -75,8 +75,8 @@ fun RgsServerScreen( } else { app.toast( type = ToastType.ERROR, - title = ToastText.Resource(R.string.wallet__ldk_start_error_title), - body = ToastText.Literal(result.exceptionOrNull()?.message ?: "Unknown error"), + title = ToastText(R.string.wallet__ldk_start_error_title), + body = ToastText(result.exceptionOrNull()?.message ?: "Unknown error"), testTag = "RgsErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index ca53e3928..afd3bc17d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -131,8 +131,8 @@ fun ChannelDetailScreen( context.setClipboardText(text) app.toast( type = ToastType.SUCCESS, - title = ToastText.Resource(R.string.common__copied), - body = ToastText.Literal(text), + title = ToastText(R.string.common__copied), + body = ToastText(text), ) }, onOpenUrl = { txId -> diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 39b40bffa..8260ee841 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -46,8 +46,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.SUCCESS, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag ) @@ -57,8 +57,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.INFO, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -68,8 +68,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.LIGHTNING, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag ) @@ -79,8 +79,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.WARNING, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag ) @@ -90,8 +90,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.ERROR, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) // endregion @@ -135,8 +135,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.SUCCESS, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -146,8 +146,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.INFO, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -157,8 +157,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.LIGHTNING, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -168,8 +168,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.WARNING, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -179,16 +179,16 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.ERROR, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, - title = ToastText.Resource(R.string.common__error), - body = throwable.message?.let { ToastText.Literal(it) } - ?: ToastText.Resource(R.string.common__error_body), + title = ToastText(R.string.common__error), + body = throwable.message?.let { ToastText(it) } + ?: ToastText(R.string.common__error_body), ) // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index c31098478..109635996 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1824,8 +1824,8 @@ class AppViewModel @Inject constructor( testTag: String? = null, ) = toast( type = type, - title = ToastText.Resource(title), - body = body?.let { ToastText.Resource(it) }, + title = ToastText(title), + body = body?.let { ToastText(it) }, autoHide = autoHide, duration = duration, testTag = testTag, @@ -1840,8 +1840,8 @@ class AppViewModel @Inject constructor( testTag: String? = null, ) = toast( type = type, - title = ToastText.Literal(title), - body = body?.let { ToastText.Literal(it) }, + title = ToastText(title), + body = body?.let { ToastText(it) }, autoHide = autoHide, duration = duration, testTag = testTag, @@ -1850,9 +1850,9 @@ class AppViewModel @Inject constructor( fun toast(error: Throwable) { toast( type = ToastType.ERROR, - title = ToastText.Resource(R.string.common__error), - body = error.message?.let { ToastText.Literal(it) } - ?: ToastText.Resource(R.string.common__error_body) + title = ToastText(R.string.common__error), + body = error.message?.let { ToastText(it) } + ?: ToastText(R.string.common__error_body) ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 4670bdcd5..996260567 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -484,8 +484,8 @@ class TransferViewModel @Inject constructor( val skippedMsg = context.getString(R.string.lightning__force_channels_skipped) val bodyText = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg toaster.lightning( - title = ToastText.Resource(R.string.lightning__force_init_title), - body = ToastText.Literal(bodyText), + title = ToastText(R.string.lightning__force_init_title), + body = ToastText(bodyText), ) } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 78e7b1921..76dbcb3cf 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -308,8 +308,8 @@ class WalletViewModel @Inject constructor( } .onFailure { toaster.error( - title = ToastText.Resource(R.string.common__error), - body = ToastText.Literal(it.message ?: context.getString(R.string.common__error_body)), + title = ToastText(R.string.common__error), + body = ToastText(it.message ?: context.getString(R.string.common__error_body)), ) } } @@ -318,8 +318,8 @@ class WalletViewModel @Inject constructor( fun updateBip21Invoice(amountSats: ULong? = walletState.value.bip21AmountSats) = viewModelScope.launch { walletRepo.updateBip21Invoice(amountSats).onFailure { error -> toaster.error( - title = ToastText.Resource(R.string.wallet__error_invoice_update), - body = ToastText.Literal(error.message ?: context.getString(R.string.common__error_body)), + title = ToastText(R.string.wallet__error_invoice_update), + body = ToastText(error.message ?: context.getString(R.string.common__error_body)), ) } } From fbe9826762f207c662752f3416ced5b48d9115ce Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:31:19 +0100 Subject: [PATCH 18/21] refactor: make Toaster methods non-suspend - Change emit() to use tryEmit() on SharedFlow - Remove suspend modifier from all toast methods - Make LocalToaster non-nullable with error default Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/Locals.kt | 4 +-- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 36 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/Locals.kt b/app/src/main/java/to/bitkit/ui/Locals.kt index 89688e9eb..e381b78b1 100644 --- a/app/src/main/java/to/bitkit/ui/Locals.kt +++ b/app/src/main/java/to/bitkit/ui/Locals.kt @@ -30,7 +30,7 @@ val LocalActivityListViewModel = staticCompositionLocalOf { null } val LocalSettingsViewModel = staticCompositionLocalOf { null } val LocalBackupsViewModel = staticCompositionLocalOf { null } -val LocalToaster = staticCompositionLocalOf { null } +val LocalToaster = staticCompositionLocalOf { error("Toaster not provided") } val appViewModel: AppViewModel? @Composable get() = LocalAppViewModel.current @@ -59,5 +59,5 @@ val backupsViewModel: BackupsViewModel? val drawerState: DrawerState? @Composable get() = LocalDrawerState.current -val toaster: Toaster? +val toaster: Toaster @Composable get() = LocalToaster.current diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 8260ee841..f41feccb6 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -19,7 +19,7 @@ class Toaster @Inject constructor() { val events: SharedFlow = _events.asSharedFlow() @Suppress("LongParameterList") - private suspend fun emit( + private fun emit( type: ToastType, title: ToastText, body: ToastText? = null, @@ -27,7 +27,7 @@ class Toaster @Inject constructor() { duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) { - _events.emit( + _events.tryEmit( Toast( type = type, title = title, @@ -40,7 +40,7 @@ class Toaster @Inject constructor() { } // region @StringRes overloads - suspend fun success( + fun success( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -51,7 +51,7 @@ class Toaster @Inject constructor() { testTag = testTag ) - suspend fun info( + fun info( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -62,7 +62,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun lightning( + fun lightning( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -73,7 +73,7 @@ class Toaster @Inject constructor() { testTag = testTag ) - suspend fun warn( + fun warn( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -84,7 +84,7 @@ class Toaster @Inject constructor() { testTag = testTag ) - suspend fun error( + fun error( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -97,31 +97,31 @@ class Toaster @Inject constructor() { // endregion // region ToastText overloads - suspend fun success( + fun success( title: ToastText, body: ToastText? = null, testTag: String? = null, ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) - suspend fun info( + fun info( title: ToastText, body: ToastText? = null, testTag: String? = null, ) = emit(ToastType.INFO, title, body, testTag = testTag) - suspend fun lightning( + fun lightning( title: ToastText, body: ToastText? = null, testTag: String? = null, ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) - suspend fun warn( + fun warn( title: ToastText, body: ToastText? = null, testTag: String? = null, ) = emit(ToastType.WARNING, title, body, testTag = testTag) - suspend fun error( + fun error( title: ToastText, body: ToastText? = null, testTag: String? = null, @@ -129,7 +129,7 @@ class Toaster @Inject constructor() { // endregion // region String literal overloads - suspend fun success( + fun success( title: String, body: String? = null, testTag: String? = null, @@ -140,7 +140,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun info( + fun info( title: String, body: String? = null, testTag: String? = null, @@ -151,7 +151,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun lightning( + fun lightning( title: String, body: String? = null, testTag: String? = null, @@ -162,7 +162,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun warn( + fun warn( title: String, body: String? = null, testTag: String? = null, @@ -173,7 +173,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun error( + fun error( title: String, body: String? = null, testTag: String? = null, @@ -184,7 +184,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun error(throwable: Throwable) = emit( + fun error(throwable: Throwable) = emit( type = ToastType.ERROR, title = ToastText(R.string.common__error), body = throwable.message?.let { ToastText(it) } From 22a08391aff1ce62175868152013d90bdd5f840b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:40:32 +0100 Subject: [PATCH 19/21] refactor: migrate external app.toast calls to toaster Replace all app.toast() and appViewModel.toast() calls in Screen composables with direct toaster method calls. Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/ContentView.kt | 6 ++-- .../main/java/to/bitkit/ui/MainActivity.kt | 6 ++-- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 6 ++-- .../ui/screens/scanner/QrScanningScreen.kt | 25 ++++++++--------- .../ui/screens/settings/DevSettingsScreen.kt | 27 +++++++++--------- .../screens/transfer/SavingsProgressScreen.kt | 9 +++--- .../transfer/SpendingAdvancedScreen.kt | 10 +++---- .../wallets/activity/ActivityDetailScreen.kt | 20 +++++-------- .../wallets/activity/ActivityExploreScreen.kt | 8 ++---- .../wallets/receive/ReceiveAmountScreen.kt | 6 ++-- .../ui/settings/BlocktankRegtestScreen.kt | 28 +++++++------------ .../to/bitkit/ui/settings/SettingsScreen.kt | 8 ++---- .../settings/advanced/AddressViewerScreen.kt | 8 ++---- .../settings/advanced/ElectrumConfigScreen.kt | 11 +++----- .../ui/settings/advanced/RgsServerScreen.kt | 11 +++----- .../settings/lightning/ChannelDetailScreen.kt | 8 ++---- 16 files changed, 79 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 9d9b77290..43566bd51 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -46,7 +46,6 @@ import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import to.bitkit.env.Env import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.ToastType import to.bitkit.models.WidgetType import to.bitkit.ui.Routes.ExternalConnection import to.bitkit.ui.components.AuthCheckScreen @@ -626,10 +625,9 @@ private fun RootNavHost( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, - toastException = { appViewModel.toast(it) }, + toastException = { appViewModel.toaster.error(it) }, toast = { title, body -> - appViewModel.toast( - type = ToastType.ERROR, + appViewModel.toaster.error( title = title, body = body ) diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 52bd312a1..9fc9b9030 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -265,7 +265,7 @@ private fun OnboardingNav( walletViewModel.setInitNodeLifecycleState() walletViewModel.createWallet(bip39Passphrase = null) }.onFailure { - appViewModel.toast(it) + appViewModel.toaster.error(it) } } }, @@ -295,7 +295,7 @@ private fun OnboardingNav( appViewModel.resetIsAuthenticatedState() walletViewModel.restoreWallet(mnemonic, passphrase) }.onFailure { - appViewModel.toast(it) + appViewModel.toaster.error(it) } } } @@ -310,7 +310,7 @@ private fun OnboardingNav( appViewModel.resetIsAuthenticatedState() walletViewModel.createWallet(bip39Passphrase = passphrase) }.onFailure { - appViewModel.toast(it) + appViewModel.toaster.error(it) } } }, diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 719bf5130..67eafc6c4 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -47,7 +47,6 @@ import to.bitkit.ext.formatToString import to.bitkit.ext.uri import to.bitkit.models.NodeLifecycleState import to.bitkit.models.ToastText -import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.LightningState import to.bitkit.ui.components.BodyM @@ -76,7 +75,7 @@ fun NodeInfoScreen( navController: NavController, ) { val wallet = walletViewModel ?: return - val app = appViewModel ?: return + val toaster = toaster val settings = settingsViewModel ?: return val isRefreshing by wallet.isRefreshing.collectAsStateWithLifecycle() @@ -91,8 +90,7 @@ fun NodeInfoScreen( onRefresh = { wallet.onPullToRefresh() }, onDisconnectPeer = { wallet.disconnectPeer(it) }, onCopy = { text -> - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = ToastText(R.string.common__copied), body = ToastText(text), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 860d19bea..4cda47e6d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -65,8 +65,6 @@ import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.getClipboardText import to.bitkit.ext.startActivityAppSettings -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput @@ -74,10 +72,11 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.utils.Logger -import to.bitkit.viewmodels.AppViewModel import java.util.concurrent.Executors const val SCAN_REQUEST_KEY = "SCAN_REQUEST" @@ -93,7 +92,7 @@ fun QrScanningScreen( onBack: () -> Unit = { navController.popBackStack() }, onScanSuccess: (String) -> Unit, ) { - val app = appViewModel ?: return + val toaster = toaster val (scanResult, setScanResult) = remember { mutableStateOf(null) } @@ -140,7 +139,7 @@ fun QrScanningScreen( val context = LocalContext.current val previewView = remember { PreviewView(context) } val preview = remember { Preview.Builder().build() } - val analyzer = remember { + val analyzer = remember(toaster) { QrCodeAnalyzer { result -> if (result.isSuccess) { val qrCode = result.getOrThrow() @@ -149,8 +148,7 @@ fun QrScanningScreen( } else { val error = requireNotNull(result.exceptionOrNull()) Logger.error("Failed to scan QR code", error) - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.other__qr_error_header, body = R.string.other__qr_error_text, ) @@ -166,12 +164,12 @@ fun QrScanningScreen( val galleryLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent(), onResult = { uri -> - uri?.let { processImageFromGallery(context, it, setScanResult, onError = { e -> app.toast(e) }) } + uri?.let { processImageFromGallery(context, it, setScanResult, onError = { e -> toaster.error(e) }) } } ) val pickMedia = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - uri?.let { processImageFromGallery(context, it, setScanResult, onError = { e -> app.toast(e) }) } + uri?.let { processImageFromGallery(context, it, setScanResult, onError = { e -> toaster.error(e) }) } } LaunchedEffect(lensFacing) { @@ -210,7 +208,7 @@ fun QrScanningScreen( context.startActivityAppSettings() }, onClickRetry = cameraPermissionState::launchPermissionRequest, - onClickPaste = handlePaste(context, app, setScanResult), + onClickPaste = handlePaste(context, toaster, setScanResult), onBack = onBack, ) }, @@ -239,7 +237,7 @@ fun QrScanningScreen( galleryLauncher.launch("image/*") } }, - onPasteFromClipboard = handlePaste(context, app, setScanResult), + onPasteFromClipboard = handlePaste(context, toaster, setScanResult), onSubmitDebug = setScanResult, ) } @@ -250,13 +248,12 @@ fun QrScanningScreen( @Composable private fun handlePaste( context: Context, - app: AppViewModel, + toaster: Toaster, setScanResult: (String?) -> Unit, ): () -> Unit = { val clipboard = context.getClipboardText()?.trim() if (clipboard.isNullOrBlank()) { - app.toast( - type = ToastType.WARNING, + toaster.warn( title = R.string.wallet__send_clipboard_empty_title, body = R.string.wallet__send_clipboard_empty_text, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index c93bc2338..eb600dc5b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -14,10 +14,8 @@ import androidx.navigation.NavController import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env -import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.activityListViewModel -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsTextButtonRow @@ -26,6 +24,7 @@ import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.util.shareZipFile +import to.bitkit.ui.toaster import to.bitkit.viewmodels.DevSettingsViewModel @Composable @@ -33,7 +32,7 @@ fun DevSettingsScreen( navController: NavController, viewModel: DevSettingsViewModel = hiltViewModel(), ) { - val app = appViewModel ?: return + val toaster = toaster val activity = activityListViewModel ?: return val settings = settingsViewModel ?: return val context = LocalContext.current @@ -78,63 +77,63 @@ fun DevSettingsScreen( title = "Reset Settings State", onClick = { settings.reset() - app.toast(type = ToastType.SUCCESS, title = "Settings state reset") + toaster.success(title = "Settings state reset") } ) SettingsTextButtonRow( title = "Reset All Activities", onClick = { activity.removeAllActivities() - app.toast(type = ToastType.SUCCESS, title = "Activities removed") + toaster.success(title = "Activities removed") } ) SettingsTextButtonRow( title = "Reset Backup State", onClick = { viewModel.resetBackupState() - app.toast(type = ToastType.SUCCESS, title = "Backup state reset") + toaster.success(title = "Backup state reset") } ) SettingsTextButtonRow( title = "Reset Widgets State", onClick = { viewModel.resetWidgetsState() - app.toast(type = ToastType.SUCCESS, title = "Widgets state reset") + toaster.success(title = "Widgets state reset") } ) SettingsTextButtonRow( title = "Refresh Currency Rates", onClick = { viewModel.refreshCurrencyRates() - app.toast(type = ToastType.SUCCESS, title = "Currency rates refreshed") + toaster.success(title = "Currency rates refreshed") } ) SettingsTextButtonRow( title = "Reset App Database", onClick = { viewModel.resetDatabase() - app.toast(type = ToastType.SUCCESS, title = "Database state reset") + toaster.success(title = "Database state reset") } ) SettingsTextButtonRow( title = "Reset Blocktank State", onClick = { viewModel.resetBlocktankState() - app.toast(type = ToastType.SUCCESS, title = "Blocktank state reset") + toaster.success(title = "Blocktank state reset") } ) SettingsTextButtonRow( title = "Reset Cache Store", onClick = { viewModel.resetCacheStore() - app.toast(type = ToastType.SUCCESS, title = "Cache store reset") + toaster.success(title = "Cache store reset") } ) SettingsTextButtonRow( title = "Wipe App", onClick = { viewModel.wipeWallet() - app.toast(type = ToastType.SUCCESS, title = "Wallet wiped") + toaster.success(title = "Wallet wiped") } ) @@ -145,14 +144,14 @@ fun DevSettingsScreen( onClick = { val count = 100 activity.generateRandomTestData(count) - app.toast(type = ToastType.SUCCESS, title = "Generated $count test activities") + toaster.success(title = "Generated $count test activities") } ) SettingsTextButtonRow( "Fake New BG Receive", onClick = { viewModel.fakeBgReceive() - app.toast(type = ToastType.INFO, title = "Restart app to see the payment received sheet") + toaster.info(title = "Restart app to see the payment received sheet") } ) SettingsTextButtonRow( diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 0114cb4cf..c8946335f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import to.bitkit.R -import to.bitkit.models.ToastType import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton @@ -36,6 +35,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.transfer.components.TransferAnimationView import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.removeAccentTags import to.bitkit.ui.utils.withAccent import to.bitkit.ui.utils.withAccentBoldBright @@ -51,6 +51,7 @@ fun SavingsProgressScreen( onContinueClick: () -> Unit = {}, onTransferUnavailable: () -> Unit = {}, ) { + val toaster = toaster var progressState by remember { mutableStateOf(SavingsProgressState.PROGRESS) } // Effect to close channels & update UI @@ -68,8 +69,7 @@ fun SavingsProgressScreen( if (nonTrustedChannels.isEmpty()) { // All channels are trusted peers - show error and navigate back immediately - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.lightning__close_error, body = R.string.lightning__close_error_msg, ) @@ -79,8 +79,7 @@ fun SavingsProgressScreen( channels = nonTrustedChannels, onGiveUp = { app.showSheet(Sheet.ForceTransfer) }, onTransferUnavailable = { - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.lightning__close_error, body = R.string.lightning__close_error_msg, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index 5d76b4d1f..bd9da7188 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -26,10 +26,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ext.mockOrder -import to.bitkit.models.ToastType import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight @@ -45,6 +43,7 @@ import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.TransferEffect @@ -63,7 +62,7 @@ fun SpendingAdvancedScreen( amountInputViewModel: AmountInputViewModel = hiltViewModel(), ) { val currentOnOrderCreated by rememberUpdatedState(onOrderCreated) - val app = appViewModel ?: return + val toaster = toaster val state by viewModel.spendingUiState.collectAsStateWithLifecycle() val order = state.order ?: return val amountUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() @@ -85,13 +84,12 @@ fun SpendingAdvancedScreen( TransferEffect.OnOrderCreated -> currentOnOrderCreated() is TransferEffect.ToastException -> { isLoading = false - app.toast(effect.e) + toaster.error(effect.e) } is TransferEffect.ToastError -> { isLoading = false - app.toast( - type = ToastType.ERROR, + toaster.error( title = effect.title, body = effect.body, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index e50ef2593..ba0ce7d48 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -58,9 +58,7 @@ import to.bitkit.ext.toActivityItemDate import to.bitkit.ext.toActivityItemTime import to.bitkit.ext.totalValue import to.bitkit.models.FeeRate.Companion.getFeeShortDescription -import to.bitkit.models.ToastType import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BodySSB @@ -81,6 +79,7 @@ import to.bitkit.ui.sheets.BoostTransactionSheet import to.bitkit.ui.sheets.ComingSoonSheet import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.copyToClipboard import to.bitkit.ui.utils.getScreenTitleRes import to.bitkit.viewmodels.ActivityDetailViewModel @@ -163,7 +162,7 @@ fun ActivityDetailScreen( is ActivityDetailViewModel.ActivityLoadState.Success -> { val item = loadState.activity - val app = appViewModel ?: return@Box + val toaster = toaster val copyToastTitle = stringResource(R.string.common__copied) val tags by detailViewModel.tags.collectAsStateWithLifecycle() @@ -226,8 +225,7 @@ fun ActivityDetailScreen( isCpfpChild = isCpfpChild, boostTxDoesExist = boostTxDoesExist, onCopy = { text -> - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = copyToastTitle, body = text.ellipsisMiddle(40) ) @@ -249,8 +247,7 @@ fun ActivityDetailScreen( onDismiss = detailViewModel::onDismissBoostSheet, item = it, onSuccess = { - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = R.string.wallet__boost_success_title, body = R.string.wallet__boost_success_msg, testTag = "BoostSuccessToast" @@ -259,8 +256,7 @@ fun ActivityDetailScreen( onCloseClick() }, onFailure = { - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.wallet__boost_error_title, body = R.string.wallet__boost_error_msg, testTag = "BoostFailureToast" @@ -268,15 +264,13 @@ fun ActivityDetailScreen( detailViewModel.onDismissBoostSheet() }, onMaxFee = { - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.wallet__send_fee_error, body = R.string.wallet__send_fee_error_max, ) }, onMinFee = { - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.wallet__send_fee_error, body = R.string.wallet__send_fee_error_min, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 7de2fbf5a..362166753 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -45,9 +45,7 @@ import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue -import to.bitkit.models.ToastType import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Caption13Up @@ -59,6 +57,7 @@ import to.bitkit.ui.screens.wallets.activity.components.ActivityIcon import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.copyToClipboard import to.bitkit.ui.utils.getBlockExplorerUrl import to.bitkit.ui.utils.getScreenTitleRes @@ -131,7 +130,7 @@ fun ActivityExploreScreen( is ActivityDetailViewModel.ActivityLoadState.Success -> { val item = loadState.activity - val app = appViewModel ?: return@ScreenColumn + val toaster = toaster val context = LocalContext.current val txDetails by detailViewModel.txDetails.collectAsStateWithLifecycle() @@ -166,8 +165,7 @@ fun ActivityExploreScreen( txDetails = txDetails, boostTxDoesExist = boostTxDoesExist, onCopy = { text -> - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = toastMessage, body = text.ellipsisMiddle(40), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt index 15ecf20d8..d9ff266d7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt @@ -31,7 +31,6 @@ import to.bitkit.R import to.bitkit.models.NodeLifecycleState import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Caption13Up @@ -49,6 +48,7 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.walletViewModel import to.bitkit.utils.Logger import to.bitkit.viewmodels.AmountInputViewModel @@ -62,7 +62,7 @@ fun ReceiveAmountScreen( currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), ) { - val app = appViewModel ?: return + val toaster = toaster val wallet = walletViewModel ?: return val blocktank = blocktankViewModel ?: return val lightningState by wallet.lightningState.collectAsStateWithLifecycle() @@ -106,7 +106,7 @@ fun ReceiveAmountScreen( ) ) }.onFailure { e -> - app.toast(e) + toaster.error(e) Logger.error("Failed to create CJIT", e) } isCreatingInvoice = false diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index b0d3406d4..6f9ced42a 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -28,8 +28,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption import to.bitkit.ui.components.Caption13Up @@ -39,6 +37,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.walletViewModel import to.bitkit.utils.Logger @@ -50,7 +49,7 @@ fun BlocktankRegtestScreen( ) { val coroutineScope = rememberCoroutineScope() val wallet = walletViewModel ?: return - val app = appViewModel ?: return + val toaster = toaster val walletState by wallet.walletState.collectAsStateWithLifecycle() ScreenColumn { @@ -112,15 +111,13 @@ fun BlocktankRegtestScreen( val sats = depositAmount.toULongOrNull() ?: error("Invalid deposit amount: $depositAmount") val txId = viewModel.regtestDeposit(depositAddress, sats) Logger.debug("Deposit successful with txId: $txId") - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = "Success", body = "Deposit successful. TxID: $txId", ) }.onFailure { Logger.error("Deposit failed", it) - app.toast( - type = ToastType.ERROR, + toaster.error( title = "Failed to deposit", body = it.message.orEmpty(), ) @@ -158,15 +155,13 @@ fun BlocktankRegtestScreen( mineBlockCount.toUIntOrNull() ?: error("Invalid block count: $mineBlockCount") viewModel.regtestMine(count) Logger.debug("Successfully mined $count blocks") - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = "Success", body = "Successfully mined $count blocks", ) }.onFailure { Logger.error("Mining failed", it) - app.toast( - type = ToastType.ERROR, + toaster.error( title = "Failed to mine", body = it.message.orEmpty(), ) @@ -210,15 +205,13 @@ fun BlocktankRegtestScreen( val amount = if (paymentAmount.isEmpty()) null else paymentAmount.toULongOrNull() val paymentId = viewModel.regtestPay(paymentInvoice, amount) Logger.debug("Payment successful with ID: $paymentId") - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = "Success", body = "Payment successful. ID: $paymentId", ) }.onFailure { Logger.error("Payment failed", it) - app.toast( - type = ToastType.ERROR, + toaster.error( title = "Failed to pay invoice from LND", body = it.message.orEmpty(), ) @@ -277,14 +270,13 @@ fun BlocktankRegtestScreen( forceCloseAfterS = closeAfter, ) Logger.debug("Channel closed successfully with txId: $closingTxId") - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = "Success", body = "Channel closed. Closing TxID: $closingTxId" ) }.onFailure { Logger.error("Channel close failed", it) - app.toast(it) + toaster.error(it) } } }, diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index c864549e0..c18e72c28 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -25,9 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.models.ToastType import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.navigateToAboutSettings import to.bitkit.ui.navigateToAdvancedSettings @@ -41,6 +39,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.toaster private const val DEV_MODE_TAP_THRESHOLD = 5 @@ -48,7 +47,7 @@ private const val DEV_MODE_TAP_THRESHOLD = 5 fun SettingsScreen( navController: NavController, ) { - val app = appViewModel ?: return + val toaster = toaster val settings = settingsViewModel ?: return val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle() var enableDevModeTapCount by remember { mutableIntStateOf(0) } @@ -83,8 +82,7 @@ fun SettingsScreen( } else { R.string.settings__dev_disabled_message } - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = titleRes, body = bodyRes, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 55d8719a7..7cdb1a559 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -31,9 +31,7 @@ import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel import to.bitkit.models.ToastText -import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption @@ -50,6 +48,7 @@ import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.BlockExplorerType import to.bitkit.ui.utils.getBlockExplorerUrl @@ -58,8 +57,8 @@ fun AddressViewerScreen( navController: NavController, viewModel: AddressViewerViewModel = hiltViewModel(), ) { - val app = appViewModel ?: return val context = LocalContext.current + val toaster = toaster val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -78,8 +77,7 @@ fun AddressViewerScreen( onGenerateMoreAddresses = viewModel::loadMoreAddresses, onCopy = { text -> context.setClipboardText(text) - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = ToastText(R.string.common__copied), body = ToastText(text), ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index e188903ba..22fbf3422 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -32,8 +32,6 @@ import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer import to.bitkit.models.ToastText -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight @@ -50,6 +48,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster @Composable fun ElectrumConfigScreen( @@ -58,8 +57,8 @@ fun ElectrumConfigScreen( viewModel: ElectrumConfigViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val app = appViewModel ?: return val context = LocalContext.current + val toaster = toaster // Handle result from Scanner LaunchedEffect(savedStateHandle) { @@ -75,8 +74,7 @@ fun ElectrumConfigScreen( LaunchedEffect(uiState.connectionResult) { uiState.connectionResult?.let { result -> if (result.isSuccess) { - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = ToastText(R.string.settings__es__server_updated_title), body = ToastText( context.getString(R.string.settings__es__server_updated_message) @@ -86,8 +84,7 @@ fun ElectrumConfigScreen( testTag = "ElectrumUpdatedToast", ) } else { - app.toast( - type = ToastType.WARNING, + toaster.warn( title = R.string.settings__es__server_error, body = R.string.settings__es__server_error_description, testTag = "ElectrumErrorToast", diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index b5eeccd4d..276f42340 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -26,8 +26,6 @@ import androidx.navigation.NavController import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ToastText -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight @@ -42,6 +40,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster @Composable fun RgsServerScreen( @@ -50,7 +49,7 @@ fun RgsServerScreen( viewModel: RgsServerViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val app = appViewModel ?: return + val toaster = toaster // Handle result from Scanner LaunchedEffect(savedStateHandle) { @@ -66,15 +65,13 @@ fun RgsServerScreen( LaunchedEffect(uiState.connectionResult) { uiState.connectionResult?.let { result -> if (result.isSuccess) { - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = R.string.settings__rgs__update_success_title, body = R.string.settings__rgs__update_success_description, testTag = "RgsUpdatedToast", ) } else { - app.toast( - type = ToastType.ERROR, + toaster.error( title = ToastText(R.string.wallet__ldk_start_error_title), body = ToastText(result.exceptionOrNull()?.message ?: "Unknown error"), testTag = "RgsErrorToast", diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index afd3bc17d..75c3e540c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -56,9 +56,7 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText import to.bitkit.models.ToastText -import to.bitkit.models.ToastType import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.ChannelStatusUi @@ -75,6 +73,7 @@ import to.bitkit.ui.settings.lightning.components.ChannelStatusView import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.getBlockExplorerUrl import to.bitkit.ui.walletViewModel import java.time.Instant @@ -88,7 +87,7 @@ fun ChannelDetailScreen( viewModel: LightningConnectionsViewModel, ) { val context = LocalContext.current - val app = appViewModel ?: return + val toaster = toaster val wallet = walletViewModel ?: return val selectedChannel by viewModel.selectedChannel.collectAsStateWithLifecycle() @@ -129,8 +128,7 @@ fun ChannelDetailScreen( }, onCopyText = { text -> context.setClipboardText(text) - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = ToastText(R.string.common__copied), body = ToastText(text), ) From 9db7c89c6f0c3d77c91ea04e0ec28482edbc9ede Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:45:34 +0100 Subject: [PATCH 20/21] refactor: migrate internal AppViewModel toast calls Co-Authored-By: Claude Opus 4.5 --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 141 ++++++++---------- 1 file changed, 63 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 109635996..74236cc69 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -451,8 +451,7 @@ class AppViewModel @Inject constructor( migrationService.setShowingMigrationLoading(false) delay(MIGRATION_AUTH_RESET_DELAY_MS) resetIsAuthenticatedStateInternal() - toast( - type = ToastType.ERROR, + toaster.error( title = "Migration Warning", body = "Migration completed but node restart failed. Please restart the app." ) @@ -535,20 +534,18 @@ class AppViewModel @Inject constructor( activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel) return } - toast( - type = ToastType.LIGHTNING, - title = context.getString(R.string.lightning__channel_opened_title), - body = context.getString(R.string.lightning__channel_opened_msg), + toaster.lightning( + title = R.string.lightning__channel_opened_title, + body = R.string.lightning__channel_opened_msg, testTag = "SpendingBalanceReadyToast", ) } private suspend fun notifyTransactionRemoved(event: Event.OnchainTransactionEvicted) { if (activityRepo.wasTransactionReplaced(event.txid)) return - toast( - type = ToastType.WARNING, - title = context.getString(R.string.wallet__toast_transaction_removed_title), - body = context.getString(R.string.wallet__toast_transaction_removed_description), + toaster.warn( + title = R.string.wallet__toast_transaction_removed_title, + body = R.string.wallet__toast_transaction_removed_description, testTag = "TransactionRemovedToast", ) } @@ -560,25 +557,27 @@ class AppViewModel @Inject constructor( showTransactionSheet(result.sheet) } - private fun notifyTransactionUnconfirmed() = toast( - type = ToastType.WARNING, - title = context.getString(R.string.wallet__toast_transaction_unconfirmed_title), - body = context.getString(R.string.wallet__toast_transaction_unconfirmed_description), + private fun notifyTransactionUnconfirmed() = toaster.warn( + title = R.string.wallet__toast_transaction_unconfirmed_title, + body = R.string.wallet__toast_transaction_unconfirmed_description, testTag = "TransactionUnconfirmedToast", ) private suspend fun notifyTransactionReplaced(event: Event.OnchainTransactionReplaced) { val isReceive = activityRepo.isReceivedTransaction(event.txid) - toast( - type = ToastType.INFO, - title = when (isReceive) { - true -> R.string.wallet__toast_received_transaction_replaced_title - else -> R.string.wallet__toast_transaction_replaced_title - }.let { context.getString(it) }, - body = when (isReceive) { - true -> R.string.wallet__toast_received_transaction_replaced_description - else -> R.string.wallet__toast_transaction_replaced_description - }.let { context.getString(it) }, + toaster.info( + title = context.getString( + when (isReceive) { + true -> R.string.wallet__toast_received_transaction_replaced_title + else -> R.string.wallet__toast_transaction_replaced_title + } + ), + body = context.getString( + when (isReceive) { + true -> R.string.wallet__toast_received_transaction_replaced_description + else -> R.string.wallet__toast_transaction_replaced_description + } + ), testTag = when (isReceive) { true -> "ReceivedTransactionReplacedToast" else -> "TransactionReplacedToast" @@ -586,10 +585,9 @@ class AppViewModel @Inject constructor( ) } - private fun notifyPaymentFailed() = toast( - type = ToastType.ERROR, - title = context.getString(R.string.wallet__toast_payment_failed_title), - body = context.getString(R.string.wallet__toast_payment_failed_description), + private fun notifyPaymentFailed() = toaster.error( + title = R.string.wallet__toast_payment_failed_title, + body = R.string.wallet__toast_payment_failed_description, testTag = "PaymentFailedToast", ) @@ -777,8 +775,7 @@ class AppViewModel @Inject constructor( if (lnurl is LnurlParams.LnurlPay) { val minSendable = lnurl.data.minSendableSat() if (_sendUiState.value.amount < minSendable) { - toast( - type = ToastType.ERROR, + toaster.error( title = context.getString(R.string.wallet__lnurl_pay__error_min__title), body = context.getString(R.string.wallet__lnurl_pay__error_min__description) .replace("{amount}", minSendable.toString()), @@ -832,10 +829,9 @@ class AppViewModel @Inject constructor( private fun onPasteClick() { val data = context.getClipboardText()?.trim() if (data.isNullOrBlank()) { - toast( - type = ToastType.WARNING, - title = context.getString(R.string.wallet__send_clipboard_empty_title), - body = context.getString(R.string.wallet__send_clipboard_empty_text), + toaster.warn( + title = R.string.wallet__send_clipboard_empty_title, + body = R.string.wallet__send_clipboard_empty_text, ) return } @@ -877,10 +873,9 @@ class AppViewModel @Inject constructor( is Scanner.Gift -> onScanGift(scan.code, scan.amount) else -> { Logger.warn("Unhandled scan data: $scan", context = TAG) - toast( - type = ToastType.WARNING, - title = context.getString(R.string.other__scan_err_decoding), - body = context.getString(R.string.other__scan_err_interpret_title), + toaster.warn( + title = R.string.other__scan_err_decoding, + body = R.string.other__scan_err_interpret_title, ) } } @@ -894,10 +889,9 @@ class AppViewModel @Inject constructor( ?.invoice ?.takeIf { invoice -> if (invoice.isExpired) { - toast( - type = ToastType.ERROR, - title = context.getString(R.string.other__scan_err_decoding), - body = context.getString(R.string.other__scan__error__expired), + toaster.error( + title = R.string.other__scan_err_decoding, + body = R.string.other__scan__error__expired, ) Logger.debug( @@ -963,10 +957,9 @@ class AppViewModel @Inject constructor( private suspend fun onScanLightning(invoice: LightningInvoice, scanResult: String) { if (invoice.isExpired) { - toast( - type = ToastType.ERROR, - title = context.getString(R.string.other__scan_err_decoding), - body = context.getString(R.string.other__scan__error__expired), + toaster.error( + title = R.string.other__scan_err_decoding, + body = R.string.other__scan__error__expired, ) return } @@ -975,10 +968,9 @@ class AppViewModel @Inject constructor( if (quickPayHandled) return if (!lightningRepo.canSend(invoice.amountSatoshis)) { - toast( - type = ToastType.ERROR, - title = context.getString(R.string.wallet__error_insufficient_funds_title), - body = context.getString(R.string.wallet__error_insufficient_funds_msg) + toaster.error( + title = R.string.wallet__error_insufficient_funds_title, + body = R.string.wallet__error_insufficient_funds_msg, ) return } @@ -1019,10 +1011,9 @@ class AppViewModel @Inject constructor( val maxSendable = data.maxSendableSat() if (!lightningRepo.canSend(minSendable)) { - toast( - type = ToastType.WARNING, - title = context.getString(R.string.other__lnurl_pay_error), - body = context.getString(R.string.other__lnurl_pay_error_no_capacity), + toaster.warn( + title = R.string.other__lnurl_pay_error, + body = R.string.other__lnurl_pay_error_no_capacity, ) return } @@ -1067,10 +1058,9 @@ class AppViewModel @Inject constructor( val maxWithdrawable = data.maxWithdrawableSat() if (minWithdrawable > maxWithdrawable) { - toast( - type = ToastType.WARNING, - title = context.getString(R.string.other__lnurl_withdr_error), - body = context.getString(R.string.other__lnurl_withdr_error_minmax) + toaster.warn( + title = R.string.other__lnurl_withdr_error, + body = R.string.other__lnurl_withdr_error_minmax, ) return } @@ -1116,15 +1106,13 @@ class AppViewModel @Inject constructor( k1 = k1, domain = domain, ).onFailure { - toast( - type = ToastType.WARNING, + toaster.warn( title = context.getString(R.string.other__lnurl_auth_error), body = context.getString(R.string.other__lnurl_auth_error_msg) .replace("{raw}", it.message?.takeIf { m -> m.isNotBlank() } ?: it.javaClass.simpleName), ) }.onSuccess { - toast( - type = ToastType.SUCCESS, + toaster.success( title = context.getString(R.string.other__lnurl_auth_success_title), body = when (domain.isNotBlank()) { true -> context.getString(R.string.other__lnurl_auth_success_msg_domain) @@ -1325,7 +1313,7 @@ class AppViewModel @Inject constructor( it.copy(decodedInvoice = invoice) } }.onFailure { - toast(Exception(context.getString(R.string.wallet__error_lnurl_invoice_fetch))) + toaster.error(Exception(context.getString(R.string.wallet__error_lnurl_invoice_fetch))) hideSheet() return } @@ -1338,7 +1326,7 @@ class AppViewModel @Inject constructor( val validatedAddress = runCatching { validateBitcoinAddress(address) } .getOrElse { e -> Logger.error("Invalid bitcoin send address: '$address'", e, context = TAG) - toast(Exception(context.getString(R.string.wallet__error_invalid_bitcoin_address))) + toaster.error(Exception(context.getString(R.string.wallet__error_invalid_bitcoin_address))) hideSheet() return } @@ -1360,10 +1348,9 @@ class AppViewModel @Inject constructor( activityRepo.syncActivities() }.onFailure { e -> Logger.error(msg = "Error sending onchain payment", e = e, context = TAG) - toast( - type = ToastType.ERROR, + toaster.error( title = context.getString(R.string.wallet__error_sending_title), - body = e.message ?: context.getString(R.string.common__error_body) + body = e.message ?: context.getString(R.string.common__error_body), ) hideSheet() } @@ -1411,7 +1398,7 @@ class AppViewModel @Inject constructor( preActivityMetadataRepo.deletePreActivityMetadata(createdMetadataPaymentId) } Logger.error("Error sending lightning payment", e, context = TAG) - toast(e) + toaster.error(e) hideSheet() } } @@ -1452,10 +1439,9 @@ class AppViewModel @Inject constructor( callback = lnurl.data.callback, paymentRequest = invoice ).onSuccess { - toast( - type = ToastType.SUCCESS, - title = context.getString(R.string.other__lnurl_withdr_success_title), - body = context.getString(R.string.other__lnurl_withdr_success_msg), + toaster.success( + title = R.string.other__lnurl_withdr_success_title, + body = R.string.other__lnurl_withdr_success_msg, ) hideSheet() _sendUiState.update { it.copy(isLoading = false) } @@ -1485,7 +1471,7 @@ class AppViewModel @Inject constructor( mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) }.onFailure { e -> Logger.error(msg = "Activity not found", context = TAG) - toast(e) + toaster.error(e) _transactionSheet.update { it.copy(isLoadingDetails = false) } } } @@ -1509,7 +1495,7 @@ class AppViewModel @Inject constructor( mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) }.onFailure { e -> Logger.error(msg = "Activity not found", context = TAG) - toast(e) + toaster.error(e) _successSendUiState.update { it.copy(isLoadingDetails = false) } } } @@ -1894,10 +1880,9 @@ class AppViewModel @Inject constructor( keychain.upsertString(Keychain.Key.PIN_ATTEMPTS_REMAINING.name, newAttempts.toString()) if (newAttempts <= 0) { - toast( - type = ToastType.SUCCESS, - title = context.getString(R.string.security__wiped_title), - body = context.getString(R.string.security__wiped_message), + toaster.success( + title = R.string.security__wiped_title, + body = R.string.security__wiped_message, ) delay(250) // small delay for UI feedback mainScreenEffect(MainScreenEffect.WipeWallet) From 72513b95debcfa797f3f577a8faf89467ddb87ae Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 08:40:12 +0100 Subject: [PATCH 21/21] refactor: remove toast() methods from AppViewModel Co-Authored-By: Claude Opus 4.5 --- .../screens/wallets/send/SendAmountScreen.kt | 14 ++-- .../wallets/send/SendRecipientScreen.kt | 19 +++--- .../java/to/bitkit/viewmodels/AppViewModel.kt | 66 ------------------- 3 files changed, 13 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index a857fac93..4c7d2de4b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 @@ -32,12 +31,10 @@ import to.bitkit.ext.maxWithdrawableSat import to.bitkit.models.BalanceState import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.ToastType import to.bitkit.models.safe import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalBalances import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth @@ -57,6 +54,7 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.viewmodels.AmountInputUiState import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.LnurlParams @@ -76,8 +74,7 @@ fun SendAmountScreen( currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), ) { - val app = appViewModel - val context = LocalContext.current + val toaster = toaster val amountInputUiState: AmountInputUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() val currentOnEvent by rememberUpdatedState(onEvent) @@ -108,10 +105,9 @@ fun SendAmountScreen( }.takeIf { canGoBack }, onClickMax = { maxSats -> if (uiState.lnurl == null) { - app?.toast( - type = ToastType.INFO, - title = context.getString(R.string.wallet__send_max_spending__title), - body = context.getString(R.string.wallet__send_max_spending__description) + toaster.info( + title = R.string.wallet__send_max_spending__title, + body = R.string.wallet__send_max_spending__description, ) } amountInputViewModel.setSats(maxSats, currencies) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index dcec61ea4..eb33429c9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -59,8 +59,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import to.bitkit.R import to.bitkit.ext.startActivityAppSettings -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BottomSheetPreview @@ -76,6 +74,7 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes import to.bitkit.ui.theme.TRANSITION_SCREEN_MS +import to.bitkit.ui.toaster import to.bitkit.ui.utils.withAccent import to.bitkit.utils.AppError import to.bitkit.utils.Logger @@ -91,7 +90,7 @@ fun SendRecipientScreen( onEvent: (SendEvent) -> Unit, modifier: Modifier = Modifier, ) { - val app = appViewModel + val toaster = toaster // Context & lifecycle val context = LocalContext.current @@ -139,10 +138,9 @@ fun SendRecipientScreen( } else { val error = requireNotNull(result.exceptionOrNull()) Logger.error("Scan failed", error, context = TAG) - app?.toast( - type = ToastType.ERROR, - title = context.getString(R.string.other__qr_error_header), - body = context.getString(R.string.other__qr_error_text), + toaster.error( + title = R.string.other__qr_error_header, + body = R.string.other__qr_error_text, ) } } @@ -173,11 +171,10 @@ fun SendRecipientScreen( isCameraInitialized = true }.onFailure { Logger.error("Camera initialization failed", it, context = TAG) - app?.toast( - type = ToastType.ERROR, + toaster.error( title = context.getString(R.string.other__qr_error_header), body = context.getString(R.string.other__camera_init_error) - .replace("{message}", it.message.orEmpty()) + .replace("{message}", it.message.orEmpty()), ) isCameraInitialized = false } @@ -206,7 +203,7 @@ fun SendRecipientScreen( onEvent(SendEvent.AddressContinue(qrCode)) } - val handleGalleryError: (Throwable) -> Unit = { app?.toast(it) } + val handleGalleryError: (Throwable) -> Unit = { toaster.error(it) } val galleryLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent(), diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 74236cc69..072927b8e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -83,8 +83,6 @@ import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast -import to.bitkit.models.ToastText -import to.bitkit.models.ToastType import to.bitkit.models.TransactionSpeed import to.bitkit.models.safe import to.bitkit.models.toActivityFilter @@ -120,7 +118,6 @@ import to.bitkit.utils.timedsheets.sheets.QuickPayTimedSheet import java.math.BigDecimal import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException -import kotlin.time.Duration import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) @@ -1781,69 +1778,6 @@ class AppViewModel @Inject constructor( private val toastQueue = toastQueueProvider(Dispatchers.Main.immediate) val currentToast: StateFlow = toastQueue.currentToast - fun toast( - type: ToastType, - title: ToastText, - body: ToastText? = null, - autoHide: Boolean = true, - duration: Duration = Toast.DURATION_DEFAULT, - testTag: String? = null, - ) { - toastQueue.enqueue( - Toast( - type = type, - title = title, - body = body, - autoHide = autoHide, - duration = duration, - testTag = testTag, - ) - ) - } - - fun toast( - type: ToastType, - @StringRes title: Int, - @StringRes body: Int? = null, - autoHide: Boolean = true, - duration: Duration = Toast.DURATION_DEFAULT, - testTag: String? = null, - ) = toast( - type = type, - title = ToastText(title), - body = body?.let { ToastText(it) }, - autoHide = autoHide, - duration = duration, - testTag = testTag, - ) - - fun toast( - type: ToastType, - title: String, - body: String? = null, - autoHide: Boolean = true, - duration: Duration = Toast.DURATION_DEFAULT, - testTag: String? = null, - ) = toast( - type = type, - title = ToastText(title), - body = body?.let { ToastText(it) }, - autoHide = autoHide, - duration = duration, - testTag = testTag, - ) - - fun toast(error: Throwable) { - toast( - type = ToastType.ERROR, - title = ToastText(R.string.common__error), - body = error.message?.let { ToastText(it) } - ?: ToastText(R.string.common__error_body) - ) - } - - fun toast(toast: Toast) = toastQueue.enqueue(toast) - fun hideToast() = toastQueue.dismissCurrentToast() fun pauseToast() = toastQueue.pauseCurrentToast()