Updated on 2026-08-14
This commit is contained in:
parent
1437c14019
commit
3f671e3e0f
46 changed files with 1454 additions and 43 deletions
1
features/txhistory/api/.gitignore
vendored
Normal file
1
features/txhistory/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
26
features/txhistory/api/build.gradle.kts
Normal file
26
features/txhistory/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.txhistory.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Core */
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
/** Domain models */
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
implementation(deps.compose.foundation)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.txhistory
|
||||
|
||||
interface TxHistoryFeatureToggles {
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.features.txhistory.component
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Stable
|
||||
interface TxHistoryComponent {
|
||||
|
||||
val txHistoryState: StateFlow<TxHistoryUM>
|
||||
|
||||
fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM)
|
||||
|
||||
fun reload()
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val openExplorer: () -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, TxHistoryComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
interface TxHistoryContentUpdateEmitter {
|
||||
suspend fun triggerUpdate()
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
sealed interface TxHistoryUM {
|
||||
|
||||
val isBalanceHidden: Boolean
|
||||
|
||||
data class Loading(override val isBalanceHidden: Boolean, private val onExploreClick: () -> Unit) : TxHistoryUM {
|
||||
val items = persistentListOf(
|
||||
TxHistoryItemUM.Title(onExploreClick = onExploreClick),
|
||||
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_1")),
|
||||
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_2")),
|
||||
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_3")),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet transaction history state with content
|
||||
*/
|
||||
data class Content(
|
||||
override val isBalanceHidden: Boolean,
|
||||
val items: ImmutableList<TxHistoryItemUM>,
|
||||
val loadMore: () -> Boolean,
|
||||
) : TxHistoryUM
|
||||
|
||||
/** Empty state */
|
||||
data class Empty(override val isBalanceHidden: Boolean, val onExploreClick: () -> Unit) : TxHistoryUM
|
||||
|
||||
/**
|
||||
* Not supported tx history state
|
||||
*
|
||||
* @property pendingTransactions pending transactions
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
*/
|
||||
data class NotSupported(
|
||||
override val isBalanceHidden: Boolean,
|
||||
val pendingTransactions: ImmutableList<TransactionState>,
|
||||
val onExploreClick: () -> Unit,
|
||||
) : TxHistoryUM
|
||||
|
||||
/**
|
||||
* Error state
|
||||
*
|
||||
* @property onReloadClick lambda be invoke when reload button was clicked
|
||||
*/
|
||||
data class Error(
|
||||
override val isBalanceHidden: Boolean,
|
||||
val onReloadClick: () -> Unit,
|
||||
val onExploreClick: () -> Unit,
|
||||
) : TxHistoryUM
|
||||
|
||||
fun copySealed(isBalanceHidden: Boolean): TxHistoryUM {
|
||||
return when (this) {
|
||||
is Content -> copy(isBalanceHidden = isBalanceHidden)
|
||||
is NotSupported -> copy(isBalanceHidden = isBalanceHidden)
|
||||
is Empty -> copy(isBalanceHidden = isBalanceHidden)
|
||||
is Error -> copy(isBalanceHidden = isBalanceHidden)
|
||||
is Loading -> copy(isBalanceHidden = isBalanceHidden)
|
||||
}
|
||||
}
|
||||
|
||||
/** Transactions history item state */
|
||||
sealed interface TxHistoryItemUM {
|
||||
|
||||
/**
|
||||
* Title item
|
||||
*
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
*/
|
||||
data class Title(val onExploreClick: () -> Unit) : TxHistoryItemUM
|
||||
|
||||
/**
|
||||
* Group title item
|
||||
*
|
||||
* @property title title
|
||||
* @property itemKey key to use in compose
|
||||
*/
|
||||
data class GroupTitle(
|
||||
val title: String,
|
||||
val itemKey: String,
|
||||
) : TxHistoryItemUM
|
||||
|
||||
/**
|
||||
* Transaction item
|
||||
*
|
||||
* @property state transaction state
|
||||
*/
|
||||
data class Transaction(val state: TransactionState) : TxHistoryItemUM
|
||||
}
|
||||
}
|
||||
1
features/txhistory/impl/.gitignore
vendored
Normal file
1
features/txhistory/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
59
features/txhistory/impl/build.gradle.kts
Normal file
59
features/txhistory/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.txhistory.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/* Project - API */
|
||||
implementation(projects.features.txhistory.api)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.pagination)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
/* AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.shimmer)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/* Other */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.txhistory
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTxHistoryFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : TxHistoryFeatureToggles {
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("TX_HISTORY_REFACTORING_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.txhistory.component
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.*
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.features.txhistory.model.TxHistoryModel
|
||||
import com.tangem.features.txhistory.ui.txHistoryItems
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class DefaultTxHistoryComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: TxHistoryComponent.Params,
|
||||
) : TxHistoryComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TxHistoryModel = getOrCreateModel(params)
|
||||
|
||||
override val txHistoryState: StateFlow<TxHistoryUM>
|
||||
get() = model.uiState
|
||||
|
||||
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) {
|
||||
txHistoryItems(listState, state)
|
||||
}
|
||||
|
||||
override fun reload() {
|
||||
model.reload()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : TxHistoryComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: TxHistoryComponent.Params): DefaultTxHistoryComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.txhistory.impl.R
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
|
||||
internal class TxHistoryItemToTransactionStateConverter(
|
||||
private val currency: CryptoCurrency,
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) : Converter<TxHistoryItem, TransactionState> {
|
||||
override fun convert(value: TxHistoryItem): TransactionState {
|
||||
return TransactionState.Content(
|
||||
txHash = value.txHash,
|
||||
amount = value.getAmount(),
|
||||
time = value.timestampInMillis.toTimeFormat(),
|
||||
status = value.status.tiUiStatus(),
|
||||
direction = value.extractDirection(),
|
||||
iconRes = value.extractIcon(),
|
||||
title = value.extractTitle(),
|
||||
subtitle = value.extractSubtitle(),
|
||||
timestamp = value.timestampInMillis,
|
||||
onClick = { txHistoryUiActions.openTxInExplorer(value.txHash) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) {
|
||||
R.drawable.ic_close_24
|
||||
} else {
|
||||
when (type) {
|
||||
is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24
|
||||
is TxHistoryItem.TransactionType.Staking.Stake,
|
||||
is TxHistoryItem.TransactionType.Staking.Vote,
|
||||
is TxHistoryItem.TransactionType.Staking.Restake,
|
||||
-> R.drawable.ic_transaction_history_staking_24
|
||||
is TxHistoryItem.TransactionType.Staking.ClaimRewards,
|
||||
-> R.drawable.ic_transaction_history_claim_rewards_24
|
||||
is TxHistoryItem.TransactionType.Staking.Unstake,
|
||||
is TxHistoryItem.TransactionType.Staking.Withdraw,
|
||||
-> R.drawable.ic_transaction_history_unstaking_24
|
||||
is TxHistoryItem.TransactionType.Operation,
|
||||
is TxHistoryItem.TransactionType.Swap,
|
||||
is TxHistoryItem.TransactionType.Transfer,
|
||||
is TxHistoryItem.TransactionType.UnknownOperation,
|
||||
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) {
|
||||
is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval)
|
||||
is TxHistoryItem.TransactionType.Operation -> stringReference(type.name)
|
||||
is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap)
|
||||
is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer)
|
||||
is TxHistoryItem.TransactionType.Staking.Stake -> resourceReference(R.string.common_stake)
|
||||
is TxHistoryItem.TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake)
|
||||
is TxHistoryItem.TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote)
|
||||
is TxHistoryItem.TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards)
|
||||
is TxHistoryItem.TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw)
|
||||
is TxHistoryItem.TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake)
|
||||
is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractSubtitle(): TextReference =
|
||||
when (val interactionAddress = interactionAddressType) {
|
||||
is TxHistoryItem.InteractionAddressType.Contract -> resourceReference(
|
||||
id = R.string.transaction_history_contract_address,
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
is TxHistoryItem.InteractionAddressType.Multiple -> resourceReference(
|
||||
id = if (isOutgoing) {
|
||||
R.string.transaction_history_transaction_to_address
|
||||
} else {
|
||||
R.string.transaction_history_transaction_from_address
|
||||
},
|
||||
formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)),
|
||||
)
|
||||
is TxHistoryItem.InteractionAddressType.User -> resourceReference(
|
||||
id = if (isOutgoing) {
|
||||
R.string.transaction_history_transaction_to_address
|
||||
} else {
|
||||
R.string.transaction_history_transaction_from_address
|
||||
},
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
is TxHistoryItem.InteractionAddressType.Validator -> resourceReference(
|
||||
id = R.string.transaction_history_transaction_validator,
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
null -> {
|
||||
TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractDirection() =
|
||||
if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING
|
||||
|
||||
private fun TxHistoryItem.getAmount(): String {
|
||||
if (type is TxHistoryItem.TransactionType.Staking.Vote ||
|
||||
type == TxHistoryItem.TransactionType.Staking.ClaimRewards ||
|
||||
type == TxHistoryItem.TransactionType.Staking.Withdraw
|
||||
) {
|
||||
return ""
|
||||
}
|
||||
val prefix = when {
|
||||
status == TxHistoryItem.TransactionStatus.Failed -> ""
|
||||
this.amount.isZero() -> ""
|
||||
else -> if (isOutgoing) StringsSigns.MINUS else StringsSigns.PLUS
|
||||
}
|
||||
return prefix + amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) {
|
||||
TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed
|
||||
TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed
|
||||
TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.txhistory.di
|
||||
|
||||
import com.tangem.features.txhistory.DefaultTxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.component.DefaultTxHistoryComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface TxHistoryFeatureModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureToggles: DefaultTxHistoryFeatureToggles): TxHistoryFeatureToggles
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindComponentFactory(factory: DefaultTxHistoryComponent.Factory): TxHistoryComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.txhistory.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.txhistory.model.TxHistoryModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface TxHistoryModelModule {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TxHistoryModel::class)
|
||||
fun bindModel(model: TxHistoryModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.txhistory.di
|
||||
|
||||
import com.tangem.features.txhistory.entity.DefaultTxHistoryUpdater
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUpdateListener
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TxHistoryUpdaterModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryContentContentUpdateEmitter(impl: DefaultTxHistoryUpdater): TxHistoryContentUpdateEmitter = impl
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryUpdaterListener(impl: DefaultTxHistoryUpdater): TxHistoryUpdateListener = impl
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class DefaultTxHistoryUpdater @Inject constructor() : TxHistoryUpdateListener, TxHistoryContentUpdateEmitter {
|
||||
|
||||
private val updateChannel = Channel<Unit>(Channel.BUFFERED)
|
||||
override val updates: Flow<Unit> = updateChannel.receiveAsFlow()
|
||||
|
||||
override suspend fun triggerUpdate() {
|
||||
updateChannel.send(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface TxHistoryUpdateListener {
|
||||
val updates: Flow<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
package com.tangem.features.txhistory.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUpdateListener
|
||||
import com.tangem.features.txhistory.utils.TxHistoryListManager
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TxHistoryModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val txHistoryUpdateListener: TxHistoryUpdateListener,
|
||||
repository: TxHistoryRepositoryV2,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model(), TxHistoryUiActions {
|
||||
|
||||
private val params: TxHistoryComponent.Params = paramsContainer.require()
|
||||
private val txHistoryItemConverter =
|
||||
TxHistoryItemToTransactionStateConverter(currency = params.currency, txHistoryUiActions = this)
|
||||
private val txHistoryListManager = TxHistoryListManager(
|
||||
repository = repository,
|
||||
dispatchers = dispatchers,
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.currency,
|
||||
txHistoryItemConverter = txHistoryItemConverter,
|
||||
txHistoryUiActions = this,
|
||||
)
|
||||
private val _uiState: MutableStateFlow<TxHistoryUM> =
|
||||
MutableStateFlow(TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = ::openExplorer))
|
||||
val uiState: StateFlow<TxHistoryUM> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
handleBalanceHiding()
|
||||
subscribeToUiItemChanges()
|
||||
loadTxInfo()
|
||||
subscribeToUpdateListener()
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
}
|
||||
|
||||
private fun subscribeToUiItemChanges() {
|
||||
txHistoryListManager.uiItems
|
||||
.onEach { updateState(it) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToUpdateListener() {
|
||||
txHistoryUpdateListener.updates
|
||||
.onEach { reload() }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun loadTxInfo() {
|
||||
_uiState.update { state -> getLoadingState(state.isBalanceHidden) }
|
||||
modelScope.launch {
|
||||
txHistoryItemsCountUseCase.invoke(userWalletId = params.userWalletId, currency = params.currency)
|
||||
.onLeft(::handleErrorState)
|
||||
.onRight { txHistoryListManager.startLoading() }
|
||||
}
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
// fast exit
|
||||
if (uiState.value is TxHistoryUM.NotSupported) return
|
||||
|
||||
_uiState.update { state ->
|
||||
if (state !is TxHistoryUM.Content) getLoadingState(state.isBalanceHidden) else state
|
||||
}
|
||||
modelScope.launch {
|
||||
txHistoryItemsCountUseCase.invoke(userWalletId = params.userWalletId, currency = params.currency)
|
||||
.onLeft(::handleErrorState)
|
||||
.onRight { txHistoryListManager.reload() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBalanceHiding() {
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.onEach { _uiState.update { state -> state.copySealed(isBalanceHidden = it.isBalanceHidden) } }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun loadMoreItems(): Boolean {
|
||||
modelScope.launch { txHistoryListManager.loadMore(params.userWalletId, params.currency) }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateState(items: ImmutableList<TxHistoryUM.TxHistoryItemUM>) {
|
||||
_uiState.update { state ->
|
||||
if (state is TxHistoryUM.Content) {
|
||||
state.copy(items = items)
|
||||
} else {
|
||||
TxHistoryUM.Content(
|
||||
items = items,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
loadMore = ::loadMoreItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleErrorState(error: TxHistoryStateError) {
|
||||
_uiState.update { state ->
|
||||
when (error) {
|
||||
is TxHistoryStateError.DataError -> TxHistoryUM.Error(
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
onReloadClick = ::reload,
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
TxHistoryStateError.EmptyTxHistories -> TxHistoryUM.Empty(
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
TxHistoryStateError.TxHistoryNotImplemented -> TxHistoryUM.NotSupported(
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
pendingTransactions = persistentListOf(),
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoadingState(isBalanceHidden: Boolean): TxHistoryUM.Loading {
|
||||
return TxHistoryUM.Loading(isBalanceHidden = isBalanceHidden, onExploreClick = ::openExplorer)
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates() {
|
||||
val userWallet: UserWallet = requireNotNull(getUserWalletUseCase(params.userWalletId).getOrNull()) {
|
||||
"User wallet not found"
|
||||
}
|
||||
getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = params.currency.id,
|
||||
isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.onEach(::handlePendingTxsChanges)
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun handlePendingTxsChanges(maybeCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>) {
|
||||
maybeCurrencyStatus.onRight { status ->
|
||||
val pendingTxs = status.value.pendingTransactions
|
||||
.map(txHistoryItemConverter::convert)
|
||||
.toPersistentList()
|
||||
_uiState.update { state ->
|
||||
if (state is TxHistoryUM.NotSupported) {
|
||||
state.copy(pendingTransactions = pendingTxs)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun openExplorer() {
|
||||
params.openExplorer()
|
||||
}
|
||||
|
||||
override fun openTxInExplorer(txHash: String) {
|
||||
getExplorerTransactionUrlUseCase(
|
||||
txHash = txHash,
|
||||
networkId = params.currency.network.id,
|
||||
).fold(
|
||||
ifLeft = { Timber.e(it.toString()) },
|
||||
ifRight = { urlOpener.openUrl(url = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package com.tangem.features.txhistory.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.components.transactions.PendingTxsBlock
|
||||
import com.tangem.core.ui.components.transactions.Transaction
|
||||
import com.tangem.core.ui.components.transactions.TxHistoryTitle
|
||||
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock
|
||||
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
|
||||
private const val LOAD_ITEMS_BUFFER = 20
|
||||
|
||||
internal fun LazyListScope.txHistoryItems(listState: LazyListState, state: TxHistoryUM) {
|
||||
when (state) {
|
||||
is TxHistoryUM.Content -> contentItems(listState, state)
|
||||
is TxHistoryUM.Empty -> nonContentItem(state = EmptyTransactionsBlockState.Empty(state.onExploreClick))
|
||||
is TxHistoryUM.Error -> nonContentItem(
|
||||
state = EmptyTransactionsBlockState.FailedToLoad(
|
||||
onReload = state.onReloadClick,
|
||||
onExplore = state.onExploreClick,
|
||||
),
|
||||
)
|
||||
is TxHistoryUM.Loading -> loadingItems(state)
|
||||
is TxHistoryUM.NotSupported -> {
|
||||
if (state.pendingTransactions.isNotEmpty()) {
|
||||
item(key = "PendingTxsBlock", contentType = "PendingTxsBlock") {
|
||||
PendingTxsBlock(pendingTxs = state.pendingTransactions, isBalanceHidden = state.isBalanceHidden)
|
||||
}
|
||||
}
|
||||
|
||||
nonContentItem(
|
||||
state = EmptyTransactionsBlockState.NotImplemented(onExplore = state.onExploreClick),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
|
||||
item(key = state::class.java, contentType = state::class.java) {
|
||||
EmptyTransactionBlock(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.loadingItems(state: TxHistoryUM.Loading) {
|
||||
itemsIndexed(
|
||||
items = state.items,
|
||||
key = { _, item ->
|
||||
when (item) {
|
||||
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> item.itemKey
|
||||
is TxHistoryUM.TxHistoryItemUM.Title -> item.onExploreClick.hashCode()
|
||||
is TxHistoryUM.TxHistoryItemUM.Transaction ->
|
||||
item.state.txHash + (item.state as? TransactionState.Content)?.hashCode()
|
||||
}
|
||||
},
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { index, item ->
|
||||
TxHistoryListItem(
|
||||
state = item,
|
||||
isBalanceHidden = true,
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.items.lastIndex,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.contentItems(listState: LazyListState, state: TxHistoryUM.Content) {
|
||||
itemsIndexed(
|
||||
items = state.items,
|
||||
key = { _, item ->
|
||||
when (item) {
|
||||
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> item.itemKey
|
||||
is TxHistoryUM.TxHistoryItemUM.Title -> item.onExploreClick.hashCode()
|
||||
is TxHistoryUM.TxHistoryItemUM.Transaction ->
|
||||
item.state.txHash + (item.state as? TransactionState.Content)?.hashCode()
|
||||
}
|
||||
},
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { index, item ->
|
||||
TxHistoryListItem(
|
||||
state = item,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.items.lastIndex,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
item {
|
||||
InfiniteListHandler(
|
||||
listState = listState,
|
||||
buffer = LOAD_ITEMS_BUFFER,
|
||||
onLoadMore = state.loadMore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TxHistoryListItem(
|
||||
state: TxHistoryUM.TxHistoryItemUM,
|
||||
isBalanceHidden: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (state) {
|
||||
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> {
|
||||
TxHistoryGroupTitle(config = state, modifier = modifier)
|
||||
}
|
||||
is TxHistoryUM.TxHistoryItemUM.Title -> {
|
||||
TxHistoryTitle(onExploreClick = state.onExploreClick, modifier = modifier)
|
||||
}
|
||||
is TxHistoryUM.TxHistoryItemUM.Transaction -> {
|
||||
Transaction(
|
||||
state = state.state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TxHistoryGroupTitle(config: TxHistoryUM.TxHistoryItemUM.GroupTitle, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size24),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text(
|
||||
text = config.title,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListBatchingContext
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListConfig
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchListState
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
private typealias TxHistoryBatchAction = BatchAction<Int, TxHistoryListConfig, Nothing>
|
||||
|
||||
internal class TxHistoryListManager(
|
||||
private val repository: TxHistoryRepositoryV2,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val userWalletId: UserWalletId,
|
||||
private val currency: CryptoCurrency,
|
||||
txHistoryItemConverter: TxHistoryItemToTransactionStateConverter,
|
||||
txHistoryUiActions: TxHistoryUiActions,
|
||||
) {
|
||||
|
||||
private val jobHolder = JobHolder()
|
||||
private val actionsFlow: MutableSharedFlow<TxHistoryBatchAction> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
private val state: MutableStateFlow<TxHistoryListState> = MutableStateFlow(TxHistoryListState())
|
||||
private val uiManager = TxHistoryUiManager(
|
||||
state = state,
|
||||
txHistoryItemConverter = txHistoryItemConverter,
|
||||
txHistoryUiActions = txHistoryUiActions,
|
||||
)
|
||||
|
||||
val uiItems: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = uiManager.items
|
||||
|
||||
suspend fun startLoading() = coroutineScope {
|
||||
val batchFlow = repository.getTxHistoryBatchFlow(
|
||||
context = TxHistoryListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = this,
|
||||
),
|
||||
batchSize = 50,
|
||||
)
|
||||
|
||||
batchFlow.state
|
||||
.onEach { state -> updateState(state) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TxHistoryListConfig(userWalletId, currency, refresh = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun reload() {
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TxHistoryListConfig(userWalletId, currency, refresh = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadMore(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.LoadMore(
|
||||
requestParams = TxHistoryListConfig(userWalletId, currency, refresh = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateState(batchListState: BatchListState<Int, PaginationWrapper<TxHistoryItem>>) {
|
||||
state.update { state ->
|
||||
val clearUiBatches =
|
||||
state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating
|
||||
state.copy(
|
||||
status = batchListState.status,
|
||||
uiBatches = uiManager.createOrUpdateUiBatches(
|
||||
newCurrencyBatches = batchListState.data,
|
||||
clearUiBatches = clearUiBatches,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
|
||||
data class TxHistoryListState(
|
||||
val status: PaginationStatus<*> = PaginationStatus.None,
|
||||
val uiBatches: List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> = listOf(),
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
internal interface TxHistoryUiActions {
|
||||
|
||||
fun openExplorer()
|
||||
fun openTxInExplorer(txHash: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.pagination.Batch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import java.util.UUID
|
||||
|
||||
internal class TxHistoryUiManager(
|
||||
private val state: MutableStateFlow<TxHistoryListState>,
|
||||
private val txHistoryItemConverter: TxHistoryItemToTransactionStateConverter,
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val items: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = state
|
||||
.mapLatest { state ->
|
||||
state.uiBatches.asSequence()
|
||||
.flatMap { it.data }
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun createOrUpdateUiBatches(
|
||||
newCurrencyBatches: List<Batch<Int, PaginationWrapper<TxHistoryItem>>>,
|
||||
clearUiBatches: Boolean,
|
||||
): List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> {
|
||||
val currentUiBatches = state.value.uiBatches
|
||||
val batches = if (clearUiBatches) mutableListOf() else currentUiBatches.toMutableList()
|
||||
|
||||
for ((key, data) in newCurrencyBatches) {
|
||||
// Find if batch with same key exists
|
||||
val existingBatchIndex = batches.indexOfFirst { it.key == key }
|
||||
val shouldUpdateExisting = existingBatchIndex != -1 &&
|
||||
currentUiBatches[existingBatchIndex].data.transactionItemsSizeNotEqual(data.items)
|
||||
|
||||
// Case 1: Update existing batch if sizes differ
|
||||
if (shouldUpdateExisting) {
|
||||
val items = generateUiItems(key, data)
|
||||
batches[existingBatchIndex] = Batch(key = key, data = items)
|
||||
continue
|
||||
}
|
||||
|
||||
// Case 2: Skip if batch exists and has same size
|
||||
if (existingBatchIndex != -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Case 3: Create new batch
|
||||
val items = generateUiItems(key, data)
|
||||
batches.add(Batch(key = key, data = items))
|
||||
}
|
||||
|
||||
return batches
|
||||
}
|
||||
|
||||
private fun generateUiItems(key: Int, data: PaginationWrapper<TxHistoryItem>): List<TxHistoryUM.TxHistoryItemUM> {
|
||||
val items = mutableListOf<TxHistoryUM.TxHistoryItemUM>()
|
||||
|
||||
// Add title for the first batch
|
||||
if (key == 0) {
|
||||
items.add(TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = txHistoryUiActions::openExplorer))
|
||||
}
|
||||
|
||||
// Process batch items only if there are any
|
||||
if (data.items.isNotEmpty()) {
|
||||
// Add first item with its group title
|
||||
val firstItem = data.items.first()
|
||||
val firstDate = firstItem.timestampInMillis.toDateFormatWithTodayYesterday()
|
||||
|
||||
items.add(
|
||||
TxHistoryUM.TxHistoryItemUM.GroupTitle(
|
||||
title = firstDate,
|
||||
itemKey = UUID.randomUUID().toString(),
|
||||
),
|
||||
)
|
||||
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(firstItem)))
|
||||
|
||||
// Process remaining items with date separators when needed
|
||||
data.items.zipWithNext { current, next ->
|
||||
val currentDate = current.timestampInMillis.toDateFormatWithTodayYesterday()
|
||||
val nextDate = next.timestampInMillis.toDateFormatWithTodayYesterday()
|
||||
|
||||
if (currentDate != nextDate) {
|
||||
items.add(
|
||||
TxHistoryUM.TxHistoryItemUM.GroupTitle(
|
||||
title = nextDate,
|
||||
itemKey = UUID.randomUUID().toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(next)))
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
private fun List<TxHistoryUM.TxHistoryItemUM>.transactionItemsSizeNotEqual(
|
||||
txHistoryItems: List<TxHistoryItem>,
|
||||
): Boolean {
|
||||
return this.filterIsInstance<TxHistoryUM.TxHistoryItemUM.Transaction>().size != txHistoryItems.size
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue