Updated on 2026-08-14
This commit is contained in:
parent
64a40de825
commit
4a25e38ccc
12 changed files with 151 additions and 28 deletions
|
|
@ -46,14 +46,36 @@ abstract class Model : InstanceKeeper.Instance {
|
|||
progressFlow: MutableSharedFlow<Boolean>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,11 +44,24 @@ inline fun <T> MutableList<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boo
|
|||
*/
|
||||
inline fun <T> List<T>.addOrReplace(item: T, predicate: (T) -> Boolean): List<T> {
|
||||
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 <T> MutableList<T>.addOrReplace(item: T, predicate: (T) -> Boolean) {
|
||||
val isReplaced = replaceBy(item, predicate)
|
||||
|
||||
if (!isReplaced) {
|
||||
add(item)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ interface AddCustomTokenComponent : ComposableBottomSheetComponent {
|
|||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val onDismiss: () -> Unit,
|
||||
val onCurrencyAdded: () -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, AddCustomTokenComponent>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Params, CustomTokenFormComponent>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor(
|
|||
params = AddCustomTokenComponent.Params(
|
||||
userWalletId = config.userWalletId,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
onCurrencyAdded = model::reloadList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Unit>
|
||||
|
||||
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<Unit> = 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<Unit> = 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<Unit> = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CustomTokenFormUM> = 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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<CurrencyItemUM>,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue