From 5a3e53cb3b12640436632cac53cde7c910c7484c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Mar 2026 21:07:20 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Fade.kt | 20 +- .../ui/ds/button/SecondaryTangemButton.kt | 2 + .../organizetokens/OrganizeTokensComponent.kt | 43 ++ .../organizetokens/di/OrganizeTokensModule.kt | 6 + .../entity/OrganizeRowItemUM.kt | 116 ++++++ .../organizetokens/entity/OrganizeTokensUM.kt | 27 ++ .../model/CryptoCurrenciesIdsResolver.kt | 20 + .../child/organizetokens/model/Intents.kt | 2 + .../model/OrganizeTokensModel.kt | 283 +++++++++++++ .../model/common/DraggableItemOperations.kt | 8 + .../model/common/DraggableItemsOperations.kt | 90 +++++ .../converter/OrganizeTokensListConverter.kt | 82 ++++ .../items/OrganizeAccountItemConverter.kt | 35 ++ .../items/OrganizeNetworkItemConverter.kt | 25 ++ .../items/OrganizeTokenItemConverter.kt | 67 ++++ .../model/dnd/DragAndDropAdapter.kt | 217 ++++++++++ .../model/dnd/DragAndDropAdapterLegacy.kt | 3 + .../model/dnd/DraggableGroupsOperations.kt | 66 ++++ .../OrganizeContentStateTransformer.kt | 39 ++ ...rganizeDisableBalanceSortingTransformer.kt | 14 + ...OrganizeSortingProgressStateTransformer.kt | 31 ++ .../organizetokens/ui/OrganizeDropDownMenu.kt | 43 ++ .../ui/OrganizeTokensContent.kt | 371 ++++++++++++++++++ .../ui/preview/OrganizeTokensPreview.kt | 129 ++++++ .../wallet/child/wallet/WalletComponent.kt | 10 + .../router/DefaultWalletRouter.kt | 21 +- .../presentation/router/InnerWalletRouter.kt | 3 + .../wallet/state/model/WalletDialogConfig.kt | 3 + 28 files changed, 1763 insertions(+), 13 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeRowItemUM.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensUM.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/OrganizeTokensListConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeAccountItemConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeNetworkItemConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeTokenItemConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeContentStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeDisableBalanceSortingTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeSortingProgressStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeDropDownMenu.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt index ea58b6415a..9a4cb95448 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt @@ -39,22 +39,18 @@ fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemThe } /** - * A composable that draws a fade effect at the right end of the screen. Same as [BottomFade] - * but with a horizontal gradient. + * A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating + * elements and floating button at the bottom of the screen. */ @Composable -fun HorizontalFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { +fun BottomFade(gradientBrush: Brush, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + Box( modifier = modifier - .fillMaxHeight() - .background( - brush = Brush.horizontalGradient( - colors = listOf( - Color.Transparent, - backgroundColor, - ), - ), - ), + .fillMaxWidth() + .height(TangemTheme.dimens.size100 + bottomBarHeight) + .background(gradientBrush), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt index 95b2371aee..856d3ebce9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.R +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme @@ -83,6 +84,7 @@ fun SecondaryTangemButton( onClick = onClick, modifier = modifier .clip(shape.toShape(size)) + .hazeEffectTangem() .then(backgroundModifier), text = text, contentColor = contentColor, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt new file mode 100644 index 0000000000..2a9b249380 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.feature.wallet.child.organizetokens + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel +import com.tangem.feature.wallet.child.organizetokens.ui.OrganizeTokensContent + +internal class OrganizeTokensComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + private val model: OrganizeTokensModel = getOrCreateModel(params) + + override fun dismiss() { + params.callback.onDismiss() + } + + @Composable + override fun BottomSheet() { + val uiState by model.uiState.collectAsStateWithLifecycle() + + OrganizeTokensContent( + organizeTokensUM = uiState, + dragAndDropIntents = model.dragAndDropAdapter, + onDismiss = ::dismiss, + ) + } + + interface Callback { + fun onDismiss() + } + + data class Params( + val userWalletId: UserWalletId, + val callback: Callback, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/di/OrganizeTokensModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/di/OrganizeTokensModule.kt index e27eb472ed..97ad9b7027 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/di/OrganizeTokensModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/di/OrganizeTokensModule.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.child.organizetokens.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model +import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModelLegacy import dagger.Binds import dagger.Module @@ -17,4 +18,9 @@ internal interface OrganizeTokensModule { @IntoMap @ClassKey(OrganizeTokensModelLegacy::class) fun bindOrganizeTokensModelLegacy(model: OrganizeTokensModelLegacy): Model + + @Binds + @IntoMap + @ClassKey(OrganizeTokensModel::class) + fun bindOrganizeTokensModel(model: OrganizeTokensModel): Model } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeRowItemUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeRowItemUM.kt new file mode 100644 index 0000000000..48dcba2564 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeRowItemUM.kt @@ -0,0 +1,116 @@ +package com.tangem.feature.wallet.child.organizetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM + +/** + * Helper class for the DND list items + * + * @property id ID of the item + * @property roundingModeUM item [RoundingModeUM] + * @property isShowShadow if true then item should be elevated + * */ +@Immutable +internal sealed class OrganizeRowItemUM { + abstract val id: String + abstract val roundingModeUM: RoundingModeUM + abstract val isShowShadow: Boolean + + /** + * Item for token. + * + * @property id ID of the token + * @property groupId ID of the network group which contains this token + * @property accountId ID of account which contains this token + * @property tokenRowUM state of the token item + * @property roundingModeUM item [RoundingModeUM] + * @property isShowShadow if true then item should be elevated + * */ + data class Token( + override val isShowShadow: Boolean = false, + override val roundingModeUM: RoundingModeUM = RoundingModeUM.None, + val tokenRowUM: TangemTokenRowUM, + val groupId: String, + val accountId: String = "", + ) : OrganizeRowItemUM() { + override val id: String = tokenRowUM.id + } + + /** + * Item for network group header. + * + * @property id ID of the network group + * @property accountId ID of account which contains this network group + * @property headerRowUM state of the network group header item + * @property roundingModeUM item [RoundingModeUM] + * @property isShowShadow if true then item should be elevated + * */ + data class Network( + override val roundingModeUM: RoundingModeUM = RoundingModeUM.None, + override val isShowShadow: Boolean = false, + val headerRowUM: TangemHeaderRowUM, + val accountId: String = "", + ) : OrganizeRowItemUM() { + override val id: String = headerRowUM.id + } + + /** + * Item for portfolio. + * + * @property id ID of the portfolio + * @property headerRowUM state of the portfolio item + * @property roundingModeUM item [RoundingModeUM] + * @property isShowShadow if true then item should be elevated + * */ + data class Portfolio( + override val roundingModeUM: RoundingModeUM = RoundingModeUM.None, + val headerRowUM: TangemHeaderRowUM, + ) : OrganizeRowItemUM() { + override val id: String = headerRowUM.id + override val isShowShadow: Boolean = false + } + + /** + * Helper item used to detect possible positions where a draggable item can be placed. + * + * @property id ID of the placeholder for corresponding ID of the group + * @property accountId ID of account which contains this placeholder + * */ + data class Placeholder( + override val id: String, + val accountId: String = "", + ) : OrganizeRowItemUM() { + override val isShowShadow: Boolean = false + override val roundingModeUM: RoundingModeUM = RoundingModeUM.None + } + + /** + * Update item [RoundingModeUM] + * + * @param mode new [RoundingModeUM] + * + * @return updated [DraggableItem] + * */ + fun updateRoundingMode(mode: RoundingModeUM): OrganizeRowItemUM = when (this) { + is Placeholder -> this + is Portfolio -> this.copy(roundingModeUM = mode) + is Network -> this.copy(roundingModeUM = mode) + is Token -> this.copy(roundingModeUM = mode) + } + + /** + * Update item shadow visibility + * + * @param show if true then item should be elevated + * + * @return updated [DraggableItem] + * */ + fun updateShadowVisibility(show: Boolean): OrganizeRowItemUM = when (this) { + is Portfolio, + is Placeholder, + -> this + is Network -> this.copy(isShowShadow = show) + is Token -> this.copy(isShowShadow = show) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensUM.kt new file mode 100644 index 0000000000..4162f46d7f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensUM.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.wallet.child.organizetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.event.StateEvent +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class OrganizeTokensUM( + val tokenList: PersistentList, + val organizeMenuUM: OrganizeMenuUM, + val isGrouped: Boolean, + val isAccountsMode: Boolean, + val scrollListToTop: StateEvent, + val cancelButton: TangemButtonUM, + val applyButton: TangemButtonUM, + val isBalanceHidden: Boolean, +) { + + data class OrganizeMenuUM( + val isEnabled: Boolean = false, + val isSortedByBalance: Boolean = false, + val isGrouped: Boolean = false, + val onSortClick: () -> Unit, + val onGroupClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt index cda6bbe86c..528b935475 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt @@ -5,6 +5,7 @@ import com.tangem.domain.account.status.model.AccountCryptoCurrencies import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM internal class CryptoCurrenciesIdsResolver { @@ -35,4 +36,23 @@ internal class CryptoCurrenciesIdsResolver { .toList() } } + + fun resolve(tokenList: List, accountStatusList: AccountStatusList?): AccountCryptoCurrencies { + if (accountStatusList == null) return emptyMap() + + val draggableTokens = tokenList.filterIsInstance() + + return accountStatusList.accountStatuses + .filterCryptoPortfolio() + .filter { it.tokenList != TokenList.Empty } + .associate { accountStatus -> + val currenciesById = accountStatus.flattenCurrencies().associateBy { it.currency.id.value } + + accountStatus.account to draggableTokens + .asSequence() + .filter { it.accountId == accountStatus.account.accountId.value } + .mapNotNull { token -> currenciesById[token.id]?.currency } + .toList() + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt index 48dc8f24ca..1eef2b75b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.child.organizetokens.model import androidx.compose.runtime.Stable import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM import org.burnoutcrew.reorderable.ItemPosition internal interface OrganizeTokensIntents { @@ -25,6 +26,7 @@ internal interface DragAndDropIntents { fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean fun onItemDraggingStartLegacy(item: DraggableItem) + fun onItemDraggingStart(item: OrganizeRowItemUM) fun onItemDraggingEnd() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt new file mode 100644 index 0000000000..6d6f148c84 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt @@ -0,0 +1,283 @@ +package com.tangem.feature.wallet.child.organizetokens.model + +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCase +import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCase +import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCase +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.models.TokensSortType +import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent +import com.tangem.feature.wallet.child.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM +import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapter +import com.tangem.feature.wallet.child.organizetokens.model.transformer.OrganizeContentStateTransformer +import com.tangem.feature.wallet.child.organizetokens.model.transformer.OrganizeDisableBalanceSortingTransformer +import com.tangem.feature.wallet.child.organizetokens.model.transformer.OrganizeSortingProgressStateTransformer +import com.tangem.feature.wallet.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.update +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@Stable +@ModelScoped +internal class OrganizeTokensModel @Inject constructor( + paramsContainer: ParamsContainer, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + override val dispatchers: CoroutineDispatcherProvider, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventsHandler: AnalyticsEventHandler, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val toggleTokenListGroupingUseCase: ToggleTokenListGroupingUseCase, + private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, + private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, +) : Model(), OrganizeTokensIntents { + + private val params: OrganizeTokensComponent.Params = paramsContainer.require() + + private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + + private val userWalletId = params.userWalletId + + private var cachedAccountStatusList: AccountStatusList? = null + + private var isAccountsModeEnabled: Boolean = false + + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + val dragAndDropAdapter by lazy(LazyThreadSafetyMode.NONE) { + DragAndDropAdapter(uiState) + } + + init { + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened()) + + getBalanceHidingSettingsUseCase() + .onEach { balanceSettings -> + uiState.update { + it.copy( + isBalanceHidden = balanceSettings.isBalanceHidden, + ) + } + } + .launchIn(modelScope) + + bootstrapTokenList() + bootstrapDragAndDropUpdates() + } + + override fun onBackClick() { + params.callback.onDismiss() + } + + override fun onSortClick() { + val list = cachedAccountStatusList ?: return + if (list.sortType == TokensSortType.BALANCE) return + + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) + + modelScope.launch { + toggleTokenListSortingUseCase(list).fold( + ifLeft = { + uiState.update { + it.copy( + organizeMenuUM = it.organizeMenuUM.copy(isEnabled = false), + ) + } + }, + ifRight = { accountStatusList -> + uiState.update( + OrganizeContentStateTransformer( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = selectedAppCurrencyFlow.value, + ), + ) + cachedAccountStatusList = accountStatusList + }, + ) + } + } + + override fun onGroupClick() { + val list = cachedAccountStatusList ?: return + + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) + + modelScope.launch { + toggleTokenListGroupingUseCase(list).fold( + ifLeft = { + uiState.update { + it.copy( + organizeMenuUM = it.organizeMenuUM.copy(isEnabled = false), + ) + } + }, + ifRight = { accountStatusList -> + uiState.update( + OrganizeContentStateTransformer( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = selectedAppCurrencyFlow.value, + ), + ) + cachedAccountStatusList = accountStatusList + }, + ) + } + } + + override fun onApplyClick() { + modelScope.launch { + uiState.update(OrganizeSortingProgressStateTransformer(true)) + val resolver = CryptoCurrenciesIdsResolver() + val isSortedByBalance = uiState.value.organizeMenuUM.isSortedByBalance + val isGroupedByNetwork = uiState.value.isGrouped + val tokensListUM = uiState.value.tokenList + + sendAnalyticsEvent( + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + val result = applyTokenListSortingUseCase( + sortedTokensIdsByAccount = resolver.resolve(tokensListUM, cachedAccountStatusList), + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + result.fold( + ifLeft = { + uiState.update { + it.copy( + organizeMenuUM = it.organizeMenuUM.copy(isEnabled = false), + ) + } + }, + ifRight = { + onBackClick() + uiState.update(OrganizeSortingProgressStateTransformer(false)) + }, + ) + } + } + + override fun onCancelClick() { + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel()) + onBackClick() + } + + private fun bootstrapTokenList() { + modelScope.launch { + val accountList = singleAccountStatusListSupplier.getSyncOrNull( + SingleAccountStatusListProducer.Params(userWalletId), + ) ?: return@launch + + isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() + + uiState.update( + transformer = OrganizeContentStateTransformer( + accountStatusList = accountList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = selectedAppCurrencyFlow.value, + ), + ) + + cachedAccountStatusList = accountList + } + } + + private fun bootstrapDragAndDropUpdates() { + dragAndDropAdapter.dragAndDropUpdates + .filterNotNull() + .distinctUntilChanged() + .onEach { (type, updatedTokenList) -> + disableSortingByBalanceIfListChanged(type) + uiState.update { it.copy(tokenList = updatedTokenList) } + } + .launchIn(modelScope) + } + + private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) { + if (dragOperationType !is DragAndDropAdapter.DragOperation.Type.End) return + + if (uiState.value.organizeMenuUM.isSortedByBalance && dragOperationType.isItemsOrderChanged) { + cachedAccountStatusList = cachedAccountStatusList?.copy(sortType = TokensSortType.NONE) + uiState.update(OrganizeDisableBalanceSortingTransformer) + } + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + + private fun sendAnalyticsEvent(isGroupedByNetwork: Boolean, isSortedByBalance: Boolean) { + analyticsEventsHandler.send( + PortfolioOrganizeTokensAnalyticsEvent.Apply( + grouping = if (isGroupedByNetwork) { + AnalyticsParam.OnOffState.On + } else { + AnalyticsParam.OnOffState.Off + }, + organizeSortType = if (isSortedByBalance) { + AnalyticsParam.OrganizeSortType.ByBalance + } else { + AnalyticsParam.OrganizeSortType.Manually + }, + ), + ) + } + + private fun getInitialState(): OrganizeTokensUM { + return OrganizeTokensUM( + tokenList = persistentListOf(), + organizeMenuUM = OrganizeTokensUM.OrganizeMenuUM( + onSortClick = ::onSortClick, + onGroupClick = ::onGroupClick, + ), + cancelButton = TangemButtonUM( + text = resourceReference(R.string.common_cancel), + onClick = ::onCancelClick, + type = TangemButtonType.Secondary, + ), + applyButton = TangemButtonUM( + text = resourceReference(R.string.common_apply), + onClick = ::onApplyClick, + type = TangemButtonType.Primary, + ), + scrollListToTop = consumedEvent(), + isBalanceHidden = true, + isGrouped = false, + isAccountsMode = false, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt index 28cfeba4d9..39545e3663 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt @@ -1,10 +1,18 @@ package com.tangem.feature.wallet.child.organizetokens.model.common import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM internal fun getGroupPlaceholderLegacy(index: Int, accountId: String = ""): DraggableItem.Placeholder { return DraggableItem.Placeholder( id = "placeholder_${accountId}_${index.inc()}", accountId = accountId, ) +} + +internal fun getGroupPlaceholder(index: Int, accountId: String = ""): OrganizeRowItemUM.Placeholder { + return OrganizeRowItemUM.Placeholder( + id = "placeholder_${accountId}_${index.inc()}", + accountId = accountId, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt index 5735d4ee77..634bb22b07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.child.organizetokens.model.common import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM internal fun List.uniteItemsLegacy(isAccountsMode: Boolean): List { @@ -45,6 +46,48 @@ internal fun List.uniteItemsLegacy(isAccountsMode: Boolean): List }.toList() } +internal fun List.uniteItems(isAccountsMode: Boolean): List { + val items = this + val lastItemIndex = items.lastIndex + + return items + .asSequence() + .mapIndexed { index, item -> + val mode = when (index) { + 0 -> if (item is OrganizeRowItemUM.Placeholder) { + RoundingModeUM.None + } else { + RoundingModeUM.Top() + } + lastItemIndex -> RoundingModeUM.Bottom() + 1 -> if (items.first() is OrganizeRowItemUM.Placeholder) { + RoundingModeUM.Top() + } else { + RoundingModeUM.None + } + else -> when (item) { + is OrganizeRowItemUM.Placeholder -> RoundingModeUM.None + is OrganizeRowItemUM.Network -> if (isAccountsMode) { + RoundingModeUM.None + } else { + RoundingModeUM.Top(isShowGap = true) + } + is OrganizeRowItemUM.Token -> applyRoundingModeToToken( + isAccountsMode = isAccountsMode, + items = items, + index = index, + lastItemIndex = lastItemIndex, + ) + is OrganizeRowItemUM.Portfolio -> RoundingModeUM.Top(isShowGap = true) + } + } + + item + .updateRoundingMode(mode) + .updateShadowVisibility(show = false) + }.toList() +} + internal fun List.divideMovingItem(movingItem: DraggableItem): List { val mutableList = this.toMutableList() val listIterator = mutableList.listIterator() @@ -110,4 +153,51 @@ private fun applyRoundingModeToTokenLegacy( RoundingModeUM.Bottom(isShowGap = true) } else -> RoundingModeUM.None +} + +/** + * Applying rounding to tokens + * + * If is in accounts mode without grouping + * * PORTFOLIO + * * TOKEN + * * TOKEN <- add rounding + * * PORTFOLIO index + 1 is PORTFOLIO + * + * If is in accounts mode with grouping + * * PORTFOLIO + * * PLACEHOLDER + * * GROUPING + * * TOKEN + * * TOKEN <- add rounding + * * PLACEHOLDER index + 1 is PLACEHOLDER + * * PORTFOLIO index + 2 is PORTFOLIO + * * PLACEHOLDER + * + * If is not accounts mode without grouping + * * TOKEN + * * TOKEN <- add rounding + * + * If is not accounts mode with grouping + * * PLACEHOLDER + * * GROUPING + * * TOKEN + * * TOKEN <- add rounding + * * PLACEHOLDER index + 1 is PLACEHOLDER + */ +private fun applyRoundingModeToToken( + isAccountsMode: Boolean, + items: List, + index: Int, + lastItemIndex: Int, +) = when { + isAccountsMode && index + 1 < lastItemIndex && + (items[index + 1] is OrganizeRowItemUM.Portfolio || + items[index + 1] is OrganizeRowItemUM.Placeholder && items[index + 2] is OrganizeRowItemUM.Portfolio) -> { + RoundingModeUM.Bottom(isShowGap = true) + } + (!isAccountsMode || index + 1 == lastItemIndex) && items[index + 1] is OrganizeRowItemUM.Placeholder -> { + RoundingModeUM.Bottom(isShowGap = true) + } + else -> RoundingModeUM.None } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/OrganizeTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/OrganizeTokensListConverter.kt new file mode 100644 index 0000000000..7fdbdd5854 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/OrganizeTokensListConverter.kt @@ -0,0 +1,82 @@ +package com.tangem.feature.wallet.child.organizetokens.model.converter + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder +import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems +import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizeAccountItemConverter +import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizeNetworkItemConverter +import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizeTokenItemConverter +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addIf +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal class OrganizeTokensListConverter( + private val isAccountsMode: Boolean, + private val appCurrency: AppCurrency, +) : Converter> { + + private val accountItemConverter by lazy(LazyThreadSafetyMode.NONE) { + OrganizeAccountItemConverter(appCurrency) + } + + private val tokenItemConverter by lazy(LazyThreadSafetyMode.NONE) { + OrganizeTokenItemConverter(appCurrency) + } + + override fun convert(value: AccountStatusList): PersistentList { + return value.accountStatuses + .asSequence() + .filterCryptoPortfolio() + .flatMap { accountStatus -> + buildList { + addIf( + condition = isAccountsMode, + create = { accountItemConverter.convert(accountStatus) }, + ) + when (val tokenList = accountStatus.tokenList) { + is TokenList.Ungrouped -> addAll( + elements = tokenItemConverter.convertList( + input = tokenList.currencies.mapToAccountCryptoCurrencyStatus(accountStatus), + ), + ) + is TokenList.GroupedByNetwork -> { + addIf( + condition = isAccountsMode, + create = { getGroupPlaceholder(-1, accountStatus.accountId.value) }, + ) + tokenList.groups.asSequence().forEachIndexed { index, (groupNetwork, currencies) -> + add(OrganizeNetworkItemConverter.convert(accountStatus.accountId to groupNetwork)) + addAll( + elements = tokenItemConverter.convertList( + input = currencies.mapToAccountCryptoCurrencyStatus(accountStatus), + ), + ) + add(getGroupPlaceholder(index, accountStatus.accountId.value)) + } + } + TokenList.Empty -> Unit + } + } + }.toList() + .uniteItems(isAccountsMode).toPersistentList() + } + + private fun List.mapToAccountCryptoCurrencyStatus( + accountStatus: AccountStatus.CryptoPortfolio, + ): List { + return map { cryptoCurrencyStatus -> + AccountCryptoCurrencyStatus( + account = accountStatus.account, + status = cryptoCurrencyStatus, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeAccountItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeAccountItemConverter.kt new file mode 100644 index 0000000000..ebba1ec794 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeAccountItemConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.child.organizetokens.model.converter.items + +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountStatus +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM +import com.tangem.utils.converter.Converter + +internal class OrganizeAccountItemConverter( + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: AccountStatus.CryptoPortfolio): OrganizeRowItemUM.Portfolio { + val accountBalance = value.tokenList.totalFiatBalance as? TotalFiatBalance.Loaded + return OrganizeRowItemUM.Portfolio( + headerRowUM = TangemHeaderRowUM( + id = value.accountId.value, + title = value.account.accountName.toUM().value, + subtitle = stringReference( + accountBalance?.amount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeNetworkItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeNetworkItemConverter.kt new file mode 100644 index 0000000000..4ef2f77614 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeNetworkItemConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.wallet.child.organizetokens.model.converter.items + +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.network.Network +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM +import com.tangem.feature.wallet.impl.R +import com.tangem.utils.converter.Converter + +internal object OrganizeNetworkItemConverter : Converter, OrganizeRowItemUM.Network> { + + override fun convert(value: Pair): OrganizeRowItemUM.Network { + val (accountId, groupNetwork) = value + return OrganizeRowItemUM.Network( + headerRowUM = TangemHeaderRowUM( + id = groupNetwork.id.toString(), + title = stringReference(groupNetwork.name), + tailUM = TangemRowTailUM.Draggable(R.drawable.ic_group_drop_24), + ), + accountId = accountId.value, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeTokenItemConverter.kt new file mode 100644 index 0000000000..c1cfa0e69b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeTokenItemConverter.kt @@ -0,0 +1,67 @@ +package com.tangem.feature.wallet.child.organizetokens.model.converter.items + +import androidx.compose.ui.text.SpanStyle +import com.tangem.common.getTotalCryptoAmount +import com.tangem.common.getTotalFiatAmount +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.cryptoStyled +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM +import com.tangem.feature.wallet.child.organizetokens.model.common.getTokenItemId +import com.tangem.feature.wallet.impl.R +import com.tangem.utils.converter.Converter + +internal class OrganizeTokenItemConverter( + private val appCurrency: AppCurrency, +) : Converter { + + private val iconStateConverter by lazy(LazyThreadSafetyMode.NONE) { + CryptoCurrencyToIconStateConverter() + } + + override fun convert(value: AccountCryptoCurrencyStatus): OrganizeRowItemUM.Token { + val (account, currencyStatus) = value + val currency = currencyStatus.currency + + return OrganizeRowItemUM.Token( + tokenRowUM = TangemTokenRowUM.Actionable( + id = getTokenItemId(currency.id), + headIconUM = TangemIconUM.Currency(iconStateConverter.convert(currencyStatus)), + titleUM = TangemTokenRowUM.TitleUM.Content( + text = stringReference(currency.name), + ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Empty, + topEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = currencyStatus.getTotalFiatAmount().formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + ), + bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = currencyStatus.getTotalCryptoAmount().formatStyled { + cryptoStyled( + cryptoCurrency = currency, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + ), + tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24), + onItemClick = null, + onItemLongClick = null, + ), + groupId = currency.network.id.toString(), + accountId = account.accountId.value, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt new file mode 100644 index 0000000000..079ba2c104 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt @@ -0,0 +1,217 @@ +package com.tangem.feature.wallet.child.organizetokens.model.dnd + +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM +import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM +import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents +import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.burnoutcrew.reorderable.ItemPosition + +internal class DragAndDropAdapter( + private val organizeTokensUMFlow: StateFlow, +) : DragAndDropIntents { + + private var draggingItem: OrganizeRowItemUM? = null + private var draggingListState: OrganizeTokensUM? = null + + private val tokensUM: OrganizeTokensUM + get() = organizeTokensUMFlow.value + + private val draggableGroupsOperations by lazy(LazyThreadSafetyMode.NONE) { + DraggableGroupsOperations() + } + + val dragAndDropUpdates: StateFlow + field = MutableStateFlow(value = null) + + override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { + val tokensListUM = tokensUM.tokenList + + val (dragOverItem, draggingItem) = findItemsToMove( + items = tokensListUM, + moveOverItemKey = dragOver.key, + movedItemKey = dragging.key, + ) + + if (dragOverItem == null || draggingItem == null) { + return false + } + + val canDrag = when (draggingItem) { + is OrganizeRowItemUM.Network -> checkCanMoveHeaderOver( + item = draggingItem, + moveOverItem = dragOverItem, + ) + is OrganizeRowItemUM.Token -> checkCanMoveTokenOver( + item = draggingItem, + moveOverItem = dragOverItem, + isAccountsMode = tokensUM.isAccountsMode, + isGrouped = tokensUM.isGrouped, + ) + is OrganizeRowItemUM.Placeholder, + is OrganizeRowItemUM.Portfolio, + -> false + } + + return canDrag + } + + override fun onItemDraggingStartLegacy(item: DraggableItem) { + /* no-op */ + } + + override fun onItemDraggingStart(item: OrganizeRowItemUM) { + if (draggingItem != null) return + draggingItem = item + + dragAndDropUpdates.value = DragOperation( + type = DragOperation.Type.Start, + tokenList = when (item) { + is OrganizeRowItemUM.Placeholder, + is OrganizeRowItemUM.Portfolio, + -> tokensUM.tokenList + is OrganizeRowItemUM.Network -> draggableGroupsOperations + .collapseGroup(tokensUM.tokenList, item) + .divideMovingItem(item) + is OrganizeRowItemUM.Token -> tokensUM.tokenList.divideMovingItem(item) + }.toPersistentList(), + ) + + draggingListState = tokensUM + } + + override fun onItemDraggingEnd() { + val draggingItem = draggingItem ?: return + + dragAndDropUpdates.value = DragOperation( + type = DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged(tokensUM)), + tokenList = when (draggingItem) { + is OrganizeRowItemUM.Network -> draggableGroupsOperations + .expandGroups(tokensUM.tokenList, tokensUM.isAccountsMode) + .uniteItems(tokensUM.isAccountsMode) + is OrganizeRowItemUM.Token -> tokensUM.tokenList.uniteItems(tokensUM.isAccountsMode) + is OrganizeRowItemUM.Placeholder, + is OrganizeRowItemUM.Portfolio, + -> tokensUM.tokenList + }.toPersistentList(), + ) + + this.draggingItem = null + } + + override fun onItemDragged(from: ItemPosition, to: ItemPosition) { + dragAndDropUpdates.value = DragOperation( + type = DragOperation.Type.Dragged, + tokenList = tokensUM.tokenList.mutate { + it.add(to.index, it.removeAt(from.index)) + }.toPersistentList(), + ) + } + + private fun findItemsToMove( + items: List, + moveOverItemKey: Any?, + movedItemKey: Any?, + ): Pair { + var moveOverItem: OrganizeRowItemUM? = null + var movedItem: OrganizeRowItemUM? = null + + for (item in items) { + if (item.id == moveOverItemKey) { + moveOverItem = item + } + if (item.id == movedItemKey) { + movedItem = item + } + if (moveOverItem != null && movedItem != null) { + break + } + } + + return Pair(moveOverItem, movedItem) + } + + private fun checkCanMoveHeaderOver(item: OrganizeRowItemUM.Network, moveOverItem: OrganizeRowItemUM) = + when (moveOverItem) { + // Header can be moved only in its account + is OrganizeRowItemUM.Placeholder -> item.accountId == moveOverItem.accountId + else -> false + } + + private fun checkCanMoveTokenOver( + item: OrganizeRowItemUM.Token, + moveOverItem: OrganizeRowItemUM, + isGrouped: Boolean, + isAccountsMode: Boolean, + ): Boolean { + return when (moveOverItem) { + is OrganizeRowItemUM.Network -> false // Token item can not be moved to group item + is OrganizeRowItemUM.Token -> when { + // Token item can be moved only in its group + isGrouped -> item.groupId == moveOverItem.groupId + + // Token item can be moved only in its account + isAccountsMode -> item.accountId == moveOverItem.accountId + + // If ungrouped and not accounts mode then item can be moved anywhere + else -> true + } + is OrganizeRowItemUM.Portfolio, + is OrganizeRowItemUM.Placeholder, + -> false // Token item can not be moved to portfolio or placeholder + } + } + + private fun checkIsItemsOrderChanged(tokensUM: OrganizeTokensUM): Boolean { + fun OrganizeTokensUM?.getItemsIds(): List? = this?.tokenList?.mapNotNull { item -> + if (item is OrganizeRowItemUM.Placeholder) { + null + } else { + item.id + } + } + + return tokensUM.getItemsIds() != draggingListState.getItemsIds() + } + + private fun List.divideMovingItem(movingItem: OrganizeRowItemUM): List { + val mutableList = this.toMutableList() + val listIterator = mutableList.listIterator() + + while (listIterator.hasNext()) { + val item = listIterator.next() + + if (item.id == movingItem.id) { + val dividedItem = movingItem + .updateRoundingMode(RoundingModeUM.All()) + .updateShadowVisibility(show = true) + + listIterator.set(dividedItem) + break + } + } + + return mutableList + } + + data class DragOperation( + val type: Type, + val tokenList: PersistentList, + ) { + + sealed class Type { + + data object Start : Type() + + data object Dragged : Type() + + data class End(val isItemsOrderChanged: Boolean) : Type() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapterLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapterLegacy.kt index f6de70d6e6..94308476d9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapterLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapterLegacy.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.child.organizetokens.model.dnd import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem @@ -90,6 +91,8 @@ internal class DragAndDropAdapterLegacy( draggingListState = tokenListUM } + override fun onItemDraggingStart(item: OrganizeRowItemUM) { /* no-op */ } + override fun onItemDraggingEnd() { val draggingItem = draggingItem ?: return diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt index 6487f86b78..02230787ee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt @@ -1,11 +1,14 @@ package com.tangem.feature.wallet.child.organizetokens.model.dnd import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholderLegacy internal class DraggableGroupsOperations { + private var groupIdToTokens: Map>? = null private var groupIdToTokensLegacy: Map>? = null fun collapseGroupLegacy(items: List, movingGroup: DraggableItem.GroupHeader): List { @@ -67,4 +70,67 @@ internal class DraggableGroupsOperations { return expandedGroups } + + fun expandGroups(items: List, isAccountsMode: Boolean): List { + if (groupIdToTokens.isNullOrEmpty()) return items + + val accountList = items.filterIsInstance() + val currentGroups = items.filterIsInstance() + + val expandedGroups = if (isAccountsMode) { + accountList + .asSequence() + .flatMap { account -> + buildList { + add(account) + currentGroups + .asSequence() + .filter { it.accountId == account.id } + .forEachIndexed { index, group -> + if (index == 0) { + add(getGroupPlaceholder(accountId = group.accountId, index = -1)) + } + add(group) + addAll(groupIdToTokens?.get(group.id).orEmpty()) + add(getGroupPlaceholder(accountId = group.accountId, index = index)) + } + } + } + } else { + currentGroups + .asSequence() + .flatMapIndexed { index, group -> + buildList { + if (index == 0) { + add(getGroupPlaceholder(accountId = group.accountId, index = -1)) + } + add(group) + addAll(groupIdToTokens?.get(group.id).orEmpty()) + add(getGroupPlaceholder(accountId = group.accountId, index = index)) + } + } + }.toList() + + groupIdToTokens = null + + return expandedGroups + } + + fun collapseGroup( + items: List, + movingGroup: OrganizeRowItemUM.Network, + ): List { + if (!groupIdToTokens.isNullOrEmpty()) return items + + groupIdToTokens = items + .asSequence() + .filterIsInstance() + .groupBy { it.groupId } + + val itemsWithoutGroupTokens = items.filterNot { + it is OrganizeRowItemUM.Token && it.groupId == movingGroup.id + } + + return itemsWithoutGroupTokens + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeContentStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeContentStateTransformer.kt new file mode 100644 index 0000000000..3396068acb --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeContentStateTransformer.kt @@ -0,0 +1,39 @@ +package com.tangem.feature.wallet.child.organizetokens.model.transformer + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM +import com.tangem.feature.wallet.child.organizetokens.model.converter.OrganizeTokensListConverter +import com.tangem.utils.transformer.Transformer + +internal class OrganizeContentStateTransformer( + private val accountStatusList: AccountStatusList, + private val isAccountsMode: Boolean, + private val appCurrency: AppCurrency, +) : Transformer { + + private val tokenListConverter by lazy(LazyThreadSafetyMode.NONE) { + OrganizeTokensListConverter( + isAccountsMode = isAccountsMode, + appCurrency = appCurrency, + ) + } + + override fun transform(prevState: OrganizeTokensUM): OrganizeTokensUM { + val isGrouping = accountStatusList.groupType == TokensGroupType.NETWORK + val isSortedByBalance = accountStatusList.sortType == TokensSortType.BALANCE + + return prevState.copy( + isGrouped = isGrouping, + isAccountsMode = isAccountsMode, + tokenList = tokenListConverter.convert(value = accountStatusList), + organizeMenuUM = prevState.organizeMenuUM.copy( + isEnabled = prevState.tokenList.isNotEmpty(), + isSortedByBalance = isSortedByBalance, + isGrouped = isGrouping, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeDisableBalanceSortingTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeDisableBalanceSortingTransformer.kt new file mode 100644 index 0000000000..1242d0f1dd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeDisableBalanceSortingTransformer.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.wallet.child.organizetokens.model.transformer + +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM +import com.tangem.utils.transformer.Transformer + +internal object OrganizeDisableBalanceSortingTransformer : Transformer { + override fun transform(prevState: OrganizeTokensUM): OrganizeTokensUM { + return prevState.copy( + organizeMenuUM = prevState.organizeMenuUM.copy( + isSortedByBalance = false, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeSortingProgressStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeSortingProgressStateTransformer.kt new file mode 100644 index 0000000000..e16022ef42 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/transformer/OrganizeSortingProgressStateTransformer.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.child.organizetokens.model.transformer + +import com.tangem.core.ui.ds.button.TangemButtonState +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM +import com.tangem.utils.transformer.Transformer + +internal class OrganizeSortingProgressStateTransformer( + private val isSortingInProgress: Boolean, +) : Transformer { + override fun transform(prevState: OrganizeTokensUM): OrganizeTokensUM { + return prevState.copy( + organizeMenuUM = prevState.organizeMenuUM.copy( + isEnabled = !isSortingInProgress, + ), + cancelButton = prevState.cancelButton.copy( + state = if (isSortingInProgress) { + TangemButtonState.Disabled + } else { + TangemButtonState.Default + }, + ), + applyButton = prevState.applyButton.copy( + state = if (isSortingInProgress) { + TangemButtonState.Loading + } else { + TangemButtonState.Default + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeDropDownMenu.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeDropDownMenu.kt new file mode 100644 index 0000000000..b840828ac4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeDropDownMenu.kt @@ -0,0 +1,43 @@ +package com.tangem.feature.wallet.child.organizetokens.ui + +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.contextmenu.TangemContextMenu +import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM +import com.tangem.feature.wallet.impl.R + +@Composable +internal fun OrganizeDropDownMenu( + organizeMenuUM: OrganizeTokensUM.OrganizeMenuUM, + showDropdownMenu: Boolean, + onDropdownDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + TangemContextMenu( + expanded = showDropdownMenu, + onDismissRequest = onDropdownDismiss, + offset = DpOffset.Zero, + modifier = modifier, + ) { + TangemContextMenuCheckboxItem( + title = TextReference.Res(R.string.organize_tokens_sort_by_balance), + isChecked = organizeMenuUM.isSortedByBalance, + onClick = organizeMenuUM.onSortClick, + ) + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors2.border.neutral.quaternary, + ) + TangemContextMenuCheckboxItem( + title = TextReference.Res(R.string.organize_tokens_group), + isChecked = organizeMenuUM.isGrouped, + onClick = organizeMenuUM.onGroupClick, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt new file mode 100644 index 0000000000..1de1a10cab --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt @@ -0,0 +1,371 @@ +package com.tangem.feature.wallet.child.organizetokens.ui + +import android.content.res.Configuration +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.ds.button.TangemButton +import com.tangem.core.ui.ds.row.header.TangemHeaderRow +import com.tangem.core.ui.ds.row.token.TangemTokenRow +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarActionContent +import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.reordarable.ReorderableItem +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.OrganizeTokensScreenTestTags +import com.tangem.core.ui.utils.lazyListItemPosition +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM +import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM +import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents +import com.tangem.feature.wallet.child.organizetokens.ui.preview.OrganizeTokensPreview +import com.tangem.feature.wallet.impl.R +import dev.chrisbanes.haze.rememberHazeState +import org.burnoutcrew.reorderable.ItemPosition +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.rememberReorderableLazyListState +import org.burnoutcrew.reorderable.reorderable + +@Composable +internal fun OrganizeTokensContent( + organizeTokensUM: OrganizeTokensUM, + dragAndDropIntents: DragAndDropIntents, + onDismiss: () -> Unit, +) { + var isShowDropdownMenu by rememberSaveable { mutableStateOf(false) } + + val hazeState = rememberHazeState() + + TangemBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + addBottomInsets = false, + containerColor = TangemTheme.colors2.surface.level2, + title = { + TangemTopBar( + title = resourceReference(R.string.organize_tokens_title), + endContent = { + TangemTopBarActionContent( + actionUM = TangemTopBarActionUM( + iconRes = R.drawable.ic_exchange_mini_24, + isActionable = true, + onClick = { isShowDropdownMenu = true }, + ghostModeProgress = 0f, + ), + iconSize = TangemTheme.dimens2.x7, + ) + OrganizeDropDownMenu( + organizeMenuUM = organizeTokensUM.organizeMenuUM, + showDropdownMenu = isShowDropdownMenu, + onDropdownDismiss = { isShowDropdownMenu = false }, + modifier = Modifier.hazeEffectTangem(hazeState), + ) + }, + ) + }, + content = { + TokenList( + organizeTokensUM = organizeTokensUM, + dragAndDropIntents = dragAndDropIntents, + modifier = Modifier.hazeSourceTangem(hazeState), + ) + }, + ) +} + +@Suppress("MagicNumber") +@Composable +private fun TokenList( + organizeTokensUM: OrganizeTokensUM, + dragAndDropIntents: DragAndDropIntents, + modifier: Modifier = Modifier, +) { + val tokensListState = rememberLazyListState() + + val hapticFeedback = LocalHapticFeedback.current + val tokenList = organizeTokensUM.tokenList + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level2), + ) { + val onDragEnd: (Int, Int) -> Unit = remember { + { _, _ -> + dragAndDropIntents.onItemDraggingEnd() + } + } + val reorderableListState = rememberReorderableLazyListState( + onMove = dragAndDropIntents::onItemDragged, + listState = tokensListState, + canDragOver = dragAndDropIntents::canDragItemOver, + onDragEnd = onDragEnd, + ) + + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + val listContentPadding = PaddingValues( + top = TangemTheme.dimens2.x1, + bottom = TangemTheme.dimens2.x1 + bottomBarHeight, + start = TangemTheme.dimens2.x3, + end = TangemTheme.dimens2.x3, + ) + + LazyColumn( + modifier = Modifier + .align(Alignment.TopCenter) + .reorderable(reorderableListState) + .testTag(OrganizeTokensScreenTestTags.TOKENS_LAZY_LIST) + .hazeSourceTangem(zIndex = 1f), + state = reorderableListState.listState, + contentPadding = listContentPadding, + ) { + itemsIndexed( + items = tokenList, + key = { _, item -> item.id }, + ) { index, item -> + + val onDragStart = remember(item) { + { + dragAndDropIntents.onItemDraggingStart(item) + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } + } + + DraggableItem( + index = index, + item = item, + reorderableState = reorderableListState, + onDragStart = onDragStart, + isBalanceHidden = organizeTokensUM.isBalanceHidden, + ) + } + } + + BottomFade( + gradientBrush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + TangemTheme.colors2.surface.level2.copy(0.9f), + TangemTheme.colors2.surface.level2, + ), + ), + modifier = Modifier.align(Alignment.BottomCenter), + ) + + BottomButtons(organizeTokensUM = organizeTokensUM) + } +} + +@Composable +private fun LazyItemScope.DraggableItem( + index: Int, + item: OrganizeRowItemUM, + reorderableState: ReorderableLazyListState, + onDragStart: () -> Unit, + isBalanceHidden: Boolean, +) { + var isDragging by remember { + mutableStateOf(value = false) + } + + val itemModifier = Modifier.applyShapeAndShadow(item.roundingModeUM, item.isShowShadow) + + ReorderableItem( + reorderableState = reorderableState, + index = index, + key = item.id, + ) { isItemDragging -> + isDragging = isItemDragging + + val modifierWithBackground = itemModifier + .background(color = TangemTheme.colors.background.primary) + .semantics { lazyListItemPosition = index } + + when (item) { + is OrganizeRowItemUM.Network -> TangemHeaderRow( + modifier = modifierWithBackground, + reorderableState = reorderableState, + headerRowUM = item.headerRowUM, + ) + is OrganizeRowItemUM.Portfolio -> TangemHeaderRow( + modifier = modifierWithBackground, + headerRowUM = item.headerRowUM, + isBalanceHidden = isBalanceHidden, + ) + is OrganizeRowItemUM.Token -> TangemTokenRow( + modifier = modifierWithBackground, + tokenRowUM = item.tokenRowUM, + reorderableState = reorderableState, + isBalanceHidden = isBalanceHidden, + ) + // Should be presented in the list but remain invisible + is OrganizeRowItemUM.Placeholder -> Box(modifier = Modifier.fillMaxWidth()) + } + } + + DisposableEffect(isDragging) { + onDispose { + if (isDragging) { + onDragStart() + } + } + } +} + +@Composable +private fun BoxScope.BottomButtons(organizeTokensUM: OrganizeTokensUM) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = TangemTheme.dimens2.x4) + .padding(bottom = bottomBarHeight + TangemTheme.dimens2.x4), + ) { + TangemButton( + buttonUM = organizeTokensUM.cancelButton, + modifier = Modifier.weight(1f), + ) + TangemButton( + buttonUM = organizeTokensUM.applyButton, + modifier = Modifier.weight(1f), + ) + } +} + +private fun Modifier.applyShapeAndShadow(roundingMode: RoundingModeUM, showShadow: Boolean): Modifier { + return composed { + val radius by animateDpAsState( + targetValue = when (roundingMode) { + is RoundingModeUM.None -> TangemTheme.dimens2.x0 + is RoundingModeUM.All -> TangemTheme.dimens2.x3 + is RoundingModeUM.Bottom, + is RoundingModeUM.Top, + -> TangemTheme.dimens2.x4 + }, + label = "item_shape_radius", + ) + val elevation by animateDpAsState( + targetValue = if (showShadow) { + TangemTheme.dimens2.x2 + } else { + TangemTheme.dimens2.x0 + }, + label = "item_elevation", + ) + + this + .padding(paddingValues = getItemGap(roundingMode)) + .shadow( + elevation = elevation, + shape = getItemShape(roundingMode, radius), + clip = true, + ) + } +} + +@Composable +@ReadOnlyComposable +private fun getItemGap(roundingMode: RoundingModeUM): PaddingValues { + val paddingValue = TangemTheme.dimens2.x1 + + return if (roundingMode.isShowGap) { + when (roundingMode) { + is RoundingModeUM.None -> PaddingValues(all = TangemTheme.dimens2.x0) + is RoundingModeUM.All -> PaddingValues(vertical = paddingValue) + is RoundingModeUM.Top -> PaddingValues(top = paddingValue) + is RoundingModeUM.Bottom -> PaddingValues(bottom = paddingValue) + } + } else { + PaddingValues(all = TangemTheme.dimens2.x0) + } +} + +@Stable +private fun getItemShape(roundingMode: RoundingModeUM, radius: Dp): Shape { + return when (roundingMode) { + is RoundingModeUM.None -> RectangleShape + is RoundingModeUM.Top -> RoundedCornerShape( + topStart = radius, + topEnd = radius, + ) + is RoundingModeUM.Bottom -> RoundedCornerShape( + bottomStart = radius, + bottomEnd = radius, + ) + is RoundingModeUM.All -> RoundedCornerShape( + size = radius, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun OrganizeTokensContent_Preview( + @PreviewParameter(OrganizeTokensContentPreviewProvider::class) params: OrganizeTokensUM, +) { + TangemThemePreviewRedesign { + OrganizeTokensContent( + organizeTokensUM = params, + dragAndDropIntents = object : DragAndDropIntents { + override fun onItemDragged(from: ItemPosition, to: ItemPosition) {} + + override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean = false + + override fun onItemDraggingStartLegacy(item: DraggableItem) {} + + override fun onItemDraggingStart(item: OrganizeRowItemUM) {} + + override fun onItemDraggingEnd() {} + }, + onDismiss = {}, + ) + } +} + +private class OrganizeTokensContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + OrganizeTokensPreview.defaultState, + OrganizeTokensPreview.defaultState.copy(isGrouped = false), + ) +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt new file mode 100644 index 0000000000..8ca6f08e99 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt @@ -0,0 +1,129 @@ +package com.tangem.feature.wallet.child.organizetokens.ui.preview + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM +import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM +import com.tangem.feature.wallet.impl.R +import kotlinx.collections.immutable.toPersistentList +import java.util.UUID + +internal object OrganizeTokensPreview { + + private const val networksSize = 10 + private const val tokensSize = 3 + + private val draggableToken = TangemTokenRowUM.Actionable( + id = UUID.randomUUID().toString(), + headIconUM = TangemIconUM.Currency( + CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + titleUM = TangemTokenRowUM.TitleUM.Content(stringReference(value = "Polygon")), + subtitleUM = TangemTokenRowUM.SubtitleUM.Empty, + topEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("$ 42,900.13"), + ), + bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("733,71097 POL"), + ), + tailUM = TangemRowTailUM.Draggable(R.drawable.ic_group_drop_24), + onItemClick = null, + onItemLongClick = null, + ) + + private val tokenList = List(networksSize) { it } + .flatMap { index -> + val lastNetworkIndex = networksSize - 1 + val lastTokenIndex = tokensSize - 1 + val networkNumber = index + 1 + + val group = OrganizeRowItemUM.Network( + headerRowUM = TangemHeaderRowUM( + id = networkNumber.toString(), + title = stringReference(value = "$networkNumber"), + ), + roundingModeUM = when (index) { + 0 -> RoundingModeUM.Top() + lastNetworkIndex -> RoundingModeUM.Bottom() + else -> RoundingModeUM.None + }, + accountId = "account_$networkNumber", + ) + + val tokens: MutableList = mutableListOf() + repeat(times = tokensSize) { i -> + val tokenNumber = i + 1 + tokens.add( + OrganizeRowItemUM.Token( + tokenRowUM = draggableToken.copy( + id = "${group.id}_token_$tokenNumber", + titleUM = TangemTokenRowUM.TitleUM.Content( + text = stringReference(value = "Token $tokenNumber from $networkNumber network"), + ), + ), + groupId = group.id, + accountId = "account_$networkNumber", + roundingModeUM = when { + i == lastTokenIndex && index == lastNetworkIndex -> RoundingModeUM.Bottom() + else -> RoundingModeUM.None + }, + ), + ) + } + + val divider = OrganizeRowItemUM.Placeholder( + id = "divider_$networkNumber", + accountId = "account_$networkNumber", + ) + + buildList { + add(group) + addAll(tokens) + if (index != lastNetworkIndex) { + add(divider) + } + } + } + .toPersistentList() + + val defaultState by lazy { + OrganizeTokensUM( + tokenList = tokenList, + organizeMenuUM = OrganizeTokensUM.OrganizeMenuUM( + onSortClick = {}, + onGroupClick = {}, + ), + cancelButton = TangemButtonUM( + text = resourceReference(R.string.common_cancel), + onClick = {}, + type = TangemButtonType.Secondary, + ), + applyButton = TangemButtonUM( + text = resourceReference(R.string.common_apply), + onClick = {}, + type = TangemButtonType.Primary, + ), + scrollListToTop = consumedEvent(), + isAccountsMode = true, + isBalanceHidden = true, + isGrouped = false, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index f7c026a60f..840e428d26 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -21,6 +21,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent import com.tangem.domain.tokens.model.details.TokenAction +import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent import com.tangem.feature.wallet.child.wallet.model.WalletModel import com.tangem.feature.wallet.navigation.WalletRoute @@ -146,6 +147,15 @@ internal class WalletComponent @AssistedInject constructor( ), ) } + is WalletDialogConfig.OrganizeTokens -> { + OrganizeTokensComponent( + appComponentContext = childByContext(componentContext), + params = OrganizeTokensComponent.Params( + userWalletId = dialogConfig.userWalletId, + callback = model.innerWalletRouter.organizeCallbacks, + ), + ) + } } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 59252f1620..57ad1e77a7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -2,10 +2,12 @@ package com.tangem.feature.wallet.presentation.router import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency @@ -18,6 +20,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.TokenAction +import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig @@ -32,6 +35,7 @@ internal class DefaultWalletRouter @Inject constructor( private val router: AppRouter, private val urlOpener: UrlOpener, private val reduxStateHolder: ReduxStateHolder, + private val designFeatureToggles: DesignFeatureToggles, ) : InnerWalletRouter { override val dialogNavigation: SlotNavigation = SlotNavigation() @@ -41,8 +45,17 @@ internal class DefaultWalletRouter @Inject constructor( onBufferOverflow = BufferOverflow.DROP_LATEST, ) + override val organizeCallbacks: OrganizeTokensComponent.Callback + get() = OrganizeCallbacks() + override fun openOrganizeTokensScreen(userWalletId: UserWalletId) { - navigateToFlow.tryEmit(WalletRoute.OrganizeTokens(userWalletId)) + if (designFeatureToggles.isRedesignEnabled) { + dialogNavigation.activate( + configuration = WalletDialogConfig.OrganizeTokens(userWalletId), + ) + } else { + navigateToFlow.tryEmit(WalletRoute.OrganizeTokens(userWalletId)) + } } override fun openDetailsScreen(selectedWalletId: UserWalletId) { @@ -165,4 +178,10 @@ internal class DefaultWalletRouter @Inject constructor( override fun openQrScanner() { router.push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.MainScreen)) } + + inner class OrganizeCallbacks : OrganizeTokensComponent.Callback { + override fun onDismiss() { + dialogNavigation.dismiss() + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 53115f3db2..b05530a666 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -13,6 +13,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.TokenAction +import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig @@ -34,6 +35,8 @@ internal interface InnerWalletRouter { val navigateToFlow: SharedFlow + val organizeCallbacks: OrganizeTokensComponent.Callback + /** Open organize tokens screen */ fun openOrganizeTokensScreen(userWalletId: UserWalletId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index e1144d4983..01e89e2b5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -41,4 +41,7 @@ internal sealed interface WalletDialogConfig { @Serializable data class KycRejected(val walletId: UserWalletId, val customerId: String) : WalletDialogConfig + + @Serializable + data class OrganizeTokens(val userWalletId: UserWalletId) : WalletDialogConfig } \ No newline at end of file