Skip to content
Open
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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
- ALWAYS prefer `when (subject)` with Kotlin guard conditions (`if`) over condition-based `when {}` with `is` type checks, e.g. `when (event) { is Foo if event.x == y -> ... }` instead of `when { event is Foo && event.x == y -> ... }`
- ALWAYS prefer `sealed interface` over `sealed class` when no shared state or constructor is needed
- NEVER duplicate error logging in `.onFailure {}` if the called method already logs the same error internally
- ALWAYS use `ImmutableList`/`ImmutableMap`/`ImmutableSet` instead of `List`/`Map`/`Set` for composable function parameters and UiState data class fields
- ALWAYS annotate UiState data classes with `@Immutable`; use `@Stable` instead when any field holds a non-immutable type (e.g. `Throwable`, external library types from `bitkitcore`/`ldknode`/`vssclient`, or types containing plain `List`/`Map`/`Set`)
- ALWAYS use `.toImmutableList()`, `.toImmutableMap()`, `.toImmutableSet()` when producing collections for UI state
- ALWAYS use `persistentListOf()`, `persistentMapOf()`, `persistentSetOf()` for default values in UiState fields

### Device Debugging (adb)

Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ dependencies {
implementation(libs.material)
implementation(libs.datastore.preferences)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.biometric)
implementation(libs.zxing)
implementation(libs.barcode.scanning)
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package to.bitkit.repositories

import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Immutable
import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.ActivityFilter
import com.synonym.bitkitcore.ActivityTags
Expand All @@ -11,6 +12,9 @@ import com.synonym.bitkitcore.OnchainActivity
import com.synonym.bitkitcore.PaymentState
import com.synonym.bitkitcore.PaymentType
import com.synonym.bitkitcore.SortDirection
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
Expand Down Expand Up @@ -600,7 +604,7 @@ class ActivityRepo @Inject constructor(
runCatching {
coreService.activity.allPossibleTags()
}.onSuccess { tags ->
_state.update { it.copy(tags = tags) }
_state.update { it.copy(tags = tags.toImmutableList()) }
}.onFailure {
Logger.error("getAllAvailableTags error", it, context = TAG)
}
Expand Down Expand Up @@ -677,6 +681,7 @@ class ActivityRepo @Inject constructor(
}
}

@Immutable
data class ActivityState(
val tags: List<String> = emptyList(),
val tags: ImmutableList<String> = persistentListOf(),
)
31 changes: 18 additions & 13 deletions app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package to.bitkit.repositories

import androidx.compose.runtime.Stable
import com.synonym.bitkitcore.BtOrderState2
import com.synonym.bitkitcore.ChannelLiquidityOptions
import com.synonym.bitkitcore.ChannelLiquidityParams
Expand All @@ -14,6 +15,9 @@ import com.synonym.bitkitcore.calculateChannelLiquidityOptions
import com.synonym.bitkitcore.getDefaultLspBalance
import com.synonym.bitkitcore.giftOrder
import com.synonym.bitkitcore.giftPay
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -104,7 +108,7 @@ class BlocktankRepo @Inject constructor(
.collect { paidOrderIds ->
_blocktankState.update { state ->
state.copy(
paidOrders = state.orders.filter { order -> order.id in paidOrderIds },
paidOrders = state.orders.filter { order -> order.id in paidOrderIds }.toImmutableList(),
)
}
}
Expand Down Expand Up @@ -148,9 +152,9 @@ class BlocktankRepo @Inject constructor(
val cachedCjitEntries = coreService.blocktank.cjitEntries(refresh = false)
_blocktankState.update { state ->
state.copy(
orders = cachedOrders,
cjitEntries = cachedCjitEntries,
paidOrders = cachedOrders.filter { order -> order.id in paidOrderIds },
orders = cachedOrders.toImmutableList(),
cjitEntries = cachedCjitEntries.toImmutableList(),
paidOrders = cachedOrders.filter { order -> order.id in paidOrderIds }.toImmutableList(),
)
}

Expand All @@ -159,9 +163,9 @@ class BlocktankRepo @Inject constructor(
val cjitEntries = coreService.blocktank.cjitEntries(refresh = true)
_blocktankState.update { state ->
state.copy(
orders = orders,
cjitEntries = cjitEntries,
paidOrders = orders.filter { order -> order.id in paidOrderIds },
orders = orders.toImmutableList(),
cjitEntries = cjitEntries.toImmutableList(),
paidOrders = orders.filter { order -> order.id in paidOrderIds }.toImmutableList(),
)
}

Expand Down Expand Up @@ -287,7 +291,7 @@ class BlocktankRepo @Inject constructor(
updatedOrders[index] = order
}

_blocktankState.update { state -> state.copy(orders = updatedOrders) }
_blocktankState.update { state -> state.copy(orders = updatedOrders.toImmutableList()) }

return@runCatching order
}.onFailure {
Expand Down Expand Up @@ -406,8 +410,8 @@ class BlocktankRepo @Inject constructor(

_blocktankState.update {
it.copy(
orders = payload.orders,
cjitEntries = payload.cjitEntries,
orders = payload.orders.toImmutableList(),
cjitEntries = payload.cjitEntries.toImmutableList(),
info = payload.info,
)
}
Expand Down Expand Up @@ -516,10 +520,11 @@ class BlocktankRepo @Inject constructor(
}
}

@Stable
data class BlocktankState(
val orders: List<IBtOrder> = emptyList(),
val paidOrders: List<IBtOrder> = emptyList(),
val cjitEntries: List<IcJitEntry> = emptyList(),
val orders: ImmutableList<IBtOrder> = persistentListOf(),
val paidOrders: ImmutableList<IBtOrder> = persistentListOf(),
val cjitEntries: ImmutableList<IcJitEntry> = persistentListOf(),
val info: IBtInfo? = null,
val minCjitSats: Int? = null,
)
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package to.bitkit.repositories

import androidx.compose.runtime.Stable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -112,7 +116,7 @@ class CurrencyRepo @Inject constructor(
rate.quote == settings.selectedCurrency
}
_currencyState.value.copy(
rates = cachedData.cachedRates,
rates = cachedData.cachedRates.toImmutableList(),
selectedCurrency = settings.selectedCurrency,
displayUnit = settings.displayUnit,
primaryDisplay = settings.primaryDisplay,
Expand Down Expand Up @@ -248,8 +252,9 @@ class CurrencyRepo @Inject constructor(
override fun convertSatsToFiatString(sats: Long): String = convertSatsToFiatPair(sats).getOrNull()?.second ?: ""
}

@Stable
data class CurrencyState(
val rates: List<FxRate> = emptyList(),
val rates: ImmutableList<FxRate> = persistentListOf(),
val error: Throwable? = null,
val hasStaleData: Boolean = false,
val selectedCurrency: String = "USD",
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package to.bitkit.repositories

import androidx.compose.runtime.Stable
import com.google.firebase.messaging.FirebaseMessaging
import com.synonym.bitkitcore.AddressType
import com.synonym.bitkitcore.ClosedChannelDetails
Expand All @@ -10,6 +11,9 @@ import com.synonym.bitkitcore.Scanner
import com.synonym.bitkitcore.createChannelRequestUrl
import com.synonym.bitkitcore.createWithdrawCallbackUrl
import com.synonym.bitkitcore.lnurlAuth
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -1170,8 +1174,8 @@ class LightningRepo @Inject constructor(
it.copy(
nodeId = getNodeId().orEmpty(),
nodeStatus = getStatus(),
peers = getPeers().orEmpty(),
channels = getChannels().orEmpty(),
peers = getPeers().orEmpty().toImmutableList(),
channels = getChannels().orEmpty().toImmutableList(),
balances = getBalances(),
)
}
Expand Down Expand Up @@ -1388,12 +1392,13 @@ class NodeRunTimeoutError(opName: String) : AppError("Timeout waiting for node t
class GetPaymentsError : AppError("It wasn't possible get the payments")
class SyncUnhealthyError : AppError("Wallet sync failed before send")

@Stable
data class LightningState(
val nodeId: String = "",
val nodeStatus: NodeStatus? = null,
val nodeLifecycleState: NodeLifecycleState = NodeLifecycleState.Stopped,
val peers: List<PeerDetails> = emptyList(),
val channels: List<ChannelDetails> = emptyList(),
val peers: ImmutableList<PeerDetails> = persistentListOf(),
val channels: ImmutableList<ChannelDetails> = persistentListOf(),
val balances: BalanceDetails? = null,
val isSyncingWallet: Boolean = false,
val isGeoBlocked: Boolean = false,
Expand Down
15 changes: 10 additions & 5 deletions app/src/main/java/to/bitkit/repositories/WalletRepo.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package to.bitkit.repositories

import androidx.compose.runtime.Immutable
import com.synonym.bitkitcore.AddressType
import com.synonym.bitkitcore.PreActivityMetadata
import com.synonym.bitkitcore.Scanner
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -434,7 +438,7 @@ class WalletRepo @Inject constructor(
_walletState.update {
it.copy(
bip21 = "",
selectedTags = if (clearTags) emptyList() else it.selectedTags,
selectedTags = if (clearTags) persistentListOf() else it.selectedTags,
bip21AmountSats = null,
bip21Description = "",
)
Expand Down Expand Up @@ -474,7 +478,7 @@ class WalletRepo @Inject constructor(
preActivityMetadataRepo.addPreActivityMetadataTags(paymentId, listOf(newTag)).onSuccess {
_walletState.update {
it.copy(
selectedTags = (it.selectedTags + newTag).distinct()
selectedTags = (it.selectedTags + newTag).distinct().toImmutableList()
)
}
settingsStore.addLastUsedTag(newTag)
Expand All @@ -495,7 +499,7 @@ class WalletRepo @Inject constructor(
.onSuccess {
_walletState.update {
it.copy(
selectedTags = it.selectedTags.filterNot { tagItem -> tagItem == tag }
selectedTags = it.selectedTags.filterNot { tagItem -> tagItem == tag }.toImmutableList()
)
}
}.onFailure {
Expand All @@ -508,7 +512,7 @@ class WalletRepo @Inject constructor(
if (paymentId == null || paymentId.isEmpty()) return@withContext

preActivityMetadataRepo.resetPreActivityMetadataTags(paymentId).onSuccess {
_walletState.update { it.copy(selectedTags = emptyList()) }
_walletState.update { it.copy(selectedTags = persistentListOf()) }
}.onFailure {
Logger.error("Failed to reset tags for pre-activity metadata", it, context = TAG)
}
Expand Down Expand Up @@ -588,13 +592,14 @@ class WalletRepo @Inject constructor(
}
}

@Immutable
data class WalletState(
val onchainAddress: String = "",
val bolt11: String = "",
val bip21: String = "",
val bip21AmountSats: ULong? = null,
val bip21Description: String = "",
val selectedTags: List<String> = listOf(),
val selectedTags: ImmutableList<String> = persistentListOf(),
val walletExists: Boolean = false,
)

Expand Down
12 changes: 6 additions & 6 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -41,6 +40,7 @@ import androidx.navigation.toRoute
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -339,7 +339,7 @@ fun ContentView(
}

val balance by walletViewModel.balanceState.collectAsStateWithLifecycle()
val currencies by currencyViewModel.uiState.collectAsState()
val currencies by currencyViewModel.uiState.collectAsStateWithLifecycle()

// Keep backups in sync
LaunchedEffect(backupsViewModel) { backupsViewModel.observeAndSyncBackups() }
Expand Down Expand Up @@ -381,7 +381,7 @@ fun ContentView(
}

is Sheet.Receive -> {
val walletState by walletViewModel.walletState.collectAsState()
val walletState by walletViewModel.walletState.collectAsStateWithLifecycle()
ReceiveSheet(
walletState = walletState,
navigateToExternalConnection = {
Expand Down Expand Up @@ -658,7 +658,7 @@ private fun RootNavHost(
)
}
composableWithDefaultTransitions<Routes.Funding> {
val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsState()
val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle()
val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle()

FundingScreen(
Expand Down Expand Up @@ -796,7 +796,7 @@ private fun NavGraphBuilder.home(

SavingsWalletScreen(
isGeoBlocked = isGeoBlocked,
onchainActivities = onchainActivities.orEmpty(),
onchainActivities = onchainActivities ?: persistentListOf(),
onAllActivityButtonClick = { navController.navigateToAllActivity() },
onActivityItemClick = { navController.navigateToActivityItem(it) },
onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) },
Expand All @@ -821,7 +821,7 @@ private fun NavGraphBuilder.home(

SpendingWalletScreen(
channels = lightningState.channels,
lightningActivities = lightningActivities.orEmpty(),
lightningActivities = lightningActivities ?: persistentListOf(),
onAllActivityButtonClick = { navController.navigateToAllActivity() },
onActivityItemClick = { navController.navigateToActivityItem(it) },
onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) },
Expand Down
Loading
Loading