Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-16 18:03:03 +04:00
parent 68842dcadc
commit 5b4f0214d2
9 changed files with 848 additions and 156 deletions

View file

@ -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(),
)
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
},
)
}
}

View file

@ -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")
}
}

View file

@ -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)
},
)
}

View file

@ -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)
},
)
}

View file

@ -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(),
)
}

View file

@ -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)
}