Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ suspend fun getData(): Result<Data> = 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

Expand Down
8 changes: 4 additions & 4 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import to.bitkit.ui.shared.toast.ToastQueueManager
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.ui.shared.toast.ToastQueue
import javax.inject.Singleton

@Module
Expand All @@ -19,7 +19,7 @@ object ViewModelModule {
}

@Provides
fun provideToastManagerProvider(): (CoroutineScope) -> ToastQueueManager {
return ::ToastQueueManager
fun provideToastQueueProvider(): (CoroutineDispatcher) -> ToastQueue {
return ::ToastQueue
}
}
17 changes: 11 additions & 6 deletions app/src/main/java/to/bitkit/models/Toast.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package to.bitkit.models

import androidx.compose.runtime.Stable
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

@Stable
data class Toast(
val type: ToastType,
val title: String,
val description: String? = null,
val title: ToastText,
val body: ToastText? = null,
val autoHide: Boolean,
val visibilityTime: Long = VISIBILITY_TIME_DEFAULT,
val duration: Duration = DURATION_DEFAULT,
val testTag: String? = null,
) {
enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR }

companion object {
const val VISIBILITY_TIME_DEFAULT = 3000L
val DURATION_DEFAULT: Duration = 3.seconds
}
}

enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR }
32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/models/ToastText.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 {
@JvmInline
value class Resource(@StringRes val resId: Int) : ToastText

@JvmInline
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
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
}
9 changes: 4 additions & 5 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -373,10 +373,9 @@ 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(
body = context.getString(R.string.settings__backup__failed_message).formatPlural(
mapOf("interval" to (BACKUP_CHECK_INTERVAL / MINUTE_IN_MS))
),
)
Expand Down
9 changes: 4 additions & 5 deletions app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -92,10 +92,9 @@ 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."
body = "An error has occurred. Please try again later."
)
}
}
Expand Down
9 changes: 5 additions & 4 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -626,11 +627,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 = Toast.ToastType.ERROR,
type = ToastType.ERROR,
title = title,
description = description
body = body
)
},
)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/to/bitkit/ui/Locals.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,6 +30,7 @@ val LocalActivityListViewModel = staticCompositionLocalOf<ActivityListViewModel?
val LocalTransferViewModel = staticCompositionLocalOf<TransferViewModel?> { null }
val LocalSettingsViewModel = staticCompositionLocalOf<SettingsViewModel?> { null }
val LocalBackupsViewModel = staticCompositionLocalOf<BackupsViewModel?> { null }
val LocalToaster = staticCompositionLocalOf<Toaster?> { null }

val appViewModel: AppViewModel?
@Composable get() = LocalAppViewModel.current
Expand Down Expand Up @@ -56,3 +58,6 @@ val backupsViewModel: BackupsViewModel?

val drawerState: DrawerState?
@Composable get() = LocalDrawerState.current

val toaster: Toaster?
@Composable get() = LocalToaster.current
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -173,7 +173,7 @@ class MainActivity : FragmentActivity() {
}

val currentToast by appViewModel.currentToast.collectAsStateWithLifecycle()
ToastOverlay(
ToastHost(
toast = currentToast,
hazeState = hazeState,
onDismiss = { appViewModel.hideToast() },
Expand Down
10 changes: 5 additions & 5 deletions app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ 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.ToastText
import to.bitkit.models.ToastType
import to.bitkit.models.formatToModernDisplay
import to.bitkit.repositories.LightningState
import to.bitkit.ui.components.BodyM
Expand Down Expand Up @@ -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()
Expand All @@ -92,9 +92,9 @@ fun NodeInfoScreen(
onDisconnectPeer = { wallet.disconnectPeer(it) },
onCopy = { text ->
app.toast(
type = Toast.ToastType.SUCCESS,
title = context.getString(R.string.common__copied),
description = text
type = ToastType.SUCCESS,
title = ToastText(R.string.common__copied),
body = ToastText(text),
)
},
)
Expand Down
19 changes: 8 additions & 11 deletions app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ 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.models.Toast
import to.bitkit.repositories.ConnectivityState
import to.bitkit.ui.toaster
import to.bitkit.viewmodels.AppViewModel

@Composable
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) }
Expand All @@ -30,18 +29,16 @@ fun IsOnlineTracker(

when (connectivityState) {
ConnectivityState.CONNECTED -> {
app.toast(
type = Toast.ToastType.SUCCESS,
title = context.getString(R.string.other__connection_back_title),
description = context.getString(R.string.other__connection_back_msg),
toaster.success(
title = R.string.other__connection_back_title,
body = R.string.other__connection_back_msg,
)
}

ConnectivityState.DISCONNECTED -> {
app.toast(
type = Toast.ToastType.WARNING,
title = context.getString(R.string.other__connection_issue),
description = context.getString(R.string.other__connection_issue_explain),
toaster.warn(
title = R.string.other__connection_issue,
body = R.string.other__connection_issue_explain,
)
}

Expand Down
Loading
Loading