diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt index c845658265..b1337c72f9 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt @@ -46,14 +46,36 @@ abstract class Model : InstanceKeeper.Instance { progressFlow: MutableSharedFlow, dispatcher: CoroutineDispatcher = dispatchers.mainImmediate, crossinline block: suspend () -> Unit, + ): Job = resource( + acquire = { progressFlow.emit(true) }, + release = { progressFlow.emit(false) }, + dispatcher = dispatcher, + block = block, + ) + + /** + * Launches [block] in the model's scope and acquires a resource before executing the block and releases it after. + * + * @param acquire The block of code to acquire the resource. + * @param release The block of code to release the resource. + * @param dispatcher The [CoroutineDispatcher] to launch the coroutine. Default is [Dispatchers.Main.immediate]. + * @param block The block of code to execute. + * + * @return The [Job] of the launched coroutine. + * */ + protected inline fun resource( + crossinline acquire: suspend () -> Unit, + crossinline release: suspend () -> Unit, + dispatcher: CoroutineDispatcher = dispatchers.mainImmediate, + crossinline block: suspend () -> Unit, ): Job = modelScope.launch(dispatcher) { - progressFlow.emit(value = true) + acquire() try { block() } finally { withContext(NonCancellable) { - progressFlow.emit(value = false) + release() } } } diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt index e0fbdb5d90..86bdfe2e6e 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt @@ -44,11 +44,24 @@ inline fun MutableList.replaceBy(item: T, predicate: (T) -> Boolean): Boo */ inline fun List.addOrReplace(item: T, predicate: (T) -> Boolean): List { val mutableList = this.toMutableList() - val isReplaced = mutableList.replaceBy(item, predicate) - if (!isReplaced) { - mutableList.add(item) - } + mutableList.addOrReplace(item, predicate) return mutableList +} + +/** + * Adds the specified element to the mutable list or replaces an existing element. + * + * !!!This function is not thread-safe!!! + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + */ +inline fun MutableList.addOrReplace(item: T, predicate: (T) -> Boolean) { + val isReplaced = replaceBy(item, predicate) + + if (!isReplaced) { + add(item) + } } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt index 63a0f48e26..4539527ce5 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt @@ -9,6 +9,7 @@ interface AddCustomTokenComponent : ComposableBottomSheetComponent { data class Params( val userWalletId: UserWalletId, val onDismiss: () -> Unit, + val onCurrencyAdded: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index a8d50aa900..a506043ad2 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.manageTokens) + implementation(projects.domain.card) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index 3f6d401d30..e9fc3c448f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -16,6 +16,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent { val formValues: CustomTokenFormValues, val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, + val onCurrencyAdded: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index 48e7a1b01f..3679e6f929 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -131,6 +131,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( formValues = config.formValues, onSelectNetworkClick = ::showNetworkSelector, onSelectDerivationPathClick = ::showDerivationPathSelector, + onCurrencyAdded = ::dismissAndNotify, ), ) } @@ -170,6 +171,11 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( navigation.replaceAll(config) } + private fun dismissAndNotify() { + dismiss() + params.onCurrencyAdded() + } + @AssistedFactory interface Factory : AddCustomTokenComponent.Factory { override fun create( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index 9d90bf8789..98df4a4346 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -62,6 +62,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( params = AddCustomTokenComponent.Params( userWalletId = config.userWalletId, onDismiss = model.bottomSheetNavigation::dismiss, + onCurrencyAdded = model::reloadList, ), ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index 33f1524452..8a0bf34154 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -2,6 +2,8 @@ package com.tangem.features.managetokens.entity.managetokens import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.features.managetokens.entity.item.CurrencyItemUM import kotlinx.collections.immutable.ImmutableList @@ -15,6 +17,7 @@ internal sealed class ManageTokensUM { abstract val topBar: ManageTokensTopBarUM abstract val search: SearchBarUM abstract val loadMore: () -> Boolean + abstract val scrollToTop: StateEvent data class ReadContent( override val popBack: () -> Unit, @@ -24,6 +27,7 @@ internal sealed class ManageTokensUM { override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, override val loadMore: () -> Boolean, + override val scrollToTop: StateEvent = consumedEvent(), ) : ManageTokensUM() data class ManageContent( @@ -34,6 +38,7 @@ internal sealed class ManageTokensUM { override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, override val loadMore: () -> Boolean, + override val scrollToTop: StateEvent = consumedEvent(), val saveChanges: () -> Unit, val hasChanges: Boolean, val isSavingInProgress: Boolean, @@ -46,6 +51,7 @@ internal sealed class ManageTokensUM { isInitialBatchLoading: Boolean = this.isInitialBatchLoading, isNextBatchLoading: Boolean = this.isNextBatchLoading, isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, + scrollToTop: StateEvent = this.scrollToTop, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -55,12 +61,14 @@ internal sealed class ManageTokensUM { isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, isSavingInProgress = isSavingInProgress, + scrollToTop = scrollToTop, ) is ReadContent -> copy( search = search, items = items, isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, + scrollToTop = scrollToTop, ) } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 0565adb15e..25fbf11d48 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.managetokens.model import androidx.compose.ui.res.stringResource +import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,7 +10,9 @@ import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM @@ -31,11 +34,14 @@ import javax.inject.Inject internal class CustomTokenFormModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val customCurrencyValidator: CustomCurrencyValidator, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, private val messageSender: UiMessageSender, paramsContainer: ParamsContainer, ) : Model() { private val params: CustomTokenFormComponent.Params = paramsContainer.require() + private var createdCurrency: CryptoCurrency? = null val state: MutableStateFlow = MutableStateFlow( value = getInitialState(), @@ -78,17 +84,7 @@ internal class CustomTokenFormModel @Inject constructor( }, onClick = ::selectDerivationPath, ), - saveToken = { - // TODO: Save token: [REDACTED_JIRA] - val dialog = ContentMessage { onDismiss -> - SimpleOkDialog( - message = "Not yet implemented", - onDismissDialog = onDismiss, - ) - } - - messageSender.send(dialog) - }, + saveToken = ::addCurrency, ) } @@ -117,6 +113,8 @@ internal class CustomTokenFormModel @Inject constructor( private fun observeValidatorUpdates() = modelScope.launch { customCurrencyValidator.consumeUpdates { validatorState -> + createdCurrency = null + when (validatorState) { is CustomCurrencyValidator.State.NotStarted -> Unit is CustomCurrencyValidator.State.SearchingToken -> updateStateWithSearching() @@ -125,12 +123,16 @@ internal class CustomTokenFormModel @Inject constructor( exceptions = validatorState.exceptions, ) is CustomCurrencyValidator.State.TokenNotFound -> updateStateWithNotFoundNotification() - is CustomCurrencyValidator.State.Validated -> updateStateWithCurrency( - currency = validatorState.currency, - fillForm = validatorState.fillForm, - isAlreadyAdded = validatorState.isAlreadyAdded, - isCustom = validatorState.isCustom, - ) + is CustomCurrencyValidator.State.Validated -> { + createdCurrency = validatorState.currency + + updateStateWithCurrency( + currency = validatorState.currency, + fillForm = validatorState.fillForm, + isAlreadyAdded = validatorState.isAlreadyAdded, + isCustom = validatorState.isCustom, + ) + } } } } @@ -296,6 +298,40 @@ internal class CustomTokenFormModel @Inject constructor( } } + private fun addCurrency() = resource( + acquire = { + state.update { state -> + state.updateWithProgress(showProgress = true) + } + }, + release = { + state.update { state -> + state.updateWithProgress(showProgress = false) + } + }, + ) { + val currency = createdCurrency + if (currency == null) { + Timber.e("Trying to add currency without validation") + showErrorDialog() + return@resource + } + + derivePublicKeysUseCase(params.userWalletId, listOf(currency)).getOrElse { + Timber.e(it, "Failed to derive public keys") + showErrorDialog() + return@resource + } + + addCryptoCurrenciesUseCase(params.userWalletId, currency).getOrElse { + Timber.e(it, "Failed to add currency") + showErrorDialog() + return@resource + } + + params.onCurrencyAdded() + } + private fun selectNetwork() { params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm)) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 87e6636653..b5a235ee3a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -9,6 +9,8 @@ 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.fields.entity.SearchBarUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage @@ -67,6 +69,12 @@ internal class ManageTokensModel @Inject constructor( } } + fun reloadList() { + modelScope.launch { + manageTokensListManager.reload(params.userWalletId) + } + } + private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM { return if (userWalletId == null) { createReadContentModel() @@ -166,9 +174,18 @@ internal class ManageTokensModel @Inject constructor( (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> Timber.e(fetchError.throwable) } + state.copySealed( isInitialBatchLoading = false, isNextBatchLoading = false, + scrollToTop = if (state.isInitialBatchLoading && state.items.isNotEmpty()) { + triggeredEvent( + data = Unit, + onConsume = ::consumeScrollToTopEvent, + ) + } else { + state.scrollToTop + }, ) } is PaginationStatus.EndOfPagination -> state.copySealed( @@ -179,6 +196,14 @@ internal class ManageTokensModel @Inject constructor( } } + private fun consumeScrollToTopEvent() { + this.state.update { state -> + state.copySealed( + scrollToTop = consumedEvent(), + ) + } + } + private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { state.update { state -> state.copySealed( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 9769590b55..1cff9a28cb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.CircularProgressIndicator @@ -45,6 +46,7 @@ import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalSnackbarHostState @@ -166,9 +168,12 @@ private fun SaveChangesButton( @Composable private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + Box(modifier = modifier) { Currencies( modifier = Modifier.fillMaxSize(), + listState = listState, items = state.items, showLoadingItem = state.isNextBatchLoading, onLoadMore = state.loadMore, @@ -185,10 +190,15 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { ) } } + + EventEffect(event = state.scrollToTop) { + listState.animateScrollToItem(index = 0) + } } @Composable private fun Currencies( + listState: LazyListState, items: ImmutableList, showLoadingItem: Boolean, isEditable: Boolean, @@ -198,7 +208,6 @@ private fun Currencies( val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(density = this).toDp() } - val listState = rememberLazyListState() LazyColumn( modifier = modifier, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 78e3d1011b..1cc07f7286 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -13,6 +13,7 @@ import com.tangem.features.managetokens.utils.ui.update import com.tangem.pagination.Batch import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -49,8 +50,9 @@ internal class ManageTokensUiManager( newCurrencyBatches.forEach { (key, data) -> val indexToUpdate = currentUiBatches.indexOfFirst { it.key == key } + val currencyBatch = state.value.currencyBatches.getOrNull(indexToUpdate) - if (indexToUpdate == -1) { + if (indexToUpdate == -1 || currencyBatch == null) { val newBatch = Batch( key = key, data = data.map { item -> @@ -62,7 +64,7 @@ internal class ManageTokensUiManager( }, ) - batches.add(newBatch) + batches.addOrReplace(newBatch) { it.key == key } } else { val uiBatchToUpdate = currentUiBatches[indexToUpdate] @@ -70,8 +72,6 @@ internal class ManageTokensUiManager( 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]) {