Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-25 12:03:17 +05:00
commit 4f7b1b791b
47 changed files with 440 additions and 1451 deletions

View file

@ -177,20 +177,6 @@ internal object TokensDomainModule {
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
}
@Provides
@Singleton
fun provideToggleTokenListGroupingUseCase(
dispatchers: CoroutineDispatcherProvider,
): ToggleTokenListGroupingUseCase {
return ToggleTokenListGroupingUseCase(dispatchers)
}
@Provides
@Singleton
fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase {
return ToggleTokenListSortingUseCase(dispatchers)
}
@Provides
@Singleton
fun provideApplyTokenListSortingUseCase(

View file

@ -67,8 +67,8 @@ internal object AccountStatusUseCaseModule {
fun provideApplyTokenListSortingUseCaseV2(
accountsCRUDRepository: AccountsCRUDRepository,
dispatchers: CoroutineDispatcherProvider,
): ApplyTokenListSortingUseCaseV2 {
return ApplyTokenListSortingUseCaseV2(
): ApplyTokenListSortingUseCase {
return ApplyTokenListSortingUseCase(
accountsCRUDRepository = accountsCRUDRepository,
dispatchers = dispatchers,
)
@ -141,8 +141,8 @@ internal object AccountStatusUseCaseModule {
@Singleton
fun provideToggleTokenListSortingUseCaseV2(
dispatchers: CoroutineDispatcherProvider,
): ToggleTokenListSortingUseCaseV2 {
return ToggleTokenListSortingUseCaseV2(
): ToggleTokenListSortingUseCase {
return ToggleTokenListSortingUseCase(
dispatchers = dispatchers,
)
}
@ -151,8 +151,8 @@ internal object AccountStatusUseCaseModule {
@Singleton
fun provideToggleTokenListGroupingUseCaseV2(
dispatchers: CoroutineDispatcherProvider,
): ToggleTokenListGroupingUseCaseV2 {
return ToggleTokenListGroupingUseCaseV2(
): ToggleTokenListGroupingUseCase {
return ToggleTokenListGroupingUseCase(
dispatchers = dispatchers,
)
}

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import kotlinx.serialization.Serializable
typealias AccountCryptoCurrencies = Map<Account, List<CryptoCurrency>>
typealias AccountCryptoCurrencies = Map<out Account, List<CryptoCurrency>>
/**
* Combines an [Account] with its corresponding [CryptoCurrency].

View file

@ -25,7 +25,7 @@ private typealias SortingErrorByAccountId = MutableMap<AccountId, TokenListSorti
* @property accountsCRUDRepository Repository for CRUD operations on accounts.
* @property dispatchers Coroutine dispatcher provider for managing threading.
*/
class ApplyTokenListSortingUseCaseV2(
class ApplyTokenListSortingUseCase(
private val accountsCRUDRepository: AccountsCRUDRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -131,8 +131,8 @@ class ApplyTokenListSortingUseCaseV2(
val accountCurrencies = account.cryptoCurrencies.toList()
.sortTokens(sortedCurrencies = sortedCurrencies)
.getOrElse {
errors[account.accountId] = it
.getOrElse { error ->
errors[account.accountId] = error
return@map account
}

View file

@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
*
* @property dispatchers Provides coroutine dispatchers for executing tasks.
*/
class ToggleTokenListGroupingUseCaseV2(
class ToggleTokenListGroupingUseCase(
private val dispatchers: CoroutineDispatcherProvider,
) {

View file

@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
*
* @property dispatchers Provides coroutine dispatchers for executing tasks.
*/
class ToggleTokenListSortingUseCaseV2(
class ToggleTokenListSortingUseCase(
private val dispatchers: CoroutineDispatcherProvider,
) {

View file

@ -30,7 +30,7 @@ internal class ApplyTokenListSortingUseCaseTest {
private val accountsCRUDRepository = mockk<AccountsCRUDRepository>(relaxUnitFun = true)
private val useCase = ApplyTokenListSortingUseCaseV2(
private val useCase = ApplyTokenListSortingUseCase(
accountsCRUDRepository = accountsCRUDRepository,
dispatchers = TestingCoroutineDispatcherProvider(),
)

View file

@ -28,9 +28,9 @@ import java.math.BigDecimal
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ToggleTokenListGroupingUseCaseV2Test {
class ToggleTokenListGroupingUseCaseTest {
private val useCase = ToggleTokenListGroupingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider())
private val useCase = ToggleTokenListGroupingUseCase(dispatchers = TestingCoroutineDispatcherProvider())
private val userWalletId = UserWalletId("011")
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()

View file

@ -28,9 +28,9 @@ import java.math.BigDecimal
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ToggleTokenListSortingUseCaseV2Test {
class ToggleTokenListSortingUseCaseTest {
private val useCase = ToggleTokenListSortingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider())
private val useCase = ToggleTokenListSortingUseCase(dispatchers = TestingCoroutineDispatcherProvider())
private val userWalletId = UserWalletId("011")
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()

View file

@ -1,51 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.operations.TokenListFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class ToggleTokenListGroupingUseCase(
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(tokenList: TokenList): Either<TokenListSortingError, TokenList> {
return withContext(dispatchers.default) {
either {
when (tokenList) {
is TokenList.GroupedByNetwork -> ungroupTokens(tokenList)
is TokenList.Ungrouped -> groupTokens(tokenList)
is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty)
}
}
}
}
private fun Raise<TokenListSortingError>.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork {
validate(tokenList)
return TokenListFactory.createGroupedByNetwork(tokenList)
}
private fun Raise<TokenListSortingError>.ungroupTokens(tokenList: TokenList.GroupedByNetwork): TokenList.Ungrouped {
validate(tokenList)
return TokenListFactory.createUngrouped(tokenList)
}
private fun Raise<TokenListSortingError>.validate(tokenList: TokenList) {
ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) {
TokenListSortingError.TokenListIsLoading
}
ensure(tokenList.flattenCurrencies().isNotEmpty()) {
TokenListSortingError.TokenListIsEmpty
}
}
}

View file

@ -1,38 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.operations.TokenListFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class ToggleTokenListSortingUseCase(
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(tokenList: TokenList): Either<TokenListSortingError, TokenList> {
return withContext(dispatchers.default) {
either {
ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) {
TokenListSortingError.TokenListIsLoading
}
TokenListFactory.create(
statuses = tokenList.flattenCurrencies(),
groupType = when (tokenList) {
is TokenList.GroupedByNetwork -> TokensGroupType.NETWORK
is TokenList.Ungrouped -> TokensGroupType.NONE
is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty)
},
sortType = TokensSortType.BALANCE,
)
}
}
}
}

View file

@ -1,103 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.mock.MockTokenLists
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ToggleTokenListGroupingTest {
private val useCase = ToggleTokenListGroupingUseCase(
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Test
fun `when list is empty then error should be received`() = runTest {
// Given
val expected = TokenListSortingError.TokenListIsEmpty.left()
// When
val actual = useCase(MockTokenLists.emptyUngroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and loading then error should be received`() = runTest {
// Given
val expected = TokenListSortingError.TokenListIsLoading.left()
// When
val actual = useCase(MockTokenLists.loadingGroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and loading then error should be received`() = runTest {
// Given
val expected = TokenListSortingError.TokenListIsLoading.left()
// When
val actual = useCase(MockTokenLists.loadingUngroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and sorted then sorted grouped list should be received`() = runTest {
// Given
val expected = MockTokenLists.sortedGroupedTokenList.right()
// When
val actual = useCase(MockTokenLists.sortedUngroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and unsorted then unsorted grouped list should be received`() = runTest {
// Given
val expected = MockTokenLists.unsortedGroupedTokenList.right()
// When
val actual = useCase(MockTokenLists.unsortedUngroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and sorted then sorted ungrouped list should be received`() = runTest {
// Given
val expected = MockTokenLists.sortedUngroupedTokenList.right()
// When
val actual = useCase(MockTokenLists.sortedGroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and unsorted then unsorted ungrouped list should be received`() = runTest {
// Given
val expected = MockTokenLists.unsortedUngroupedTokenList.right()
// When
val actual = useCase(MockTokenLists.unsortedGroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
}

View file

@ -1,103 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.mock.MockTokenLists
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ToggleTokenListSortingUseCaseTest {
private val useCase = ToggleTokenListSortingUseCase(
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Test
fun `when list is empty then error should be received`() = runTest {
// Given
val expected = TokenListSortingError.TokenListIsEmpty.left()
// When
val actual = useCase(MockTokenLists.emptyTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and loading then error should be received`() = runTest {
// Given
val expected = TokenListSortingError.TokenListIsLoading.left()
// When
val actual = useCase(MockTokenLists.loadingGroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and loading then error should be received`() = runTest {
// Given
val expected = TokenListSortingError.TokenListIsLoading.left()
// When
val actual = useCase(MockTokenLists.loadingUngroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and unsorted then grouped and sorted list should be received`() = runTest {
// Given
val expected = MockTokenLists.sortedGroupedTokenList.right()
// When
val actual = useCase(MockTokenLists.unsortedGroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and unsorted then ungrouped and sorted list should be received`() = runTest {
// Given
val expected = MockTokenLists.sortedUngroupedTokenList.right()
// When
val actual = useCase(MockTokenLists.unsortedUngroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and sorted then grouped and unsorted list should be received`() = runTest {
// Given
val expected = MockTokenLists.sortedGroupedTokenList.right()
// When
val actual = useCase(MockTokenLists.unsortedGroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and sorted then ungrouped and unsorted list should be received`() = runTest {
// Given
val expected = MockTokenLists.sortedUngroupedTokenList.right()
// When
val actual = useCase(MockTokenLists.unsortedUngroupedTokenList)
// Then
Truth.assertThat(actual).isEqualTo(expected)
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen
import com.tangem.feature.wallet.child.organizetokens.ui.OrganizeTokensScreen
import kotlinx.coroutines.launch
internal class OrganizeTokensComponent(

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens.analytics
package com.tangem.feature.wallet.child.organizetokens.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens.model
package com.tangem.feature.wallet.child.organizetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.token.state.TokenItemState

View file

@ -1,27 +1,9 @@
package com.tangem.feature.wallet.presentation.organizetokens.model
package com.tangem.feature.wallet.child.organizetokens.entity
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>
data class GroupedByNetwork(
override val items: PersistentList<DraggableItem>,
) : OrganizeTokensListState()
data class Ungrouped(
override val items: PersistentList<DraggableItem>,
) : OrganizeTokensListState()
data object Empty : OrganizeTokensListState() {
override val items: PersistentList<DraggableItem> = persistentListOf()
}
}
@Immutable
internal sealed interface OrganizeTokensListUM {

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens.model
package com.tangem.feature.wallet.child.organizetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.event.StateEvent
@ -7,7 +7,6 @@ import org.burnoutcrew.reorderable.ItemPosition
@Immutable
internal data class OrganizeTokensState(
val onBackClick: () -> Unit,
val itemsState: OrganizeTokensListState,
val tokenListUM: OrganizeTokensListUM,
val header: HeaderConfig,
val actions: ActionsConfig,

View file

@ -0,0 +1,35 @@
package com.tangem.feature.wallet.child.organizetokens.model
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrencies
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM
internal class CryptoCurrenciesIdsResolver {
fun resolve(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies {
if (accountStatusList == null) return emptyMap()
val draggableTokens = when (tokensListUM) {
OrganizeTokensListUM.EmptyList -> return emptyMap()
is OrganizeTokensListUM.AccountList,
is OrganizeTokensListUM.TokensList,
-> tokensListUM.items.filterIsInstance<DraggableItem.Token>()
}
return accountStatusList.accountStatuses
.filterCryptoPortfolio()
.filter { it.tokenList != TokenList.Empty }
.associate { accountStatus ->
val currenciesById = accountStatus.flattenCurrencies().associateBy { it.currency.id.value }
accountStatus.account to draggableTokens
.asSequence()
.filter { it.accountId == accountStatus.account.accountId.value }
.mapNotNull { token -> currenciesById[token.id]?.currency }
.toList()
}
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.feature.wallet.presentation.organizetokens
package com.tangem.feature.wallet.child.organizetokens.model
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import org.burnoutcrew.reorderable.ItemPosition
internal interface OrganizeTokensIntents {

View file

@ -7,35 +7,21 @@ 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.status.usecase.ApplyTokenListSortingUseCaseV2
import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCaseV2
import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCaseV2
import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCase
import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCase
import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase
import com.tangem.domain.tokens.ToggleTokenListSortingUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensIntents
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder
import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
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.feature.wallet.child.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState
import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapter
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -49,18 +35,13 @@ internal class OrganizeTokensModel @Inject constructor(
paramsContainer: ParamsContainer,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val getTokenListUseCase: GetTokenListUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val toggleTokenListGroupingUseCase: ToggleTokenListGroupingUseCase,
private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val toggleTokenListGroupingUseCaseV2: ToggleTokenListGroupingUseCaseV2,
private val toggleTokenListSortingUseCaseV2: ToggleTokenListSortingUseCaseV2,
private val applyTokenListSortingUseCaseV2: ApplyTokenListSortingUseCaseV2,
) : Model(), OrganizeTokensIntents {
private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow()
@ -68,24 +49,17 @@ internal class OrganizeTokensModel @Inject constructor(
private var isBalanceHidden = true
private val dragAndDropAdapter = DragAndDropAdapter(
listStateProvider = Provider { uiState.value.itemsState },
)
private val dragAndDropAdapterV2 = DragAndDropAdapterV2(
tokenListUMProvider = Provider { uiState.value.tokenListUM },
)
private val stateHolder = OrganizeTokensStateHolder(
intents = this,
dragAndDropIntents = dragAndDropAdapter,
dragAndDropAdapterV2 = dragAndDropAdapterV2,
dragAndDropAdapter = dragAndDropAdapter,
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
accountsFeatureToggles = accountsFeatureToggles,
)
private val userWalletId = paramsContainer.require<OrganizeTokensComponent.Params>().userWalletId
private var cachedTokenList: TokenList? = null
private var cachedAccountStatusList: AccountStatusList? = null
private var isAccountsModeEnabled: Boolean = false
@ -113,68 +87,35 @@ internal class OrganizeTokensModel @Inject constructor(
}
override fun onSortClick() {
if (accountsFeatureToggles.isFeatureEnabled) {
val list = cachedAccountStatusList ?: return
if (list.sortType == TokensSortType.BALANCE) return
val list = cachedAccountStatusList ?: return
if (list.sortType == TokensSortType.BALANCE) return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance())
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance())
modelScope.launch {
toggleTokenListSortingUseCaseV2(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled)
cachedAccountStatusList = it
},
)
}
} 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
},
)
}
modelScope.launch {
toggleTokenListSortingUseCase(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = { accountStatusList ->
stateHolder.updateStateAfterTokenListSorting(accountStatusList, isAccountsModeEnabled)
cachedAccountStatusList = accountStatusList
},
)
}
}
override fun onGroupClick() {
if (accountsFeatureToggles.isFeatureEnabled) {
val list = cachedAccountStatusList ?: return
val list = cachedAccountStatusList ?: return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group())
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group())
modelScope.launch {
toggleTokenListGroupingUseCaseV2(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled)
cachedAccountStatusList = it
},
)
}
} else {
val list = cachedTokenList ?: return
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 = { accountStatusList ->
stateHolder.updateStateAfterTokenListSorting(accountStatusList, isAccountsModeEnabled)
cachedAccountStatusList = accountStatusList
},
)
}
}
@ -183,39 +124,20 @@ internal class OrganizeTokensModel @Inject constructor(
stateHolder.updateStateToDisplayProgress()
val resolver = CryptoCurrenciesIdsResolver()
val isSortedByBalance = uiState.value.header.isSortedByBalance
val tokensListUM = uiState.value.tokenListUM
val result = if (accountsFeatureToggles.isFeatureEnabled) {
val tokensListUM = uiState.value.tokenListUM
val isGroupedByNetwork = tokensListUM.isGrouped
val isGroupedByNetwork = tokensListUM.isGrouped
sendAnalyticsEvent(
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
sendAnalyticsEvent(
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
applyTokenListSortingUseCaseV2(
sortedTokensIdsByAccount = resolver.resolveV2(tokensListUM, cachedAccountStatusList),
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
} 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,
)
}
val result = applyTokenListSortingUseCase(
sortedTokensIdsByAccount = resolver.resolve(tokensListUM, cachedAccountStatusList),
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
result.fold(
ifLeft = stateHolder::updateStateWithError,
@ -235,72 +157,35 @@ internal class OrganizeTokensModel @Inject constructor(
private fun bootstrapTokenList() {
modelScope.launch {
if (accountsFeatureToggles.isFeatureEnabled) {
val accountList = singleAccountStatusListSupplier.getSyncOrNull(
SingleAccountStatusListProducer.Params(userWalletId),
) ?: return@launch
val accountList = singleAccountStatusListSupplier.getSyncOrNull(
SingleAccountStatusListProducer.Params(userWalletId),
) ?: return@launch
isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync()
isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync()
stateHolder.updateStateWithAccountList(
accountStatusList = accountList,
isAccountsModeEnabled = isAccountsModeEnabled,
)
stateHolder.updateStateWithAccountList(
accountStatusList = accountList,
isAccountsModeEnabled = isAccountsModeEnabled,
)
cachedAccountStatusList = accountList
} else {
val tokenList = getTokenList() ?: return@launch
stateHolder.updateStateWithTokenList(tokenList)
cachedTokenList = tokenList
}
cachedAccountStatusList = accountList
}
}
private suspend fun getTokenList(): TokenList? {
val maybeTokenList = getTokenListUseCase.launch(userWalletId)
.filterNot(Lce<TokenListError, TokenList>::isLoading)
.firstOrNull()
?: return null
return maybeTokenList
.onError(stateHolder::updateStateWithError)
.getOrNull(isPartialContentAccepted = false)
}
private fun bootstrapDragAndDropUpdates() {
if (accountsFeatureToggles.isFeatureEnabled) {
dragAndDropAdapterV2.dragAndDropUpdates
.distinctUntilChanged()
.onEach { (type, updatedListState) ->
disableSortingByBalanceIfListChangedV2(type)
dragAndDropAdapter.dragAndDropUpdates
.distinctUntilChanged()
.onEach { (type, updatedListState) ->
disableSortingByBalanceIfListChanged(type)
stateHolder.updateStateWithManualSortingV2(updatedListState)
}
.launchIn(modelScope)
} else {
dragAndDropAdapter.dragAndDropUpdates
.distinctUntilChanged()
.onEach { (type, updatedListState) ->
disableSortingByBalanceIfListChanged(type)
stateHolder.updateStateWithManualSorting(updatedListState)
}
.launchIn(modelScope)
}
stateHolder.updateStateWithManualSorting(updatedListState)
}
.launchIn(modelScope)
}
private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) {
if (dragOperationType !is DragAndDropAdapter.DragOperation.Type.End) return
if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) {
cachedTokenList = cachedTokenList?.disableSortingByBalance()
stateHolder.disableSortingByBalance()
}
}
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()

View file

@ -0,0 +1,111 @@
package com.tangem.feature.wallet.child.organizetokens.model
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState
import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter
import com.tangem.feature.wallet.child.organizetokens.model.converter.TokenListToStateConverter
import com.tangem.feature.wallet.child.organizetokens.model.converter.error.TokenListSortingErrorConverter
import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapter
import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
internal class OrganizeTokensStateHolder(
private val intents: OrganizeTokensIntents,
private val dragAndDropAdapter: DragAndDropAdapter,
private val appCurrencyProvider: Provider<AppCurrency>,
) {
private val stateFlowInternal: MutableStateFlow<OrganizeTokensState> = MutableStateFlow(getInitialState())
private val inProgressStateConverter by lazy { InProgressStateConverter() }
private val tokenListSortingErrorConverter by lazy {
TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter)
}
val stateFlow: StateFlow<OrganizeTokensState> = stateFlowInternal
fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) {
updateState {
TokenListToStateConverter(
accountStatusList = accountStatusList,
isAccountsMode = isAccountsModeEnabled,
appCurrency = appCurrencyProvider(),
).transform(this)
}
}
fun updateStateAfterTokenListSorting(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) {
updateState {
TokenListToStateConverter(
accountStatusList = accountStatusList,
isAccountsMode = isAccountsModeEnabled,
appCurrency = appCurrencyProvider(),
).transform(this).copy(
scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent),
)
}
}
fun updateStateToDisplayProgress() {
updateState { inProgressStateConverter.convert(value = this) }
}
fun updateStateToHideProgress() {
updateState { inProgressStateConverter.convertBack(value = this) }
}
fun updateStateWithManualSorting(tokenListUM: OrganizeTokensListUM) {
updateState { copy(tokenListUM = tokenListUM) }
}
fun disableSortingByBalance() {
updateState { copy(header = header.copy(isSortedByBalance = false)) }
}
fun updateHiddenState(isBalanceHidden: Boolean) {
updateState { copy(isBalanceHidden = isBalanceHidden) }
}
fun updateStateWithError(error: TokenListSortingError) {
updateState { tokenListSortingErrorConverter.convert(error) }
}
private fun getInitialState(): OrganizeTokensState {
return OrganizeTokensState(
onBackClick = intents::onBackClick,
tokenListUM = OrganizeTokensListUM.EmptyList,
header = OrganizeTokensState.HeaderConfig(
onSortClick = intents::onSortClick,
onGroupClick = intents::onGroupClick,
),
actions = OrganizeTokensState.ActionsConfig(
onApplyClick = intents::onApplyClick,
onCancelClick = intents::onCancelClick,
),
dndConfig = OrganizeTokensState.DragAndDropConfig(
onItemDragged = dragAndDropAdapter::onItemDragged,
onItemDragStart = dragAndDropAdapter::onItemDraggingStart,
onItemDragEnd = dragAndDropAdapter::onItemDraggingEnd,
canDragItemOver = dragAndDropAdapter::canDragItemOver,
),
scrollListToTop = consumedEvent(),
isBalanceHidden = true,
)
}
private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) {
stateFlowInternal.update(block)
}
private fun consumeScrollListToTopEvent() {
updateState { copy(scrollListToTop = consumedEvent()) }
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
package com.tangem.feature.wallet.child.organizetokens.model.common
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
internal fun getGroupPlaceholder(index: Int, accountId: String = ""): DraggableItem.Placeholder {
return DraggableItem.Placeholder(

View file

@ -1,36 +1,8 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
package com.tangem.feature.wallet.child.organizetokens.model.common
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
val items = prepareItems()
val lastItemIndex = items.lastIndex
return prepareItems().mapIndexed { index, item ->
val mode = when (index) {
// 1 index is used because the first item is always a placeholder, check `prepareItems()` function
1 -> DraggableItem.RoundingMode.Top()
lastItemIndex -> DraggableItem.RoundingMode.Bottom()
else -> when (item) {
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)
} else {
DraggableItem.RoundingMode.None
}
}
}
item
.updateRoundingMode(mode)
.updateShadowVisibility(show = false)
}
}
internal fun List<DraggableItem>.uniteItemsV2(isAccountsMode: Boolean): List<DraggableItem> {
internal fun List<DraggableItem>.uniteItems(isAccountsMode: Boolean): List<DraggableItem> {
val items = this
val lastItemIndex = items.lastIndex
@ -92,27 +64,6 @@ internal fun List<DraggableItem>.divideMovingItem(movingItem: DraggableItem): Li
return mutableList
}
/**
* !!! Workaround !!!
*
* We need to add a [DraggableItem.Placeholder] (since it's not draggable) as the first item of the list, because the
* [DND library](https://github.com/aclassen/ComposeReorderable) glitches when a user tries to drag the first item.
*
* @since 07.09.2023
* */
private fun List<DraggableItem>.prepareItems(): List<DraggableItem> {
val firstPlaceholderId = "initial_placeholder"
val items = this
return mutableListOf<DraggableItem>().apply {
add(DraggableItem.Placeholder(firstPlaceholderId))
val itemsWithoutFirstPlaceholder = items.filterNot { it.id == firstPlaceholderId }
addAll(itemsWithoutFirstPlaceholder)
}
}
/**
* Applying rounding to tokens
*

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
package com.tangem.feature.wallet.child.organizetokens.model.common
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network

View file

@ -0,0 +1,18 @@
package com.tangem.feature.wallet.child.organizetokens.model.common
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
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

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
package com.tangem.feature.wallet.child.organizetokens.model.common
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.tokenlist.TokenList

View file

@ -1,6 +1,6 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter
package com.tangem.feature.wallet.child.organizetokens.model.converter
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState
import com.tangem.utils.converter.TwoWayConverter
internal class InProgressStateConverter : TwoWayConverter<OrganizeTokensState, OrganizeTokensState> {

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter
package com.tangem.feature.wallet.child.organizetokens.model.converter
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.domain.account.models.AccountStatusList
@ -8,17 +8,17 @@ import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.filterCryptoPortfolio
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.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState
import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder
import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems
import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizedTokenListConverter
import com.tangem.utils.converter.Converter
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class TokenListToStateConverterV2(
internal class TokenListToStateConverter(
private val accountStatusList: AccountStatusList,
private val isAccountsMode: Boolean,
private val appCurrency: AppCurrency,
@ -82,7 +82,7 @@ internal class AccountTokenItemConverter(
emptyList()
}
}.toList()
.uniteItemsV2(true).toPersistentList(),
.uniteItems(true).toPersistentList(),
)
} else {
OrganizeTokensListUM.TokensList(
@ -92,7 +92,7 @@ internal class AccountTokenItemConverter(
add(getGroupPlaceholder(accountId = value.mainAccount.accountId.value, index = -1))
}
addAll(organizedTokenListConverter.convert(value.mainAccount))
}.uniteItemsV2(false)
}.uniteItems(false)
.toPersistentList(),
)
}

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error
package com.tangem.feature.wallet.child.organizetokens.model.converter.error
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState
import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error
package com.tangem.feature.wallet.child.organizetokens.model.converter.error
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState
import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
package com.tangem.feature.wallet.child.organizetokens.model.converter.items
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
@ -11,14 +11,14 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.common.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.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupHeaderId
import com.tangem.feature.wallet.child.organizetokens.model.common.getTokenItemId
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class CryptoCurrencyToDraggableItemConverterV2(
internal class CryptoCurrencyToDraggableItemConverter(
private val appCurrency: AppCurrency,
) : Converter<AccountCryptoCurrencyStatus, DraggableItem.Token> {

View file

@ -1,16 +1,16 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
package com.tangem.feature.wallet.child.organizetokens.model.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.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupHeaderId
import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder
import com.tangem.utils.converter.Converter
internal class NetworkGroupToDraggableItemsConverterV2(
private val itemConverter: CryptoCurrencyToDraggableItemConverterV2,
internal class NetworkGroupToDraggableItemsConverter(
private val itemConverter: CryptoCurrencyToDraggableItemConverter,
) : Converter<Pair<Account.CryptoPortfolio, NetworkGroup>, List<DraggableItem>> {
override fun convert(value: Pair<Account.CryptoPortfolio, NetworkGroup>): List<DraggableItem> {
@ -42,10 +42,10 @@ internal class NetworkGroupToDraggableItemsConverterV2(
private fun createTokens(account: Account.CryptoPortfolio, group: NetworkGroup): List<DraggableItem.Token> {
return itemConverter.convertList(
group.currencies.map {
group.currencies.map { currencyStatus ->
AccountCryptoCurrencyStatus(
account = account,
status = it,
status = currencyStatus,
)
},
)

View file

@ -1,10 +1,10 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
package com.tangem.feature.wallet.child.organizetokens.model.converter.items
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -14,9 +14,9 @@ internal class OrganizedTokenListConverter(
private val appCurrency: AppCurrency,
) : Converter<AccountStatus.CryptoPortfolio, PersistentList<DraggableItem>> {
private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) }
private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverter(appCurrency) }
private val groupsConverter by lazy {
NetworkGroupToDraggableItemsConverterV2(tokensConverter)
NetworkGroupToDraggableItemsConverter(tokensConverter)
}
override fun convert(value: AccountStatus.CryptoPortfolio): PersistentList<DraggableItem> {

View file

@ -1,11 +1,11 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd
package com.tangem.feature.wallet.child.organizetokens.model.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.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM
import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents
import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem
import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems
import com.tangem.feature.wallet.child.organizetokens.model.common.updateItems
import com.tangem.utils.Provider
import kotlinx.collections.immutable.mutate
import kotlinx.coroutines.flow.Flow
@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import org.burnoutcrew.reorderable.ItemPosition
internal class DragAndDropAdapterV2(
internal class DragAndDropAdapter(
private val tokenListUMProvider: Provider<OrganizeTokensListUM>,
) : DragAndDropIntents {
@ -81,7 +81,7 @@ internal class DragAndDropAdapterV2(
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,
-> items
is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroupV2(items, item)
is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item)
.divideMovingItem(item)
is DraggableItem.Token -> items.divideMovingItem(item)
}
@ -96,11 +96,11 @@ internal class DragAndDropAdapterV2(
updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) {
when (draggingItem) {
is DraggableItem.GroupHeader -> {
draggableGroupsOperations.expandGroupsV2(items)
.uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList)
draggableGroupsOperations.expandGroups(items)
.uniteItems(tokenListUM is OrganizeTokensListUM.AccountList)
}
is DraggableItem.Token -> {
items.uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList)
items.uniteItems(tokenListUM is OrganizeTokensListUM.AccountList)
}
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,

View file

@ -1,9 +1,8 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd
package com.tangem.feature.wallet.child.organizetokens.model.dnd
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem
import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder
internal class DraggableGroupsOperations {
@ -24,47 +23,9 @@ 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
val currentGroups = items.filterIsInstance<DraggableItem.GroupHeader>()
val lastGroupIndex = currentGroups.lastIndex
val expandedGroups = currentGroups
.flatMapIndexed { index, group ->
buildList {
add(group)
addAll(groupIdToTokens?.get(group.id).orEmpty())
if (index != lastGroupIndex) {
add(getGroupPlaceholder(index))
}
}
}
.uniteItems()
groupIdToTokens = null
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>()

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens
package com.tangem.feature.wallet.child.organizetokens.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
@ -44,12 +44,11 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState
import com.tangem.feature.wallet.child.organizetokens.ui.preview.OrganizeTokensPreview
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
import org.burnoutcrew.reorderable.reorderable
@ -76,7 +75,6 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier
.fillMaxSize(),
listState = tokensListState,
tokensListUM = state.tokenListUM,
state = state.itemsState,
dndConfig = state.dndConfig,
isBalanceHidden = state.isBalanceHidden,
)
@ -98,18 +96,13 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier
@Composable
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
}
val tokenList = tokensListUM.items
Box(modifier = modifier) {
val onDragEnd: (Int, Int) -> Unit = remember {
{ _, _ ->
@ -423,8 +416,8 @@ private fun OrganizeTokensScreenPreview(
private class OrganizeTokensStateProvider : CollectionPreviewParameterProvider<OrganizeTokensState>(
collection = listOf(
WalletPreviewData.organizeTokensState,
WalletPreviewData.groupedOrganizeTokensState,
OrganizeTokensPreview.stateAccounts,
OrganizeTokensPreview.state,
),
)
// endregion Preview

View file

@ -0,0 +1,127 @@
package com.tangem.feature.wallet.child.organizetokens.ui.preview
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState
import com.tangem.feature.wallet.impl.R
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import java.util.UUID
internal object OrganizeTokensPreview {
private const val networksSize = 10
private const val tokensSize = 3
private val tokenItemDragState by lazy {
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
iconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"),
)
}
private val draggableItems: PersistentList<DraggableItem> by lazy {
List(networksSize) { it }
.flatMap { index ->
val lastNetworkIndex = networksSize - 1
val lastTokenIndex = tokensSize - 1
val networkNumber = index + 1
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()
repeat(times = tokensSize) { i ->
val tokenNumber = i + 1
tokens.add(
DraggableItem.Token(
tokenItemState = tokenItemDragState.copy(
id = "${group.id}_token_$tokenNumber",
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Token $tokenNumber from $networkNumber network"),
),
),
groupId = group.id,
accountId = "account_$networkNumber",
roundingMode = when {
i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom()
else -> DraggableItem.RoundingMode.None
},
),
)
}
val divider = DraggableItem.Placeholder(
id = "divider_$networkNumber",
accountId = "account_$networkNumber",
)
buildList {
add(group)
addAll(tokens)
if (index != lastNetworkIndex) {
add(divider)
}
}
}
.toPersistentList()
}
val stateAccounts by lazy {
OrganizeTokensState(
onBackClick = {},
tokenListUM = OrganizeTokensListUM.AccountList(
items = draggableItems,
isGrouped = true,
),
header = OrganizeTokensState.HeaderConfig(
onSortClick = {},
onGroupClick = {},
),
dndConfig = OrganizeTokensState.DragAndDropConfig(
onItemDragged = { _, _ -> },
onItemDragStart = {},
canDragItemOver = { _, _ -> false },
onItemDragEnd = {},
),
actions = OrganizeTokensState.ActionsConfig(
onApplyClick = {},
onCancelClick = {},
),
scrollListToTop = consumedEvent(),
isBalanceHidden = true,
)
}
val state by lazy {
stateAccounts.copy(
tokenListUM = OrganizeTokensListUM.TokensList(
items = draggableItems,
isGrouped = true,
),
)
}
}

View file

@ -1,23 +1,11 @@
package com.tangem.feature.wallet.presentation.common
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.event.consumedEvent
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.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
import java.util.UUID
@Suppress("LargeClass")
internal object WalletPreviewData {
@ -67,124 +55,6 @@ internal object WalletPreviewData {
)
}
private val tokenItemDragState by lazy {
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
iconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"),
)
}
private const val networksSize = 10
private const val tokensSize = 3
private val draggableItems: PersistentList<DraggableItem> by lazy {
List(networksSize) { it }
.flatMap { index ->
val lastNetworkIndex = networksSize - 1
val lastTokenIndex = tokensSize - 1
val networkNumber = index + 1
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()
repeat(times = tokensSize) { i ->
val tokenNumber = i + 1
tokens.add(
DraggableItem.Token(
tokenItemState = tokenItemDragState.copy(
id = "${group.id}_token_$tokenNumber",
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Token $tokenNumber from $networkNumber network"),
),
),
groupId = group.id,
accountId = "account_$networkNumber",
roundingMode = when {
i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom()
else -> DraggableItem.RoundingMode.None
},
),
)
}
val divider = DraggableItem.Placeholder(
id = "divider_$networkNumber",
accountId = "account_$networkNumber",
)
buildList {
add(group)
addAll(tokens)
if (index != lastNetworkIndex) {
add(divider)
}
}
}
.toPersistentList()
}
private val draggableTokens by lazy {
draggableItems
.filterIsInstance<DraggableItem.Token>()
.toMutableList()
.also {
it[0] = it[0].copy(roundingMode = DraggableItem.RoundingMode.Top())
}
.toPersistentList()
}
val groupedOrganizeTokensState by lazy {
OrganizeTokensState(
onBackClick = {},
itemsState = OrganizeTokensListState.GroupedByNetwork(
items = draggableItems,
),
tokenListUM = OrganizeTokensListUM.EmptyList,
header = OrganizeTokensState.HeaderConfig(
onSortClick = {},
onGroupClick = {},
),
dndConfig = OrganizeTokensState.DragAndDropConfig(
onItemDragged = { _, _ -> },
onItemDragStart = {},
canDragItemOver = { _, _ -> false },
onItemDragEnd = {},
),
actions = OrganizeTokensState.ActionsConfig(
onApplyClick = {},
onCancelClick = {},
),
scrollListToTop = consumedEvent(),
isBalanceHidden = true,
)
}
val organizeTokensState by lazy {
groupedOrganizeTokensState.copy(
itemsState = OrganizeTokensListState.Ungrouped(
items = draggableTokens,
),
)
}
val actionsBottomSheet = ActionsBottomSheetConfig(
actions = listOf(
TokenActionButtonConfig(

View file

@ -1,166 +0,0 @@
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
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())
private val tokenListConverter by lazy {
val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider)
val itemsConverter = TokenListToListStateConverter(
tokensConverter = tokensConverter,
groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter),
)
TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter)
}
private val inProgressStateConverter by lazy { InProgressStateConverter() }
private val tokenListErrorConverter by lazy {
TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter)
}
private val tokenListSortingErrorConverter by lazy {
TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter)
}
val stateFlow: StateFlow<OrganizeTokensState> = stateFlowInternal
fun updateStateWithTokenList(tokenList: TokenList) {
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(
scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent),
)
}
}
fun updateStateAfterTokenListSortingV2(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) {
updateState {
TokenListToStateConverterV2(
accountStatusList = accountStatusList,
isAccountsMode = isAccountsModeEnabled,
appCurrency = appCurrencyProvider(),
).transform(this).copy(
scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent),
)
}
}
fun updateStateToDisplayProgress() {
updateState { inProgressStateConverter.convert(value = this) }
}
fun updateStateToHideProgress() {
updateState { inProgressStateConverter.convertBack(value = this) }
}
fun updateStateWithManualSortingV2(tokenListUM: OrganizeTokensListUM) {
updateState { copy(tokenListUM = tokenListUM) }
}
fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) {
updateState { copy(itemsState = itemsState) }
}
fun disableSortingByBalance() {
updateState { copy(header = header.copy(isSortedByBalance = false)) }
}
fun updateHiddenState(isBalanceHidden: Boolean) {
updateState { copy(isBalanceHidden = isBalanceHidden) }
}
fun updateStateWithError(error: TokenListError) {
updateState { tokenListErrorConverter.convert(error) }
}
fun updateStateWithError(error: TokenListSortingError) {
updateState { tokenListSortingErrorConverter.convert(error) }
}
private fun getInitialState(): OrganizeTokensState {
return OrganizeTokensState(
onBackClick = intents::onBackClick,
itemsState = OrganizeTokensListState.Empty,
tokenListUM = OrganizeTokensListUM.EmptyList,
header = OrganizeTokensState.HeaderConfig(
onSortClick = intents::onSortClick,
onGroupClick = intents::onGroupClick,
),
actions = OrganizeTokensState.ActionsConfig(
onApplyClick = intents::onApplyClick,
onCancelClick = intents::onCancelClick,
),
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,
)
}
private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) {
stateFlowInternal.update(block)
}
private fun consumeScrollListToTopEvent() {
updateState { copy(scrollListToTop = consumedEvent()) }
}
}

View file

@ -1,60 +0,0 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrencies
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.tokenlist.TokenList
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
internal class CryptoCurrenciesIdsResolver {
fun resolve(listState: OrganizeTokensListState, tokenList: TokenList?): List<CryptoCurrency.ID> {
val draggableTokens = when (listState) {
is OrganizeTokensListState.Empty -> return emptyList()
is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance<DraggableItem.Token>()
is OrganizeTokensListState.Ungrouped -> listState.items.filterIsInstance<DraggableItem.Token>()
}
val currenciesStatuses = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies }
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty,
null,
-> return emptyList()
}
return draggableTokens.mapNotNull { draggableToken ->
val currencyStatus = currenciesStatuses.firstOrNull {
it.currency.id.value == draggableToken.id
}
currencyStatus?.currency?.id
}
}
@Suppress("UseOrEmpty")
fun resolveV2(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies {
val draggableTokens = when (tokensListUM) {
OrganizeTokensListUM.EmptyList -> return emptyMap()
is OrganizeTokensListUM.AccountList,
is OrganizeTokensListUM.TokensList,
-> tokensListUM.items.filterIsInstance<DraggableItem.Token>()
}
return accountStatusList?.accountStatuses
?.filterCryptoPortfolio()
?.filter { it.tokenList != TokenList.Empty }
?.associate { accountStatus ->
val currencies = accountStatus.flattenCurrencies()
accountStatus.account to draggableTokens
.asSequence()
.filter { it.accountId == accountStatus.account.accountId.value }
.mapNotNull { sortedToken ->
currencies.firstOrNull { it.currency.id.value == sortedToken.id }?.currency
}
.toList()
} ?: emptyMap()
}
}

View file

@ -1,31 +0,0 @@
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
internal inline fun OrganizeTokensListState.updateItems(
update: (PersistentList<DraggableItem>) -> List<DraggableItem>,
): OrganizeTokensListState {
val updatedItems = update(items).toPersistentList()
return when (this) {
is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems)
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

@ -1,32 +0,0 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
internal class TokenListToStateConverter(
private val currentState: Provider<OrganizeTokensState>,
private val itemsConverter: TokenListToListStateConverter,
) : Converter<TokenList, OrganizeTokensState> {
override fun convert(value: TokenList): OrganizeTokensState {
val state = currentState()
val itemsState = itemsConverter.convert(value)
return state.copy(
itemsState = itemsState,
header = state.header.copy(
isEnabled = itemsState !is OrganizeTokensListState.Empty,
isSortedByBalance = value.sortedBy == TokensSortType.BALANCE,
isGrouped = value is TokenList.GroupedByNetwork,
),
actions = state.actions.copy(
canApply = itemsState !is OrganizeTokensListState.Empty,
),
)
}
}

View file

@ -1,74 +0,0 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.common.getTotalWithRewardsStakingBalance
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.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.StakingBalance
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.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class CryptoCurrencyToDraggableItemConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<CryptoCurrencyStatus, DraggableItem.Token> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token {
return createDraggableToken(value, appCurrencyProvider())
}
override fun convertList(input: Collection<CryptoCurrencyStatus>): List<DraggableItem.Token> {
val appCurrency = appCurrencyProvider()
return input.map { createDraggableToken(it, appCurrency) }
}
private fun createDraggableToken(
currencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency,
): DraggableItem.Token {
return DraggableItem.Token(
tokenItemState = createTokenItemState(currencyStatus, appCurrency),
groupId = getGroupHeaderId(currencyStatus.currency.network),
)
}
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 stakingBalance = currency.value.stakingBalance as? StakingBalance.Data
val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO
val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId)
?.multiply(fiatRate).orZero()
val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) }
}
}

View file

@ -1,41 +0,0 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
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 NetworkGroupToDraggableItemsConverter(
private val itemConverter: CryptoCurrencyToDraggableItemConverter,
) : Converter<NetworkGroup, List<DraggableItem>> {
override fun convert(value: NetworkGroup): List<DraggableItem> {
return buildList {
add(createGroupHeader(value))
addAll(createTokens(value))
}
}
override fun convertList(input: Collection<NetworkGroup>): List<List<DraggableItem>> {
val lastItemIndex = input.size - 1
return input.mapIndexed { index, networkGroup ->
convert(networkGroup).toMutableList()
.also { mutableGroup ->
if (index != lastItemIndex) {
mutableGroup.add(getGroupPlaceholder(index))
}
}
}
}
private fun createGroupHeader(group: NetworkGroup) = DraggableItem.GroupHeader(
id = getGroupHeaderId(group.network),
networkName = group.network.name,
)
private fun createTokens(group: NetworkGroup): List<DraggableItem.Token> {
return itemConverter.convertList(group.currencies)
}
}

View file

@ -1,45 +0,0 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.domain.models.tokenlist.TokenList
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.utils.common.uniteItems
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
internal class TokenListToListStateConverter(
private val groupsConverter: NetworkGroupToDraggableItemsConverter,
private val tokensConverter: CryptoCurrencyToDraggableItemConverter,
) : Converter<TokenList, OrganizeTokensListState> {
override fun convert(value: TokenList): OrganizeTokensListState {
return when (value) {
is TokenList.GroupedByNetwork -> createListState(value)
is TokenList.Ungrouped -> createListState(value)
is TokenList.Empty -> createEmptyListState()
}
}
private fun createListState(tokenList: TokenList.GroupedByNetwork): OrganizeTokensListState.GroupedByNetwork {
return OrganizeTokensListState.GroupedByNetwork(
items = groupsConverter.convertList(tokenList.groups)
.flatten()
.uniteItems()
.toPersistentList(),
)
}
@Suppress("UNCHECKED_CAST") // Erased type
private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped {
return OrganizeTokensListState.Ungrouped(
items = tokensConverter.convertList(tokenList.currencies)
.uniteItems()
.toPersistentList() as PersistentList<DraggableItem.Token>,
)
}
private fun createEmptyListState(): OrganizeTokensListState.Empty {
return OrganizeTokensListState.Empty
}
}

View file

@ -1,185 +0,0 @@
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.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems
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 DragAndDropAdapter(
private val listStateProvider: Provider<OrganizeTokensListState>,
) : DragAndDropIntents {
private val draggableGroupsOperations = DraggableGroupsOperations()
private val externalListState: OrganizeTokensListState
get() = listStateProvider.invoke()
private val dragAndDropUpdatesInternal: MutableStateFlow<DragOperation?> = MutableStateFlow(value = null)
private var draggingItem: DraggableItem? = null
private var draggingListState: OrganizeTokensListState? = null
val dragAndDropUpdates: Flow<DragOperation>
get() = dragAndDropUpdatesInternal.filterNotNull()
override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean {
val items = when (val listState = externalListState) {
is OrganizeTokensListState.GroupedByNetwork -> listState.items
is OrganizeTokensListState.Empty,
is OrganizeTokensListState.Ungrouped,
-> return true // If ungrouped then item can be moved anywhere
}
val (dragOverItem, draggingItem) = findItemsToMove(
items = items,
moveOverItemKey = dragOver.key,
movedItemKey = dragging.key,
)
if (dragOverItem == null || draggingItem == null) {
return false
}
return when (draggingItem) {
is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex)
is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem)
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,
-> false
}
}
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.collapseGroup(items, item)
is DraggableItem.Token -> when (this) {
is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item)
is OrganizeTokensListState.Ungrouped -> items.divideMovingItem(item)
is OrganizeTokensListState.Empty -> items
}
}
}
draggingListState = externalListState
}
override fun onItemDraggingEnd() {
val draggingItem = draggingItem ?: return
updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) {
when (draggingItem) {
is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items)
is DraggableItem.Token -> items.uniteItems()
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: OrganizeTokensListState.() -> List<DraggableItem>) {
val updatedState = externalListState.updateItems { block(externalListState) }
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(
moveOverItemPosition: ItemPosition,
moveOverItem: DraggableItem,
lastItemIndex: Int,
): Boolean {
// Group item can be moved only to group divider or to ages of the items list
return when {
moveOverItemPosition.index == 0 -> true
moveOverItemPosition.index == lastItemIndex -> true
moveOverItem is DraggableItem.Placeholder -> true
else -> false
}
}
private fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean {
// Token item can be moved only in its group
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.Portfolio,
is DraggableItem.Placeholder,
-> false
}
}
private fun checkIsItemsOrderChanged(): Boolean {
fun OrganizeTokensListState?.getItemsIds(): List<Any>? = this?.items?.mapNotNull { item ->
if (item is DraggableItem.Placeholder) {
null
} else {
item.id
}
}
return externalListState.getItemsIds() != draggingListState.getItemsIds()
}
data class DragOperation(
val type: Type,
val listState: OrganizeTokensListState,
) {
sealed class Type {
data object Start : Type()
data object Dragged : Type()
data class End(val isItemsOrderChanged: Boolean) : Type()
}
}
}