Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-06 15:59:41 +05:00
parent 804e4eca23
commit fd018a73f9
21 changed files with 918 additions and 79 deletions

View file

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

View file

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

View file

@ -38,4 +38,8 @@ sealed interface AccountStatus {
fun flattenCurrencies(): List<CryptoCurrencyStatus> = when (this) {
is CryptoPortfolio -> tokenList.flattenCurrencies()
}
fun getCryptoTokenList(): TokenList = when (this) {
is CryptoPortfolio -> tokenList
}
}

View file

@ -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<OrganizeTokensComponent.Params>().userWalletId
private var cachedTokenList: TokenList? = null
private var cachedAccountStatusList: AccountStatusList? = null
val uiState: StateFlow<OrganizeTokensState> = 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<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->

View file

@ -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<DraggableItem> 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<DraggableItem.Token> = 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 = {},

View file

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

View file

@ -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<AppCurrency>,
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
private val stateFlowInternal: MutableStateFlow<OrganizeTokensState> = 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,
)

View file

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

View file

@ -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<DraggableItem>
@ -19,4 +20,26 @@ internal sealed class OrganizeTokensListState {
data object Empty : OrganizeTokensListState() {
override val items: PersistentList<DraggableItem> = persistentListOf()
}
}
@Immutable
internal sealed interface OrganizeTokensListUM {
val items: PersistentList<DraggableItem>
val isGrouped: Boolean
data class AccountList(
override val items: PersistentList<DraggableItem>,
override val isGrouped: Boolean,
) : OrganizeTokensListUM
data class TokensList(
override val items: PersistentList<DraggableItem>,
override val isGrouped: Boolean,
) : OrganizeTokensListUM
data object EmptyList : OrganizeTokensListUM {
override val items: PersistentList<DraggableItem> = persistentListOf()
override val isGrouped: Boolean = false
}
}

View file

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

View file

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

View file

@ -12,7 +12,9 @@ internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
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<DraggableItem>.uniteItems(): List<DraggableItem> {
}
}
internal fun List<DraggableItem>.uniteItemsV2(isAccountsMode: Boolean): List<DraggableItem> {
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<DraggableItem>.divideMovingItem(movingItem: DraggableItem): List<DraggableItem> {
val mutableList = this.toMutableList()
val listIterator = mutableList.listIterator()
@ -63,12 +107,55 @@ private fun List<DraggableItem>.prepareItems(): List<DraggableItem> {
return mutableListOf<DraggableItem>().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<DraggableItem>,
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
}

View file

@ -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<DraggableItem>) -> List<DraggableItem>,
): OrganizeTokensListUM {
val updatedItems = update(items).toPersistentList()
return when (this) {
is OrganizeTokensListUM.AccountList -> copy(items = updatedItems)
is OrganizeTokensListUM.TokensList -> copy(items = updatedItems)
OrganizeTokensListUM.EmptyList -> this
}
}

View file

@ -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<OrganizeTokensState> {
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<AccountStatusList, OrganizeTokensListUM> {
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<AccountStatus.CryptoPortfolio>()
.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(),
)
}
}
}

View file

@ -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<AccountCryptoCurrencyStatus, DraggableItem.Token> {
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) }
}
}

View file

@ -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<Pair<Account.CryptoPortfolio, NetworkGroup>, List<DraggableItem>> {
override fun convert(value: Pair<Account.CryptoPortfolio, NetworkGroup>): List<DraggableItem> {
val (account, networkGroup) = value
return buildList {
add(createGroupHeader(account.accountId, networkGroup))
addAll(createTokens(account, networkGroup))
}
}
override fun convertList(
input: Collection<Pair<Account.CryptoPortfolio, NetworkGroup>>,
): List<List<DraggableItem>> {
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<DraggableItem.Token> {
return itemConverter.convertList(
group.currencies.map {
AccountCryptoCurrencyStatus(
account = account,
status = it,
)
},
)
}
}

View file

@ -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<AccountStatus, PersistentList<DraggableItem>> {
private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) }
private val groupsConverter by lazy {
NetworkGroupToDraggableItemsConverterV2(tokensConverter)
}
override fun convert(value: AccountStatus): PersistentList<DraggableItem> {
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()
}
}
}

View file

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

View file

@ -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<OrganizeTokensListUM>,
) : DragAndDropIntents {
private val tokenListUM: OrganizeTokensListUM
get() = tokenListUMProvider.invoke()
private val draggableGroupsOperations = DraggableGroupsOperations()
private val dragAndDropUpdatesInternal: MutableStateFlow<DragOperation?> = MutableStateFlow(value = null)
private var draggingItem: DraggableItem? = null
private var draggingListState: OrganizeTokensListUM? = null
val dragAndDropUpdates: Flow<DragOperation>
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<DraggableItem>) {
val updatedState = tokenListUM.updateItems { block(tokenListUM) }
dragAndDropUpdatesInternal.value = DragOperation(type, updatedState)
}
private fun findItemsToMove(
items: List<DraggableItem>,
moveOverItemKey: Any?,
movedItemKey: Any?,
): Pair<DraggableItem?, DraggableItem?> {
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<Any>? = 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()
}
}
}

View file

@ -24,6 +24,21 @@ internal class DraggableGroupsOperations {
return itemsWithoutGroupTokens.divideMovingItem(movingGroup)
}
fun collapseGroupV2(items: List<DraggableItem>, movingGroup: DraggableItem.GroupHeader): List<DraggableItem> {
if (!groupIdToTokens.isNullOrEmpty()) return items
groupIdToTokens = items
.asSequence()
.filterIsInstance<DraggableItem.Token>()
.groupBy { it.groupId }
val itemsWithoutGroupTokens = items.filterNot {
it is DraggableItem.Token && it.groupId == movingGroup.id
}
return itemsWithoutGroupTokens.divideMovingItem(movingGroup)
}
fun expandGroups(items: List<DraggableItem>): List<DraggableItem> {
if (groupIdToTokens.isNullOrEmpty()) return items
@ -46,4 +61,49 @@ internal class DraggableGroupsOperations {
return expandedGroups
}
fun expandGroupsV2(items: List<DraggableItem>): List<DraggableItem> {
if (groupIdToTokens.isNullOrEmpty()) return items
val accountList = items.filterIsInstance<DraggableItem.Portfolio>()
val currentGroups = items.filterIsInstance<DraggableItem.GroupHeader>()
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
}
}

View file

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