Updated on 2026-08-14
This commit is contained in:
parent
68842dcadc
commit
5b4f0214d2
9 changed files with 848 additions and 156 deletions
|
|
@ -1,53 +1,78 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.*
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.ManageTokensTopBarUM
|
||||
import com.tangem.features.managetokens.entity.ManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.utils.list.ChangedCurrencies
|
||||
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class ManageTokensModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val manageTokensListManager: ManageTokensListManager,
|
||||
private val messageSender: UiMessageSender,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: ManageTokensComponent.Params = paramsContainer.require()
|
||||
private val changedItemsIds: MutableSet<String> = mutableSetOf()
|
||||
private var items = initItems()
|
||||
|
||||
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(value = getInitialState(mode = params.mode))
|
||||
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(getInitialState(params.userWalletId))
|
||||
|
||||
private fun getInitialState(mode: ManageTokensComponent.Mode): ManageTokensUM {
|
||||
return when (mode) {
|
||||
ManageTokensComponent.Mode.READ_ONLY -> createReadContentModel()
|
||||
ManageTokensComponent.Mode.MANAGE -> createManageContentModel()
|
||||
init {
|
||||
manageTokensListManager.uiItems
|
||||
.onEach { items -> updateItems(items) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
manageTokensListManager.paginationStatus
|
||||
.onEach { status -> updatePaginationStatus(status) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
combine(
|
||||
manageTokensListManager.currenciesToAdd,
|
||||
manageTokensListManager.currenciesToRemove,
|
||||
::updateChangedItems,
|
||||
).launchIn(modelScope)
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launch(params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM {
|
||||
return if (userWalletId == null) {
|
||||
createReadContentModel()
|
||||
} else {
|
||||
createManageContentModel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createReadContentModel(): ManageTokensUM.ReadContent {
|
||||
return ManageTokensUM.ReadContent(
|
||||
popBack = router::pop,
|
||||
isLoading = false,
|
||||
items = initItems(),
|
||||
isInitialBatchLoading = true,
|
||||
isNextBatchLoading = false,
|
||||
items = getInitialItems(),
|
||||
topBar = ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(R.string.common_search_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
|
|
@ -59,14 +84,16 @@ internal class ManageTokensModel @Inject constructor(
|
|||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
loadMore = ::loadMoreItems,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createManageContentModel(): ManageTokensUM.ManageContent {
|
||||
return ManageTokensUM.ManageContent(
|
||||
popBack = router::pop,
|
||||
isLoading = false,
|
||||
items = initItems(),
|
||||
isInitialBatchLoading = true,
|
||||
isNextBatchLoading = false,
|
||||
items = getInitialItems(),
|
||||
topBar = ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
|
|
@ -82,11 +109,85 @@ internal class ManageTokensModel @Inject constructor(
|
|||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
onSaveClick = ::onSaveClick,
|
||||
hasChanges = false,
|
||||
saveChanges = ::onSaveClick,
|
||||
loadMore = ::loadMoreItems,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePaginationStatus(status: PaginationStatus<*>) {
|
||||
state.update { state ->
|
||||
when (status) {
|
||||
is PaginationStatus.InitialLoading -> {
|
||||
if (state.search.isActive) {
|
||||
state
|
||||
} else {
|
||||
state.copySealed(
|
||||
isInitialBatchLoading = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
is PaginationStatus.NextBatchLoading -> state.copySealed(
|
||||
isNextBatchLoading = true,
|
||||
)
|
||||
is PaginationStatus.InitialLoadingError -> {
|
||||
val message = SnackbarMessage(
|
||||
message = status.throwable.localizedMessage
|
||||
?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
state.copySealed(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
is PaginationStatus.None,
|
||||
is PaginationStatus.Paginating,
|
||||
is PaginationStatus.EndOfPagination,
|
||||
-> state.copySealed(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) {
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMoreItems(): Boolean {
|
||||
val state = state.value
|
||||
if (state.isInitialBatchLoading) return false
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.loadMore(
|
||||
userWalletId = params.userWalletId,
|
||||
query = state.search.query,
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getInitialItems(): ImmutableList<CurrencyItemUM> {
|
||||
return persistentListOf()
|
||||
}
|
||||
|
||||
private fun onAddCustomToken() {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
}
|
||||
|
|
@ -95,149 +196,35 @@ internal class ManageTokensModel @Inject constructor(
|
|||
// TODO: [REDACTED_JIRA]
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun searchCurrencies(query: String) {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
val newItems = if (query.isBlank()) {
|
||||
initItems()
|
||||
} else {
|
||||
state.value.items.filter { currency ->
|
||||
currency.model.name.contains(query, ignoreCase = true)
|
||||
}.toPersistentList()
|
||||
}
|
||||
state.update { state ->
|
||||
state.copySealed(search = state.search.copy(query = query), items = newItems)
|
||||
state.copySealed(
|
||||
search = state.search.copy(
|
||||
query = query,
|
||||
isActive = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.search(params.userWalletId, query)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
search = state.search.copy(isActive = isActive),
|
||||
search = state.search.copy(
|
||||
query = if (isActive) state.search.query else "",
|
||||
isActive = isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initItems() = List(size = 30) { index ->
|
||||
if (index < 2) {
|
||||
getCustomItem(index)
|
||||
} else {
|
||||
getBasicItem(index)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun getCustomItem(index: Int) = CurrencyItemUM.Custom(
|
||||
id = index.toString(),
|
||||
model = ChainRowUM(
|
||||
name = "Custom token $index",
|
||||
type = "CT$index",
|
||||
icon = CurrencyIconState.CustomTokenIcon(
|
||||
tint = Color.White,
|
||||
background = Color.Black,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = true,
|
||||
),
|
||||
showCustom = true,
|
||||
),
|
||||
onRemoveClick = {},
|
||||
)
|
||||
|
||||
private fun getBasicItem(index: Int) = CurrencyItemUM.Basic(
|
||||
id = index.toString(),
|
||||
model = ChainRowUM(
|
||||
name = "Currency $index",
|
||||
type = "C$index",
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_btc_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
showCustom = false,
|
||||
),
|
||||
networks = if (index == 2) {
|
||||
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index))
|
||||
} else {
|
||||
CurrencyItemUM.Basic.NetworksUM.Collapsed
|
||||
},
|
||||
onExpandClick = { toggleCurrency(index) },
|
||||
)
|
||||
|
||||
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
|
||||
CurrencyNetworkUM(
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) },
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
private fun toggleCurrency(index: Int) {
|
||||
val updatedItem = when (val item = items[index]) {
|
||||
is CurrencyItemUM.Basic -> item.copy(
|
||||
networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) {
|
||||
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index))
|
||||
} else {
|
||||
CurrencyItemUM.Basic.NetworksUM.Collapsed
|
||||
},
|
||||
)
|
||||
is CurrencyItemUM.Custom -> return
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
items = items.mutate {
|
||||
it[index] = updatedItem
|
||||
modelScope.launch {
|
||||
if (!isActive) {
|
||||
manageTokensListManager.reload(params.userWalletId)
|
||||
}
|
||||
state.copySealed(items = items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) {
|
||||
val updatedItem = when (val item = items[currencyIndex]) {
|
||||
is CurrencyItemUM.Basic -> {
|
||||
val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded)
|
||||
?.copy(
|
||||
networks = item.networks.networks.toPersistentList().mutate {
|
||||
it.fastForEachIndexed { index, network ->
|
||||
if (index == networkIndex) {
|
||||
it[index] = network.copy(
|
||||
iconResId = if (isSelected) {
|
||||
R.drawable.img_eth_22
|
||||
} else {
|
||||
R.drawable.ic_eth_16
|
||||
},
|
||||
isSelected = isSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
?: return
|
||||
|
||||
item.copy(networks = updatedNetworks)
|
||||
}
|
||||
is CurrencyItemUM.Custom -> return
|
||||
}
|
||||
|
||||
val id = "${currencyIndex}_$networkIndex"
|
||||
if (changedItemsIds.contains(id)) {
|
||||
changedItemsIds.remove(id)
|
||||
} else {
|
||||
changedItemsIds.add(id)
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
items = items.mutate {
|
||||
it[currencyIndex] = updatedItem
|
||||
}
|
||||
state.copySealed(
|
||||
items = items,
|
||||
hasChanges = changedItemsIds.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal typealias ChangedCurrencies = Map<ManagedCryptoCurrency.ID, Set<Network.ID>>
|
||||
|
||||
internal interface ChangedCurrenciesManager {
|
||||
|
||||
val currenciesToAdd: MutableStateFlow<ChangedCurrencies>
|
||||
val currenciesToRemove: MutableStateFlow<ChangedCurrencies>
|
||||
|
||||
fun updateChangedItems(
|
||||
currencyId: ManagedCryptoCurrency.ID,
|
||||
networkId: Network.ID,
|
||||
removeFromIfPresent: MutableStateFlow<ChangedCurrencies>,
|
||||
addToIfNotPresent: MutableStateFlow<ChangedCurrencies>,
|
||||
) {
|
||||
val present = removeFromIfPresent.value[currencyId].orEmpty()
|
||||
|
||||
if (networkId in present) {
|
||||
removeFromIfPresent.update { items ->
|
||||
items.toMutableMap().apply {
|
||||
val ids = present - networkId
|
||||
|
||||
if (ids.isEmpty()) {
|
||||
remove(currencyId)
|
||||
} else {
|
||||
set(currencyId, ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addToIfNotPresent.update { items ->
|
||||
val alreadyAdded = items[currencyId] ?: emptySet()
|
||||
if (networkId in alreadyAdded) {
|
||||
return@update items
|
||||
}
|
||||
|
||||
items + (currencyId to alreadyAdded + networkId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.managetokens.GetManagedTokensUseCase
|
||||
import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext
|
||||
import com.tangem.domain.managetokens.model.ManageTokensListConfig
|
||||
import com.tangem.domain.managetokens.model.ManageTokensUpdateAction
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.tokens.CheckHasLinkedTokensUseCase
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
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.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class ManageTokensListManager @Inject constructor(
|
||||
private val getManagedTokensUseCase: GetManagedTokensUseCase,
|
||||
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ChangedCurrenciesManager,
|
||||
ManageTokensUiManager(
|
||||
messageSender = messageSender,
|
||||
dispatchers = dispatchers,
|
||||
) {
|
||||
|
||||
override lateinit var scope: CoroutineScope
|
||||
|
||||
private val jobHolder = JobHolder()
|
||||
private val actionsFlow = MutableSharedFlow<ManageTokensBatchAction>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
override val state: MutableStateFlow<ManageTokensListState> = MutableStateFlow(ManageTokensListState())
|
||||
|
||||
val paginationStatus: MutableStateFlow<PaginationStatus<*>> = MutableStateFlow(PaginationStatus.None)
|
||||
|
||||
override val currenciesToAdd: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
|
||||
override val currenciesToRemove: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = state
|
||||
.mapLatest { state ->
|
||||
state.uiBatches.asSequence()
|
||||
.flatMap { it.data }
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
suspend fun launch(userWalletId: UserWalletId?) = coroutineScope {
|
||||
scope = this
|
||||
|
||||
val batchFlow = getManagedTokensUseCase(
|
||||
context = ManageTokensListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = this,
|
||||
),
|
||||
)
|
||||
|
||||
batchFlow.state
|
||||
.onEach { state -> updateState(state, userWalletId) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
|
||||
// Initial load
|
||||
reload(userWalletId)
|
||||
}
|
||||
|
||||
suspend fun reload(userWalletId: UserWalletId?) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = ManageTokensListConfig(userWalletId, searchText = null),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadMore(userWalletId: UserWalletId?, query: String) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.LoadMore(
|
||||
requestParams = ManageTokensListConfig(userWalletId, query),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun search(userWalletId: UserWalletId?, query: String) {
|
||||
state.value = ManageTokensListState()
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = ManageTokensListConfig(
|
||||
userWalletId = userWalletId,
|
||||
searchText = query,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateState(
|
||||
batchListState: BatchListState<Int, List<ManagedCryptoCurrency>>,
|
||||
userWalletId: UserWalletId?,
|
||||
) {
|
||||
paginationStatus.value = batchListState.status
|
||||
|
||||
state.update { state ->
|
||||
val newBatches = batchListState.data
|
||||
val currentBatches = state.currencyBatches
|
||||
|
||||
// Distinct until changed
|
||||
if (newBatches.size == currentBatches.size &&
|
||||
newBatches.map { it.key } == currentBatches.map { it.key } &&
|
||||
newBatches.flatMap { it.data } == currentBatches.flatMap { it.data }
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
val canEditItems = userWalletId != null
|
||||
state.copy(
|
||||
userWalletId = userWalletId,
|
||||
currencyBatches = newBatches,
|
||||
uiBatches = getUiBatches(newBatches, canEditItems),
|
||||
canEditItems = canEditItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
|
||||
updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd)
|
||||
|
||||
sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true)
|
||||
}
|
||||
|
||||
override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
|
||||
updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove)
|
||||
|
||||
sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false)
|
||||
}
|
||||
|
||||
override fun checkNeedToShowRemoveNetworkWarning(
|
||||
currencyId: ManagedCryptoCurrency.ID,
|
||||
networkId: Network.ID,
|
||||
): Boolean = networkId !in currenciesToRemove.value[currencyId].orEmpty() &&
|
||||
networkId !in currenciesToAdd.value[currencyId].orEmpty()
|
||||
|
||||
private fun sendSelectCurrencyAction(
|
||||
batchKey: Int,
|
||||
currencyId: ManagedCryptoCurrency.ID,
|
||||
networkId: Network.ID,
|
||||
isSelected: Boolean,
|
||||
) {
|
||||
val request = ManageTokensUpdateAction.AddCurrency(
|
||||
currencyId = currencyId,
|
||||
networkId = networkId,
|
||||
isSelected = isSelected,
|
||||
)
|
||||
val action = BatchAction.UpdateBatches(
|
||||
keys = setOf(batchKey),
|
||||
async = true,
|
||||
updateRequest = request,
|
||||
)
|
||||
|
||||
actionsFlow.tryEmit(action)
|
||||
}
|
||||
|
||||
override suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
return checkHasLinkedTokensUseCase(userWalletId, network).getOrElse {
|
||||
Timber.e(
|
||||
it,
|
||||
"""
|
||||
Failed to check linked tokens
|
||||
|- User wallet ID: $userWalletId
|
||||
|- Network ID: ${network.id}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val message = SnackbarMessage(
|
||||
message = it.localizedMessage
|
||||
?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import com.tangem.domain.managetokens.model.ManageTokensListConfig
|
||||
import com.tangem.domain.managetokens.model.ManageTokensUpdateAction
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
|
||||
internal typealias ManageTokensBatchAction = BatchAction<Int, ManageTokensListConfig, ManageTokensUpdateAction>
|
||||
|
||||
internal data class ManageTokensListState(
|
||||
val userWalletId: UserWalletId? = null,
|
||||
val uiBatches: List<Batch<Int, List<CurrencyItemUM>>> = mutableListOf(),
|
||||
val currencyBatches: List<Batch<Int, List<ManagedCryptoCurrency>>> = mutableListOf(),
|
||||
val canEditItems: Boolean = true,
|
||||
) {
|
||||
|
||||
fun batchIndexByCurrencyId(currencyId: ManagedCryptoCurrency.ID): Int {
|
||||
return currencyBatches
|
||||
.indexOfFirst { batch -> batch.data.any { it.id == currencyId } }
|
||||
.takeIf { it != -1 }
|
||||
?: error("Batch with currency '$currencyId' not found")
|
||||
}
|
||||
|
||||
fun updateUiBatchesItem(
|
||||
indexToBatch: Pair<Int, Batch<Int, List<CurrencyItemUM>>>,
|
||||
indexToItem: Pair<Int, CurrencyItemUM>,
|
||||
): ManageTokensListState {
|
||||
val updatedUiBatch = indexToBatch.second.copy(
|
||||
data = indexToBatch.second.data.toMutableList().apply {
|
||||
set(indexToItem.first, indexToItem.second)
|
||||
},
|
||||
)
|
||||
|
||||
return copy(
|
||||
uiBatches = uiBatches.toMutableList().apply {
|
||||
set(indexToBatch.first, updatedUiBatch)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.message.ContentMessage
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning
|
||||
import com.tangem.features.managetokens.ui.dialog.HideTokenWarning
|
||||
import com.tangem.features.managetokens.utils.mapper.toUiModel
|
||||
import com.tangem.features.managetokens.utils.ui.toggleExpanded
|
||||
import com.tangem.features.managetokens.utils.ui.update
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal abstract class ManageTokensUiManager(
|
||||
private val messageSender: UiMessageSender,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
abstract val scope: CoroutineScope
|
||||
abstract val state: MutableStateFlow<ManageTokensListState>
|
||||
|
||||
protected fun getUiBatches(
|
||||
newCurrencyBatches: List<Batch<Int, List<ManagedCryptoCurrency>>>,
|
||||
canEditItems: Boolean,
|
||||
): List<Batch<Int, List<CurrencyItemUM>>> {
|
||||
val currentUiBatches = state.value.uiBatches
|
||||
val batches = currentUiBatches.toMutableList()
|
||||
|
||||
newCurrencyBatches.forEach { (key, data) ->
|
||||
val indexToUpdate = currentUiBatches.indexOfFirst { it.key == key }
|
||||
|
||||
if (indexToUpdate == -1) {
|
||||
val newBatch = Batch(
|
||||
key = key,
|
||||
data = data.map { item ->
|
||||
item.toUiModel(
|
||||
isEditable = canEditItems,
|
||||
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
|
||||
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
batches.add(newBatch)
|
||||
} else {
|
||||
val uiBatchToUpdate = currentUiBatches[indexToUpdate]
|
||||
|
||||
if (uiBatchToUpdate.data == data) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val currentCurrencyBatches = state.value.currencyBatches
|
||||
val currencyBatch = currentCurrencyBatches[indexToUpdate]
|
||||
val updatedBatch = uiBatchToUpdate.copy(
|
||||
data = data.mapIndexed { index, item ->
|
||||
if (item == currencyBatch.data[index]) {
|
||||
return@mapIndexed uiBatchToUpdate.data[index]
|
||||
}
|
||||
|
||||
val previousUiItem = uiBatchToUpdate.data.getOrNull(index)
|
||||
if (previousUiItem == null || previousUiItem.id != item.id) {
|
||||
item.toUiModel(
|
||||
isEditable = canEditItems,
|
||||
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
|
||||
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
|
||||
)
|
||||
} else {
|
||||
previousUiItem.update(item)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
batches[indexToUpdate] = updatedBatch
|
||||
}
|
||||
}
|
||||
|
||||
return batches
|
||||
}
|
||||
|
||||
private fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) = scope.launch(dispatchers.default) {
|
||||
showRemoveNetworkWarning(
|
||||
currency = currency,
|
||||
network = currency.network,
|
||||
isCoin = currency is ManagedCryptoCurrency.Custom.Coin,
|
||||
onConfirm = {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch(
|
||||
dispatchers.default,
|
||||
) {
|
||||
state.update { batches ->
|
||||
val batchIndex = batches.batchIndexByCurrencyId(currency.id)
|
||||
val currencyBatch = batches.currencyBatches[batchIndex]
|
||||
val currencyIndex = currencyBatch.currencyIndexById(currency.id)
|
||||
|
||||
val uiBatch = batches.uiBatches[batchIndex]
|
||||
val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded(
|
||||
currency = currencyBatch.data[currencyIndex],
|
||||
isEditable = batches.canEditItems,
|
||||
onSelectCurrencyNetwork = { networkId, isSelected ->
|
||||
selectNetwork(currencyBatch.key, currency, networkId, isSelected)
|
||||
},
|
||||
)
|
||||
|
||||
batches.updateUiBatchesItem(
|
||||
indexToBatch = batchIndex to uiBatch,
|
||||
indexToItem = currencyIndex to updatedUiItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectNetwork(
|
||||
batchKey: Int,
|
||||
currency: ManagedCryptoCurrency,
|
||||
source: ManagedCryptoCurrency.SourceNetwork,
|
||||
isSelected: Boolean,
|
||||
) = scope.launch(dispatchers.default) {
|
||||
if (currency !is ManagedCryptoCurrency.Token) return@launch
|
||||
|
||||
if (isSelected) {
|
||||
addCurrency(batchKey, currency.id, source.id)
|
||||
} else {
|
||||
if (checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) {
|
||||
showRemoveNetworkWarning(
|
||||
currency = currency,
|
||||
network = source.network,
|
||||
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
|
||||
onConfirm = {
|
||||
removeCurrency(batchKey, currency.id, source.id)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
removeCurrency(batchKey, currency.id, source.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID)
|
||||
|
||||
protected abstract fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID)
|
||||
|
||||
protected abstract fun checkNeedToShowRemoveNetworkWarning(
|
||||
currencyId: ManagedCryptoCurrency.ID,
|
||||
networkId: Network.ID,
|
||||
): Boolean
|
||||
|
||||
private suspend fun showRemoveNetworkWarning(
|
||||
currency: ManagedCryptoCurrency,
|
||||
network: Network,
|
||||
isCoin: Boolean,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
val userWalletId = state.value.userWalletId
|
||||
val hasLinkedTokens = if (userWalletId == null || !isCoin) {
|
||||
false
|
||||
} else {
|
||||
checkHasLinkedTokens(userWalletId, network)
|
||||
}
|
||||
|
||||
val message = ContentMessage { onDismiss ->
|
||||
if (hasLinkedTokens) {
|
||||
HasLinkedTokensWarning(
|
||||
currency = currency,
|
||||
network = network,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
} else {
|
||||
HideTokenWarning(
|
||||
currency = currency,
|
||||
onConfirm = {
|
||||
onConfirm()
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
protected abstract suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean
|
||||
|
||||
private fun Batch<Int, List<ManagedCryptoCurrency>>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int {
|
||||
return data
|
||||
.indexOfFirst { it.id == id }
|
||||
.takeIf { it != -1 }
|
||||
?: error("Currency with currency '$id' not found in batch #$key")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.features.managetokens.utils.mapper
|
||||
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.getTintForTokenIcon
|
||||
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM
|
||||
import com.tangem.features.managetokens.utils.ui.getIconRes
|
||||
|
||||
internal fun ManagedCryptoCurrency.toUiModel(
|
||||
isEditable: Boolean,
|
||||
onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit,
|
||||
onRemoveCustomCurrencyClick: (ManagedCryptoCurrency.Custom) -> Unit,
|
||||
): CurrencyItemUM = when (this) {
|
||||
is ManagedCryptoCurrency.Custom -> toUiModel(onRemoveCustomCurrencyClick)
|
||||
is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onExpandNetworksClick)
|
||||
}
|
||||
|
||||
private fun ManagedCryptoCurrency.Custom.toUiModel(
|
||||
onRemoveCustomCurrency: (ManagedCryptoCurrency.Custom) -> Unit,
|
||||
): CurrencyItemUM = CurrencyItemUM.Custom(
|
||||
id = id,
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
icon = when (this) {
|
||||
is ManagedCryptoCurrency.Custom.Coin -> {
|
||||
CurrencyIconState.CoinIcon(
|
||||
url = iconUrl,
|
||||
fallbackResId = network.id.getIconRes(isColored = true),
|
||||
isGrayscale = false,
|
||||
showCustomBadge = true,
|
||||
)
|
||||
}
|
||||
is ManagedCryptoCurrency.Custom.Token -> {
|
||||
val background = tryGetBackgroundForTokenIcon(contractAddress)
|
||||
|
||||
CurrencyIconState.TokenIcon(
|
||||
url = iconUrl,
|
||||
fallbackBackground = background,
|
||||
fallbackTint = getTintForTokenIcon(background),
|
||||
topBadgeIconResId = network.id.getIconRes(isColored = true),
|
||||
isGrayscale = false,
|
||||
showCustomBadge = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
onRemoveClick = {
|
||||
onRemoveCustomCurrency(this)
|
||||
},
|
||||
)
|
||||
|
||||
private fun ManagedCryptoCurrency.Token.toUiModel(
|
||||
isEditable: Boolean,
|
||||
onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit,
|
||||
): CurrencyItemUM {
|
||||
val background = TangemColorPalette.Black
|
||||
|
||||
return CurrencyItemUM.Basic(
|
||||
id = id,
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
icon = CurrencyIconState.TokenIcon(
|
||||
url = iconUrl,
|
||||
topBadgeIconResId = null,
|
||||
isGrayscale = if (isEditable) !isAdded else false,
|
||||
showCustomBadge = false,
|
||||
fallbackTint = getTintForTokenIcon(background),
|
||||
fallbackBackground = background,
|
||||
),
|
||||
networks = NetworksUM.Collapsed,
|
||||
onExpandClick = {
|
||||
onExpandNetworksClick(this)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.managetokens.utils.mapper
|
||||
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM
|
||||
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
|
||||
import com.tangem.features.managetokens.utils.ui.getIconRes
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal fun ManagedCryptoCurrency.Token.toUiNetworksModel(
|
||||
isExpanded: Boolean,
|
||||
isItemsEditable: Boolean,
|
||||
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
|
||||
): NetworksUM {
|
||||
return if (isExpanded) {
|
||||
NetworksUM.Expanded(
|
||||
networks = availableNetworks.map {
|
||||
it.toUiModel(
|
||||
isSelected = it.id in addedIn,
|
||||
isEditable = isItemsEditable,
|
||||
onSelectedStateChange = onSelectedStateChange,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
} else {
|
||||
NetworksUM.Collapsed
|
||||
}
|
||||
}
|
||||
|
||||
private fun SourceNetwork.toUiModel(
|
||||
isSelected: Boolean,
|
||||
isEditable: Boolean,
|
||||
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
|
||||
): CurrencyNetworkUM {
|
||||
return CurrencyNetworkUM(
|
||||
id = id,
|
||||
name = network.name.uppercase(),
|
||||
iconResId = id.getIconRes(isColored = isSelected || !isEditable),
|
||||
isSelected = isSelected || !isEditable,
|
||||
type = typeName,
|
||||
isMainNetwork = this is SourceNetwork.Main,
|
||||
onSelectedStateChange = { selected ->
|
||||
onSelectedStateChange(this, selected)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.features.managetokens.utils.ui
|
||||
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM
|
||||
import com.tangem.features.managetokens.utils.mapper.toUiNetworksModel
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal fun CurrencyItemUM.toggleExpanded(
|
||||
currency: ManagedCryptoCurrency,
|
||||
isEditable: Boolean,
|
||||
onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit,
|
||||
): CurrencyItemUM {
|
||||
if (currency !is ManagedCryptoCurrency.Token) return this
|
||||
|
||||
return when (this) {
|
||||
is CurrencyItemUM.Custom -> this
|
||||
is CurrencyItemUM.Basic -> {
|
||||
val isExpanded = networks !is NetworksUM.Expanded
|
||||
|
||||
copy(
|
||||
icon = icon.copySealed(
|
||||
isGrayscale = if (isEditable) !currency.isAdded && !isExpanded else false,
|
||||
),
|
||||
networks = currency.toUiNetworksModel(
|
||||
isExpanded = isExpanded,
|
||||
isItemsEditable = isEditable,
|
||||
onSelectedStateChange = onSelectCurrencyNetwork,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CurrencyItemUM.update(currency: ManagedCryptoCurrency): CurrencyItemUM {
|
||||
return when (this) {
|
||||
is CurrencyItemUM.Custom -> this
|
||||
is CurrencyItemUM.Basic -> {
|
||||
if (currency !is ManagedCryptoCurrency.Token) {
|
||||
return this
|
||||
}
|
||||
|
||||
copy(
|
||||
icon = icon.copySealed(
|
||||
isGrayscale = networks is NetworksUM.Collapsed && !currency.isAdded,
|
||||
),
|
||||
networks = updateNetworks(currency),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency.Token): NetworksUM = when (networks) {
|
||||
is NetworksUM.Collapsed -> networks
|
||||
is NetworksUM.Expanded -> networks.copy(
|
||||
networks = networks.networks.map { network ->
|
||||
val isSelected = network.id in currency.addedIn
|
||||
|
||||
network.copy(
|
||||
iconResId = network.id.getIconRes(isSelected),
|
||||
isSelected = isSelected,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.managetokens.utils.ui
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.getGreyedOutIconRes
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
|
||||
|
||||
internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM {
|
||||
return copy(
|
||||
iconResId = id.getIconRes(isSelected),
|
||||
isSelected = isSelected,
|
||||
)
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
internal fun Network.ID.getIconRes(isColored: Boolean): Int = if (isColored) {
|
||||
getActiveIconRes(value)
|
||||
} else {
|
||||
getGreyedOutIconRes(value)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue