diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/NetworkTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/NetworkTitle.kt index 590e93ddb5..e7644ee705 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/NetworkTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/NetworkTitle.kt @@ -71,7 +71,6 @@ fun NetworkTitle( Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing8)) Box( modifier = Modifier - .weight(weight = 1f) .heightIn(min = TangemTheme.dimens.size24), contentAlignment = Alignment.CenterEnd, content = action, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index fa775ca36c..b5539edab3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -83,7 +83,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B } @Composable -private fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) { +fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt index c417f19085..b04e872b58 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -38,4 +38,8 @@ sealed interface AccountStatus { fun flattenCurrencies(): List = when (this) { is CryptoPortfolio -> tokenList.flattenCurrencies() } + + fun getCryptoTokenList(): TokenList = when (this) { + is CryptoPortfolio -> tokenList + } } \ 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 index 4f7dfafd03..0ca835c3df 100644 --- 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 @@ -7,6 +7,11 @@ 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.domain.account.featuretoggle.AccountsFeatureToggles +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.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -27,6 +32,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disableSortingByBalance import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter +import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2 import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -46,6 +52,9 @@ internal class OrganizeTokensModel @Inject constructor( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val analyticsEventsHandler: AnalyticsEventHandler, + private val accountsFeatureToggles: AccountsFeatureToggles, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, ) : Model(), OrganizeTokensIntents { private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() @@ -56,15 +65,22 @@ internal class OrganizeTokensModel @Inject constructor( listStateProvider = Provider { uiState.value.itemsState }, ) + private val dragAndDropAdapterV2 = DragAndDropAdapterV2( + tokenListUMProvider = Provider { uiState.value.tokenListUM }, + ) + private val stateHolder = OrganizeTokensStateHolder( intents = this, dragAndDropIntents = dragAndDropAdapter, + dragAndDropAdapterV2 = dragAndDropAdapterV2, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + accountsFeatureToggles = accountsFeatureToggles, ) private val userWalletId = paramsContainer.require().userWalletId private var cachedTokenList: TokenList? = null + private var cachedAccountStatusList: AccountStatusList? = null val uiState: StateFlow = stateHolder.stateFlow @@ -89,59 +105,92 @@ internal class OrganizeTokensModel @Inject constructor( } override fun onSortClick() { - val list = cachedTokenList ?: return - if (list.sortedBy == TokensSortType.BALANCE) return + if (accountsFeatureToggles.isFeatureEnabled) { + val list = cachedAccountStatusList ?: return + if (list.sortType == TokensSortType.BALANCE) return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) - modelScope.launch { - toggleTokenListSortingUseCase(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSorting(it) - cachedTokenList = it - }, - ) + modelScope.launch { + // todo account token list sorting + } + } else { + val list = cachedTokenList ?: return + if (list.sortedBy == TokensSortType.BALANCE) return + + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) + + modelScope.launch { + toggleTokenListSortingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { + stateHolder.updateStateAfterTokenListSorting(it) + cachedTokenList = it + }, + ) + } } } override fun onGroupClick() { - val list = cachedTokenList ?: return + if (accountsFeatureToggles.isFeatureEnabled) { + cachedAccountStatusList ?: return + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) + modelScope.launch { + // todo account token list grouping + } + } else { + val list = cachedTokenList ?: return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) - modelScope.launch { - toggleTokenListGroupingUseCase(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSorting(it) - cachedTokenList = it - }, - ) + modelScope.launch { + toggleTokenListGroupingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { + stateHolder.updateStateAfterTokenListSorting(it) + cachedTokenList = it + }, + ) + } } } override fun onApplyClick() { modelScope.launch { stateHolder.updateStateToDisplayProgress() - - val listState = uiState.value.itemsState val resolver = CryptoCurrenciesIdsResolver() - - val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork val isSortedByBalance = uiState.value.header.isSortedByBalance - sendAnalyticsEvent( - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) + val result = if (accountsFeatureToggles.isFeatureEnabled) { + val tokensListUM = uiState.value.tokenListUM - val result = applyTokenListSortingUseCase( - userWalletId = userWalletId, - sortedTokensIds = resolver.resolve(listState, cachedTokenList), - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) + val isGroupedByNetwork = tokensListUM.isGrouped + + sendAnalyticsEvent( + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + // val sorted = resolver.resolveV2(tokensListUM, cachedAccountStatusList) + TODO("Implement account token list sorting") + } else { + val listState = uiState.value.itemsState + + val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork + + sendAnalyticsEvent( + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + applyTokenListSortingUseCase( + userWalletId = userWalletId, + sortedTokensIds = resolver.resolve(listState, cachedTokenList), + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + } result.fold( ifLeft = stateHolder::updateStateWithError, @@ -161,10 +210,24 @@ internal class OrganizeTokensModel @Inject constructor( private fun bootstrapTokenList() { modelScope.launch { - val tokenList = getTokenList() ?: return@launch + if (accountsFeatureToggles.isFeatureEnabled) { + val accountList = singleAccountStatusListSupplier.getSyncOrNull( + SingleAccountStatusListProducer.Params(userWalletId), + ) ?: return@launch - stateHolder.updateStateWithTokenList(tokenList) - cachedTokenList = tokenList + val isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() + + stateHolder.updateStateWithAccountList( + accountStatusList = accountList, + isAccountsModeEnabled = isAccountsModeEnabled, + ) + + cachedAccountStatusList = accountList + } else { + val tokenList = getTokenList() ?: return@launch + stateHolder.updateStateWithTokenList(tokenList) + cachedTokenList = tokenList + } } } @@ -180,14 +243,25 @@ internal class OrganizeTokensModel @Inject constructor( } private fun bootstrapDragAndDropUpdates() { - dragAndDropAdapter.dragAndDropUpdates - .distinctUntilChanged() - .onEach { (type, updatedListState) -> - disableSortingByBalanceIfListChanged(type) + if (accountsFeatureToggles.isFeatureEnabled) { + dragAndDropAdapterV2.dragAndDropUpdates + .distinctUntilChanged() + .onEach { (type, updatedListState) -> + disableSortingByBalanceIfListChangedV2(type) - stateHolder.updateStateWithManualSorting(updatedListState) - } - .launchIn(modelScope) + stateHolder.updateStateWithManualSortingV2(updatedListState) + } + .launchIn(modelScope) + } else { + dragAndDropAdapter.dragAndDropUpdates + .distinctUntilChanged() + .onEach { (type, updatedListState) -> + disableSortingByBalanceIfListChanged(type) + + stateHolder.updateStateWithManualSorting(updatedListState) + } + .launchIn(modelScope) + } } private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) { @@ -199,6 +273,15 @@ internal class OrganizeTokensModel @Inject constructor( } } + private fun disableSortingByBalanceIfListChangedV2(dragOperationType: DragAndDropAdapterV2.DragOperation.Type) { + if (dragOperationType !is DragAndDropAdapterV2.DragOperation.Type.End) return + + if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { + cachedAccountStatusList = cachedAccountStatusList?.copy(sortType = TokensSortType.NONE) + stateHolder.disableSortingByBalance() + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 7e70100e8b..ba99940e19 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -8,15 +8,13 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.wallet.state.model.* +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList @@ -88,7 +86,7 @@ internal object WalletPreviewData { private const val networksSize = 10 private const val tokensSize = 3 - private val draggableItems by lazy { + private val draggableItems: PersistentList by lazy { List(networksSize) { it } .flatMap { index -> val lastNetworkIndex = networksSize - 1 @@ -98,11 +96,13 @@ internal object WalletPreviewData { val group = DraggableItem.GroupHeader( id = networkNumber, networkName = "$networkNumber", + roundingMode = when (index) { 0 -> DraggableItem.RoundingMode.Top() lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() else -> DraggableItem.RoundingMode.None }, + accountId = "account_$networkNumber", ) val tokens: MutableList = mutableListOf() @@ -117,6 +117,7 @@ internal object WalletPreviewData { ), ), groupId = group.id, + accountId = "account_$networkNumber", roundingMode = when { i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() else -> DraggableItem.RoundingMode.None @@ -125,7 +126,10 @@ internal object WalletPreviewData { ) } - val divider = DraggableItem.Placeholder(id = "divider_$networkNumber") + val divider = DraggableItem.Placeholder( + id = "divider_$networkNumber", + accountId = "account_$networkNumber", + ) buildList { add(group) @@ -154,6 +158,7 @@ internal object WalletPreviewData { itemsState = OrganizeTokensListState.GroupedByNetwork( items = draggableItems, ), + tokenListUM = OrganizeTokensListUM.EmptyList, header = OrganizeTokensState.HeaderConfig( onSortClick = {}, onGroupClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 84a19cc5e5..6478fc9f80 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -33,6 +33,7 @@ import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.tokenlist.ExpandedPortfolioHeader import com.tangem.core.ui.components.tokenlist.internal.DraggableGroupTitleItem import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference @@ -47,6 +48,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.rememberReorderableLazyListState @@ -73,6 +75,7 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier .padding(paddingValues) .fillMaxSize(), listState = tokensListState, + tokensListUM = state.tokenListUM, state = state.itemsState, dndConfig = state.dndConfig, isBalanceHidden = state.isBalanceHidden, @@ -96,11 +99,17 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier private fun TokenList( listState: LazyListState, state: OrganizeTokensListState, + tokensListUM: OrganizeTokensListUM, dndConfig: OrganizeTokensState.DragAndDropConfig, isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current + val tokenList = if (tokensListUM !is OrganizeTokensListUM.EmptyList) { + tokensListUM.items + } else { + state.items + } Box(modifier = modifier) { val onDragEnd: (Int, Int) -> Unit = remember { { _, _ -> @@ -127,7 +136,7 @@ private fun TokenList( // because sometimes items disappear after reordering in 1.7.4+ compose-runtime version // check removing after update to compose-runtime 1.8.0+ val forceRecompose = remember { mutableIntStateOf(0) } - LaunchedEffect(state.items) { + LaunchedEffect(tokenList) { forceRecompose.intValue++ } @@ -140,7 +149,7 @@ private fun TokenList( contentPadding = listContentPadding, ) { itemsIndexed( - items = state.items, + items = tokenList, key = { _, item -> item.id }, ) { index, item -> @@ -206,6 +215,13 @@ private fun LazyItemScope.DraggableItem( ) // Should be presented in the list but remain invisible is DraggableItem.Placeholder -> Box(modifier = Modifier.fillMaxWidth()) + is DraggableItem.Portfolio -> ExpandedPortfolioHeader( + state = item.tokenItemState, + isCollapsable = false, + modifier = modifierWithBackground + .padding(top = 8.dp) + .fillMaxWidth(), + ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 193ed15375..9c08e93374 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -2,19 +2,24 @@ package com.tangem.feature.wallet.presentation.organizetokens import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverterV2 import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.NetworkGroupToDraggableItemsConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2 import com.tangem.utils.Provider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -23,7 +28,9 @@ import kotlinx.coroutines.flow.update internal class OrganizeTokensStateHolder( private val intents: OrganizeTokensIntents, private val dragAndDropIntents: DragAndDropIntents, + private val dragAndDropAdapterV2: DragAndDropAdapterV2, private val appCurrencyProvider: Provider, + private val accountsFeatureToggles: AccountsFeatureToggles, ) { private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) @@ -54,6 +61,16 @@ internal class OrganizeTokensStateHolder( updateState { tokenListConverter.convert(tokenList) } } + fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { + updateState { + TokenListToStateConverterV2( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = appCurrencyProvider(), + ).transform(this) + } + } + fun updateStateAfterTokenListSorting(tokenList: TokenList) { updateState { tokenListConverter.convert(tokenList).copy( @@ -70,6 +87,10 @@ internal class OrganizeTokensStateHolder( updateState { inProgressStateConverter.convertBack(value = this) } } + fun updateStateWithManualSortingV2(tokenListUM: OrganizeTokensListUM) { + updateState { copy(tokenListUM = tokenListUM) } + } + fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) { updateState { copy(itemsState = itemsState) } } @@ -94,6 +115,7 @@ internal class OrganizeTokensStateHolder( return OrganizeTokensState( onBackClick = intents::onBackClick, itemsState = OrganizeTokensListState.Empty, + tokenListUM = OrganizeTokensListUM.EmptyList, header = OrganizeTokensState.HeaderConfig( onSortClick = intents::onSortClick, onGroupClick = intents::onGroupClick, @@ -102,12 +124,21 @@ internal class OrganizeTokensStateHolder( onApplyClick = intents::onApplyClick, onCancelClick = intents::onCancelClick, ), - dndConfig = OrganizeTokensState.DragAndDropConfig( - onItemDragged = dragAndDropIntents::onItemDragged, - onItemDragStart = dragAndDropIntents::onItemDraggingStart, - onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, - canDragItemOver = dragAndDropIntents::canDragItemOver, - ), + dndConfig = if (accountsFeatureToggles.isFeatureEnabled) { + OrganizeTokensState.DragAndDropConfig( + onItemDragged = dragAndDropAdapterV2::onItemDragged, + onItemDragStart = dragAndDropAdapterV2::onItemDraggingStart, + onItemDragEnd = dragAndDropAdapterV2::onItemDraggingEnd, + canDragItemOver = dragAndDropAdapterV2::canDragItemOver, + ) + } else { + OrganizeTokensState.DragAndDropConfig( + onItemDragged = dragAndDropIntents::onItemDragged, + onItemDragStart = dragAndDropIntents::onItemDraggingStart, + onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, + canDragItemOver = dragAndDropIntents::canDragItemOver, + ) + }, scrollListToTop = consumedEvent(), isBalanceHidden = true, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt index b3443ebd3f..da7e0746a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt @@ -6,7 +6,6 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem.RoundingMode /** * Helper class for the DND list items @@ -26,12 +25,14 @@ internal sealed class DraggableItem { * * @property id ID of the network group * @property networkName network group name + * @property accountId account id * @property roundingMode item [RoundingMode] * @property showShadow if true then item should be elevated * */ data class GroupHeader( override val id: Int, val networkName: String, + val accountId: String = "", override val roundingMode: RoundingMode = RoundingMode.None, override val showShadow: Boolean = false, ) : DraggableItem() { @@ -50,6 +51,7 @@ internal sealed class DraggableItem { * * @property tokenItemState state of the token item * @property groupId ID of the network group which contains this token + * @property accountId account id * @property id ID of the token * @property roundingMode item [RoundingMode] * @property showShadow if true then item should be elevated @@ -57,6 +59,7 @@ internal sealed class DraggableItem { data class Token( val tokenItemState: TokenItemState.Draggable, val groupId: Int, + val accountId: String = "", override val showShadow: Boolean = false, override val roundingMode: RoundingMode = RoundingMode.None, ) : DraggableItem() { @@ -67,14 +70,32 @@ internal sealed class DraggableItem { * Helper item used to detect possible positions where a draggable item can be placed. * * @property id ID of the placeholder + * @property accountId account id * */ data class Placeholder( override val id: String, + val accountId: String = "", ) : DraggableItem() { override val showShadow: Boolean = false override val roundingMode: RoundingMode = RoundingMode.None } + /** + * Item for portfolio. + * + * @property tokenItemState state of the portfolio item + * @property id ID of the portfolio + * @property roundingMode item [RoundingMode] + * @property showShadow if true then item should be elevated + * */ + data class Portfolio( + override val roundingMode: RoundingMode = RoundingMode.None, + val tokenItemState: TokenItemState, + ) : DraggableItem() { + override val id: String = tokenItemState.id + override val showShadow: Boolean = false + } + /** * Rounding mode of the [DraggableItem] * @@ -122,6 +143,7 @@ internal sealed class DraggableItem { * */ fun updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) { is Placeholder -> this + is Portfolio -> this.copy(roundingMode = mode) is GroupHeader -> this.copy(roundingMode = mode) is Token -> this.copy(roundingMode = mode) } @@ -134,7 +156,9 @@ internal sealed class DraggableItem { * @return updated [DraggableItem] * */ fun updateShadowVisibility(show: Boolean): DraggableItem = when (this) { - is Placeholder -> this + is Portfolio, + is Placeholder, + -> this is GroupHeader -> this.copy(showShadow = show) is Token -> this.copy(showShadow = show) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt index 111a8b5211..720edbb802 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +@Deprecated("Use OrganizeTokensListUM instead, will be removed in future releases") @Immutable internal sealed class OrganizeTokensListState { abstract val items: PersistentList @@ -19,4 +20,26 @@ internal sealed class OrganizeTokensListState { data object Empty : OrganizeTokensListState() { override val items: PersistentList = persistentListOf() } +} + +@Immutable +internal sealed interface OrganizeTokensListUM { + + val items: PersistentList + val isGrouped: Boolean + + data class AccountList( + override val items: PersistentList, + override val isGrouped: Boolean, + ) : OrganizeTokensListUM + + data class TokensList( + override val items: PersistentList, + override val isGrouped: Boolean, + ) : OrganizeTokensListUM + + data object EmptyList : OrganizeTokensListUM { + override val items: PersistentList = persistentListOf() + override val isGrouped: Boolean = false + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt index d290f81bb7..556f2e081d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt @@ -8,6 +8,7 @@ import org.burnoutcrew.reorderable.ItemPosition internal data class OrganizeTokensState( val onBackClick: () -> Unit, val itemsState: OrganizeTokensListState, + val tokenListUM: OrganizeTokensListUM, val header: HeaderConfig, val actions: ActionsConfig, val dndConfig: DragAndDropConfig, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt index 1db8f4356d..80ea9a25b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt @@ -2,6 +2,9 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -internal fun getGroupPlaceholder(index: Int): DraggableItem.Placeholder { - return DraggableItem.Placeholder(id = "placeholder_${index.inc()}") +internal fun getGroupPlaceholder(index: Int, accountId: String = ""): DraggableItem.Placeholder { + return DraggableItem.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/presentation/organizetokens/utils/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt index 7a0beac577..2c10ffaf82 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt @@ -12,7 +12,9 @@ internal fun List.uniteItems(): List { 1 -> DraggableItem.RoundingMode.Top() lastItemIndex -> DraggableItem.RoundingMode.Bottom() else -> when (item) { - is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None + is DraggableItem.Portfolio, + is DraggableItem.Placeholder, + -> DraggableItem.RoundingMode.None is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true) is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) { DraggableItem.RoundingMode.Bottom(showGap = true) @@ -28,6 +30,48 @@ internal fun List.uniteItems(): List { } } +internal fun List.uniteItemsV2(isAccountsMode: Boolean): List { + val items = this + val lastItemIndex = items.lastIndex + + return items + .asSequence() + .mapIndexed { index, item -> + val mode = when (index) { + 0 -> if (item is DraggableItem.Placeholder) { + DraggableItem.RoundingMode.None + } else { + DraggableItem.RoundingMode.Top() + } + lastItemIndex -> DraggableItem.RoundingMode.Bottom() + 1 -> if (items.first() is DraggableItem.Placeholder) { + DraggableItem.RoundingMode.Top() + } else { + DraggableItem.RoundingMode.None + } + else -> when (item) { + is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None + is DraggableItem.GroupHeader -> if (isAccountsMode) { + DraggableItem.RoundingMode.None + } else { + DraggableItem.RoundingMode.Top(showGap = true) + } + is DraggableItem.Token -> applyRoundingModeToToken( + isAccountsMode = isAccountsMode, + items = items, + index = index, + lastItemIndex = lastItemIndex, + ) + is DraggableItem.Portfolio -> DraggableItem.RoundingMode.Top(showGap = true) + } + } + + item + .updateRoundingMode(mode) + .updateShadowVisibility(show = false) + }.toList() +} + internal fun List.divideMovingItem(movingItem: DraggableItem): List { val mutableList = this.toMutableList() val listIterator = mutableList.listIterator() @@ -63,12 +107,55 @@ private fun List.prepareItems(): List { return mutableListOf().apply { add(DraggableItem.Placeholder(firstPlaceholderId)) - val itemsWithoutFirstPlaceholder = if (items.firstOrNull()?.id == firstPlaceholderId) { - items.drop(n = 1) - } else { - items - } + val itemsWithoutFirstPlaceholder = items.filterNot { it.id == firstPlaceholderId } addAll(itemsWithoutFirstPlaceholder) } +} + +/** + * 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 DraggableItem.Portfolio || + items[index + 1] is DraggableItem.Placeholder && items[index + 2] is DraggableItem.Portfolio) -> { + DraggableItem.RoundingMode.Bottom(showGap = true) + } + (!isAccountsMode || index + 1 == lastItemIndex) && items[index + 1] is DraggableItem.Placeholder -> { + DraggableItem.RoundingMode.Bottom(showGap = true) + } + else -> DraggableItem.RoundingMode.None } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt index 3b6cf65d8d..d67847aa0a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList @@ -15,4 +16,16 @@ internal inline fun OrganizeTokensListState.updateItems( is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems) is OrganizeTokensListState.Empty -> this } +} + +internal inline fun OrganizeTokensListUM.updateItems( + update: (PersistentList) -> List, +): OrganizeTokensListUM { + val updatedItems = update(items).toPersistentList() + + return when (this) { + is OrganizeTokensListUM.AccountList -> copy(items = updatedItems) + is OrganizeTokensListUM.TokensList -> copy(items = updatedItems) + OrganizeTokensListUM.EmptyList -> this + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt new file mode 100644 index 0000000000..75e1c92282 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt @@ -0,0 +1,100 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter + +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +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.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2 +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.OrganizedTokenListConverter +import com.tangem.utils.converter.Converter +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class TokenListToStateConverterV2( + private val accountStatusList: AccountStatusList, + private val isAccountsMode: Boolean, + private val appCurrency: AppCurrency, +) : Transformer { + + private val accountListItemConverter by lazy { + AccountTokenItemConverter(appCurrency = appCurrency, isAccountsMode) + } + + override fun transform(prevState: OrganizeTokensState): OrganizeTokensState { + val tokenListUM = accountListItemConverter.convert(accountStatusList) + + return prevState.copy( + tokenListUM = tokenListUM, + header = prevState.header.copy( + isEnabled = tokenListUM !is OrganizeTokensListUM.EmptyList, + isSortedByBalance = accountStatusList.sortType == TokensSortType.BALANCE, + isGrouped = accountStatusList.groupType == TokensGroupType.NETWORK, + ), + actions = prevState.actions.copy( + canApply = tokenListUM !is OrganizeTokensListUM.EmptyList, + ), + ) + } +} + +internal class AccountTokenItemConverter( + private val appCurrency: AppCurrency, + private val isAccountsMode: Boolean, +) : Converter { + + private val organizedTokenListConverter by lazy { + OrganizedTokenListConverter(appCurrency) + } + + override fun convert(value: AccountStatusList): OrganizeTokensListUM { + val isGrouping = value.groupType == TokensGroupType.NETWORK + return if (isAccountsMode) { + OrganizeTokensListUM.AccountList( + isGrouped = isGrouping, + items = value.accountStatuses + .asSequence() + .filterIsInstance() + .flatMap { accountStatus -> + if (accountStatus.tokenList != TokenList.Empty) { + buildList { + add( + DraggableItem.Portfolio( + tokenItemState = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = accountStatus.account, + ).convert(TotalFiatBalance.Loading), + ), + ) + if (isGrouping) { + add(getGroupPlaceholder(accountId = accountStatus.accountId.value, index = -1)) + } + addAll(organizedTokenListConverter.convert(accountStatus)) + } + } else { + emptyList() + } + }.toList() + .uniteItemsV2(true).toPersistentList(), + ) + } else { + OrganizeTokensListUM.TokensList( + isGrouped = isGrouping, + items = buildList { + if (isGrouping) { + add(getGroupPlaceholder(accountId = value.mainAccount.accountId.value, index = -1)) + } + addAll(organizedTokenListConverter.convert(value.mainAccount)) + }.uniteItemsV2(false) + .toPersistentList(), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt new file mode 100644 index 0000000000..8a8dc8252b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt @@ -0,0 +1,67 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items + +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero +import java.math.BigDecimal + +internal class CryptoCurrencyToDraggableItemConverterV2( + private val appCurrency: AppCurrency, +) : Converter { + + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + + override fun convert(value: AccountCryptoCurrencyStatus): DraggableItem.Token { + return createDraggableToken(value) + } + + private fun createDraggableToken(accountCryptoCurrencyStatus: AccountCryptoCurrencyStatus): DraggableItem.Token { + val (account, currencyStatus) = accountCryptoCurrencyStatus + return DraggableItem.Token( + tokenItemState = createTokenItemState(currencyStatus, appCurrency), + groupId = getGroupHeaderId(currencyStatus.currency.network), + accountId = account.accountId.value, + ) + } + + private fun createTokenItemState( + currencyStatus: CryptoCurrencyStatus, + appCurrency: AppCurrency, + ): TokenItemState.Draggable { + val currency = currencyStatus.currency + + return TokenItemState.Draggable( + id = getTokenItemId(currency.id), + iconState = iconStateConverter.convert(currencyStatus), + titleState = TokenItemState.TitleState.Content(text = stringReference(currency.name)), + subtitle2State = if (currencyStatus.value.isError) { + TokenItemState.Subtitle2State.Unreachable + } else { + TokenItemState.Subtitle2State.TextContent(text = getFormattedFiatAmount(currencyStatus, appCurrency)) + }, + ) + } + + private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { + val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data + val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO + val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) + ?.multiply(fiatRate).orZero() + + val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN + return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt new file mode 100644 index 0000000000..13206c8a13 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items + +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder +import com.tangem.utils.converter.Converter + +internal class NetworkGroupToDraggableItemsConverterV2( + private val itemConverter: CryptoCurrencyToDraggableItemConverterV2, +) : Converter, List> { + + override fun convert(value: Pair): List { + val (account, networkGroup) = value + return buildList { + add(createGroupHeader(account.accountId, networkGroup)) + addAll(createTokens(account, networkGroup)) + } + } + + override fun convertList( + input: Collection>, + ): List> { + return input.mapIndexed { index, pair -> + convert(pair).toMutableList() + .also { mutableGroup -> + mutableGroup.add( + getGroupPlaceholder(accountId = pair.first.accountId.value, index = index), + ) + } + } + } + + private fun createGroupHeader(accountId: AccountId, group: NetworkGroup) = DraggableItem.GroupHeader( + id = getGroupHeaderId(group.network), + accountId = accountId.value, + networkName = group.network.name, + ) + + private fun createTokens(account: Account.CryptoPortfolio, group: NetworkGroup): List { + return itemConverter.convertList( + group.currencies.map { + AccountCryptoCurrencyStatus( + account = account, + status = it, + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt new file mode 100644 index 0000000000..295848f5f4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items + +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class OrganizedTokenListConverter( + private val appCurrency: AppCurrency, +) : Converter> { + + private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) } + private val groupsConverter by lazy { + NetworkGroupToDraggableItemsConverterV2(tokensConverter) + } + + override fun convert(value: AccountStatus): PersistentList { + val cryptoAccount = value.account as? Account.CryptoPortfolio ?: return persistentListOf() + return when (val tokenList = value.getCryptoTokenList()) { + is TokenList.GroupedByNetwork -> groupsConverter.convertList( + tokenList.groups.map { cryptoAccount to it }, + ) + .flatten() + .toPersistentList() + + is TokenList.Ungrouped -> tokensConverter.convertList( + value.flattenCurrencies().map { + AccountCryptoCurrencyStatus( + account = cryptoAccount, + status = it, + ) + }, + ).toPersistentList() + + is TokenList.Empty -> persistentListOf() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt index 39d8295da6..7158aa1260 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt @@ -51,7 +51,9 @@ internal class DragAndDropAdapter( return when (draggingItem) { is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex) is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem) - is DraggableItem.Placeholder -> false + is DraggableItem.Placeholder, + is DraggableItem.Portfolio, + -> false } } @@ -61,7 +63,9 @@ internal class DragAndDropAdapter( updateListState(DragOperation.Type.Start) { when (item) { - is DraggableItem.Placeholder -> items + is DraggableItem.Placeholder, + is DraggableItem.Portfolio, + -> items is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) is DraggableItem.Token -> when (this) { is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item) @@ -81,7 +85,9 @@ internal class DragAndDropAdapter( when (draggingItem) { is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) is DraggableItem.Token -> items.uniteItems() - is DraggableItem.Placeholder -> items + is DraggableItem.Placeholder, + is DraggableItem.Portfolio, + -> items } } @@ -144,7 +150,9 @@ internal class DragAndDropAdapter( return when (moveOverItem) { is DraggableItem.GroupHeader -> false // Token item can not be moved to group item is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group - is DraggableItem.Placeholder -> false + is DraggableItem.Portfolio, + is DraggableItem.Placeholder, + -> false } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt new file mode 100644 index 0000000000..6c0a7531b8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt @@ -0,0 +1,209 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd + +import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2 +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems +import com.tangem.utils.Provider +import kotlinx.collections.immutable.mutate +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import org.burnoutcrew.reorderable.ItemPosition + +internal class DragAndDropAdapterV2( + private val tokenListUMProvider: Provider, +) : DragAndDropIntents { + + private val tokenListUM: OrganizeTokensListUM + get() = tokenListUMProvider.invoke() + + private val draggableGroupsOperations = DraggableGroupsOperations() + + private val dragAndDropUpdatesInternal: MutableStateFlow = MutableStateFlow(value = null) + + private var draggingItem: DraggableItem? = null + private var draggingListState: OrganizeTokensListUM? = null + + val dragAndDropUpdates: Flow + get() = dragAndDropUpdatesInternal.filterNotNull() + + override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { + val items = when (val listState = tokenListUM) { + is OrganizeTokensListUM.AccountList -> listState.items + is OrganizeTokensListUM.TokensList -> { + if (tokenListUM.isGrouped) { + listState.items + } else { + return true // If ungrouped then item can be moved anywhere + } + } + OrganizeTokensListUM.EmptyList -> return true + } + + val (dragOverItem, draggingItem) = findItemsToMove( + items = items, + moveOverItemKey = dragOver.key, + movedItemKey = dragging.key, + ) + + if (dragOverItem == null || draggingItem == null) { + return false + } + + val canDrag = when (draggingItem) { + is DraggableItem.GroupHeader -> checkCanMoveHeaderOver( + item = draggingItem, + moveOverItem = dragOverItem, + ) + is DraggableItem.Token -> checkCanMoveTokenOver( + item = draggingItem, + moveOverItem = dragOverItem, + isAccountsMode = tokenListUM is OrganizeTokensListUM.AccountList, + isGrouped = tokenListUM.isGrouped, + ) + is DraggableItem.Placeholder, + is DraggableItem.Portfolio, + -> false + } + + return canDrag + } + + override fun onItemDraggingStart(item: DraggableItem) { + if (draggingItem != null) return + draggingItem = item + + updateListState(DragOperation.Type.Start) { + when (item) { + is DraggableItem.Placeholder, + is DraggableItem.Portfolio, + -> items + is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroupV2(items, item) + .divideMovingItem(item) + is DraggableItem.Token -> items.divideMovingItem(item) + } + } + + draggingListState = tokenListUM + } + + override fun onItemDraggingEnd() { + val draggingItem = draggingItem ?: return + + updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) { + when (draggingItem) { + is DraggableItem.GroupHeader -> { + draggableGroupsOperations.expandGroupsV2(items) + .uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList) + } + is DraggableItem.Token -> { + items.uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList) + } + is DraggableItem.Placeholder, + is DraggableItem.Portfolio, + -> items + } + } + + this.draggingItem = null + } + + override fun onItemDragged(from: ItemPosition, to: ItemPosition) { + updateListState(DragOperation.Type.Dragged) { + items.mutate { + it.add(to.index, it.removeAt(from.index)) + } + } + } + + private fun updateListState(type: DragOperation.Type, block: OrganizeTokensListUM.() -> List) { + val updatedState = tokenListUM.updateItems { block(tokenListUM) } + + dragAndDropUpdatesInternal.value = DragOperation(type, updatedState) + } + + private fun findItemsToMove( + items: List, + moveOverItemKey: Any?, + movedItemKey: Any?, + ): Pair { + var moveOverItem: DraggableItem? = null + var movedItem: DraggableItem? = 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: DraggableItem.GroupHeader, moveOverItem: DraggableItem): Boolean { + return when (moveOverItem) { + // Header can be moved only in its account + is DraggableItem.Placeholder -> item.accountId == moveOverItem.accountId + else -> false + } + } + + private fun checkCanMoveTokenOver( + item: DraggableItem.Token, + moveOverItem: DraggableItem, + isGrouped: Boolean, + isAccountsMode: Boolean, + ): Boolean { + return when (moveOverItem) { + is DraggableItem.GroupHeader -> false // Token item can not be moved to group item + is DraggableItem.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 DraggableItem.Portfolio, + is DraggableItem.Placeholder, + -> false // Token item can not be moved to portfolio or placeholder + } + } + + private fun checkIsItemsOrderChanged(): Boolean { + fun OrganizeTokensListUM?.getItemsIds(): List? = this?.items?.mapNotNull { item -> + if (item is DraggableItem.Placeholder) { + null + } else { + item.id + } + } + + return tokenListUM.getItemsIds() != draggingListState.getItemsIds() + } + + data class DragOperation( + val type: Type, + val listState: OrganizeTokensListUM, + ) { + + 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/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt index 3891fdea66..e92d772829 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt @@ -24,6 +24,21 @@ internal class DraggableGroupsOperations { return itemsWithoutGroupTokens.divideMovingItem(movingGroup) } + fun collapseGroupV2(items: List, movingGroup: DraggableItem.GroupHeader): List { + if (!groupIdToTokens.isNullOrEmpty()) return items + + groupIdToTokens = items + .asSequence() + .filterIsInstance() + .groupBy { it.groupId } + + val itemsWithoutGroupTokens = items.filterNot { + it is DraggableItem.Token && it.groupId == movingGroup.id + } + + return itemsWithoutGroupTokens.divideMovingItem(movingGroup) + } + fun expandGroups(items: List): List { if (groupIdToTokens.isNullOrEmpty()) return items @@ -46,4 +61,49 @@ internal class DraggableGroupsOperations { return expandedGroups } + + fun expandGroupsV2(items: List): List { + if (groupIdToTokens.isNullOrEmpty()) return items + + val accountList = items.filterIsInstance() + val currentGroups = items.filterIsInstance() + + val expandedGroups = if (items.any { it is DraggableItem.Portfolio }) { + 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 + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index d5b1c850ac..7b24803992 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.PortfolioId @@ -21,7 +22,6 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList @@ -82,8 +82,6 @@ internal class TokenListStateConverter( private fun convertAccountList(params: TokenConverterParams.Account): WalletTokensListState { val accountList = params.accountList - // todo account null for firs iteration? - val organizeTokensButtonConfig: OrganizeTokensButtonConfig? = null fun AccountStatus.CryptoPortfolio.map(): TokensListItemUM.Portfolio { val tokenList: TokenList = this.tokenList @@ -128,7 +126,7 @@ internal class TokenListStateConverter( } return WalletTokensListState.ContentState.PortfolioContent( items = accountItems.toPersistentList(), - organizeTokensButtonConfig = organizeTokensButtonConfig, + organizeTokensButtonConfig = getOrganizeTokensButtonStateV2(accountList = accountList), ) } @@ -193,6 +191,17 @@ internal class TokenListStateConverter( } } + private fun getOrganizeTokensButtonStateV2(accountList: AccountStatusList): WalletOrganizeTokensButtonConfig? { + return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) { + WalletOrganizeTokensButtonConfig( + isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + null + } + } + private fun isSingleCurrencyWalletWithToken(): Boolean { return selectedWallet is UserWallet.Cold && selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()