Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-28 10:50:24 +03:00
parent 31e4cafad0
commit 36b4671a05
18 changed files with 629 additions and 203 deletions

View file

@ -11,7 +11,7 @@ import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensState
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
@ -205,21 +205,21 @@ internal object WalletPreviewData {
}
val groupedOrganizeTokensState by lazy {
OrganizeTokensStateHolder(
OrganizeTokensState(
itemsState = OrganizeTokensListState.GroupedByNetwork(
items = draggableItems,
),
header = OrganizeTokensStateHolder.HeaderConfig(
header = OrganizeTokensState.HeaderConfig(
onSortByBalanceClick = {},
onGroupByNetworkClick = {},
),
dragConfig = OrganizeTokensStateHolder.DragConfig(
dndConfig = OrganizeTokensState.DragAndDropConfig(
onItemDragged = { _, _ -> },
onDragStart = {},
canDragItemOver = { _, _ -> false },
onItemDragEnd = {},
),
actions = OrganizeTokensStateHolder.ActionsConfig(
actions = OrganizeTokensState.ActionsConfig(
onApplyClick = {},
onCancelClick = {},
),

View file

@ -0,0 +1,24 @@
package com.tangem.feature.wallet.presentation.organizetokens
import org.burnoutcrew.reorderable.ItemPosition
internal interface OrganizeTokensIntents {
fun onBackClick()
fun onSortClick()
fun onGroupClick()
fun onApplyClick()
fun onCancelClick()
fun onItemDragged(from: ItemPosition, to: ItemPosition)
fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean
fun onItemDraggingStart(item: DraggableItem)
fun onItemDraggingEnd()
}

View file

@ -34,7 +34,7 @@ import com.tangem.feature.wallet.presentation.common.component.DraggableTokenIte
import org.burnoutcrew.reorderable.*
@Composable
internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Modifier = Modifier) {
internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier = Modifier) {
val tokensListState = rememberLazyListState()
Scaffold(
@ -47,7 +47,7 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Mo
modifier = Modifier.padding(paddingValues),
listState = tokensListState,
state = state.itemsState,
dragConfig = state.dragConfig,
dragConfig = state.dndConfig,
)
},
floatingActionButtonPosition = FabPosition.Center,
@ -62,7 +62,7 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Mo
private fun TokenList(
listState: LazyListState,
state: OrganizeTokensListState,
dragConfig: OrganizeTokensStateHolder.DragConfig,
dragConfig: OrganizeTokensState.DragAndDropConfig,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
@ -166,7 +166,7 @@ private fun BottomGradient(modifier: Modifier = Modifier) {
@Composable
private fun TopBar(
config: OrganizeTokensStateHolder.HeaderConfig,
config: OrganizeTokensState.HeaderConfig,
tokensListState: LazyListState,
modifier: Modifier = Modifier,
) {
@ -211,7 +211,7 @@ private fun TopBar(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.organize_tokens_sort_by_balance),
iconResId = R.drawable.ic_sort_24,
onClick = config.onSortByBalanceClick,
onClick = config.onSortClick,
),
modifier = Modifier.weight(1f),
color = TangemTheme.colors.background.primary,
@ -220,7 +220,7 @@ private fun TopBar(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.organize_tokens_group),
iconResId = R.drawable.ic_group_24,
onClick = config.onGroupByNetworkClick,
onClick = config.onGroupClick,
),
modifier = Modifier.weight(1f),
color = TangemTheme.colors.background.primary,
@ -230,7 +230,7 @@ private fun TopBar(
}
@Composable
private fun Actions(config: OrganizeTokensStateHolder.ActionsConfig, modifier: Modifier = Modifier) {
private fun Actions(config: OrganizeTokensState.ActionsConfig, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
@ -308,7 +308,7 @@ private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMod
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun OrganizeTokensScreenPreview_Light(
@PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensStateHolder,
@PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensState,
) {
TangemTheme {
OrganizeTokensScreen(state)
@ -318,14 +318,14 @@ private fun OrganizeTokensScreenPreview_Light(
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun OrganizeTokensScreenPreview_Dark(
@PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensStateHolder,
@PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensState,
) {
TangemTheme(isDark = true) {
OrganizeTokensScreen(state)
}
}
private class OrganizeTokensStateProvider : CollectionPreviewParameterProvider<OrganizeTokensStateHolder>(
private class OrganizeTokensStateProvider : CollectionPreviewParameterProvider<OrganizeTokensState>(
collection = listOf(
WalletPreviewData.organizeTokensState,
WalletPreviewData.groupedOrganizeTokensState,

View file

@ -0,0 +1,155 @@
package com.tangem.feature.wallet.presentation.organizetokens
import androidx.compose.runtime.Immutable
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem.RoundingMode
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import org.burnoutcrew.reorderable.ItemPosition
@Immutable
internal data class OrganizeTokensState(
val onBackClick: () -> Unit,
val itemsState: OrganizeTokensListState,
val header: HeaderConfig,
val actions: ActionsConfig,
val dndConfig: DragAndDropConfig,
) {
data class HeaderConfig(
val isEnabled: Boolean = false,
val isSortedByBalance: Boolean = false,
val isGrouped: Boolean = false,
val onSortClick: () -> Unit,
val onGroupClick: () -> Unit,
)
data class ActionsConfig(
val canApply: Boolean = false,
val showApplyProgress: Boolean = false,
val onApplyClick: () -> Unit,
val onCancelClick: () -> Unit,
)
data class DragAndDropConfig(
val onItemDragged: (ItemPosition, ItemPosition) -> Unit,
val canDragItemOver: (ItemPosition, ItemPosition) -> Boolean,
val onItemDragEnd: () -> Unit,
val onDragStart: (DraggableItem) -> Unit,
)
}
@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.Token>,
) : OrganizeTokensListState()
object Empty : OrganizeTokensListState() {
override val items: PersistentList<DraggableItem> = persistentListOf()
}
}
/**
* Helper class for the DND list items
*
* @property id ID of the item
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
* */
@Immutable
internal sealed class DraggableItem {
abstract val id: String
abstract val roundingMode: RoundingMode
abstract val showShadow: Boolean
/**
* Item for network group header.
*
* @property id ID of the network group
* @property networkName network group name
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
* */
data class GroupHeader(
override val id: String,
val networkName: String,
override val roundingMode: RoundingMode = RoundingMode.None,
override val showShadow: Boolean = false,
) : DraggableItem()
/**
* Item for token.
*
* @property tokenItemState state of the token item
* @property groupId ID of the network group which contains this token
* @property id ID of the token
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
* */
data class Token(
val tokenItemState: TokenItemState.Draggable,
val groupId: String,
override val showShadow: Boolean = false,
override val roundingMode: RoundingMode = RoundingMode.None,
) : DraggableItem() {
override val id: String = tokenItemState.id
}
/**
* Helper item used to detect possible positions where a network group can be placed.
* Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups.
*
* @property id ID of the placeholder
* */
data class GroupPlaceholder(
override val id: String,
) : DraggableItem() {
override val showShadow: Boolean = false
override val roundingMode: RoundingMode = RoundingMode.None
}
/**
* Rounding mode of the [DraggableItem]
*
* @property showGap if true then item should have padding on rounded side
* */
@Immutable
sealed class RoundingMode {
abstract val showGap: Boolean
/**
* In this mode, item is not rounded
* */
object None : RoundingMode() {
override val showGap: Boolean = false
}
/**
* In this mode, item should have a rounded top side
*
* @property showGap if true then item should have top padding
* */
data class Top(override val showGap: Boolean = false) : RoundingMode()
/**
* In this mode, item should have a rounded bottom side
*
* @property showGap if true then item should have bottom padding
* */
data class Bottom(override val showGap: Boolean = false) : RoundingMode()
/**
* In this mode, item should have a rounded all sides
*
* @property showGap if true then item should have top and bottom padding
* */
data class All(override val showGap: Boolean = false) : RoundingMode()
}
}

View file

@ -1,179 +1,120 @@
package com.tangem.feature.wallet.presentation.organizetokens
import androidx.compose.runtime.Immutable
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import org.burnoutcrew.reorderable.ItemPosition
import com.tangem.common.Provider
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateSorting
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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
internal data class OrganizeTokensStateHolder(
val header: HeaderConfig,
val itemsState: OrganizeTokensListState,
val dragConfig: DragConfig,
val actions: ActionsConfig,
@Suppress("unused", "MemberVisibilityCanBePrivate") // TODO: Will be used in next MR
internal class OrganizeTokensStateHolder(
private val intents: OrganizeTokensIntents,
private val fiatCurrencyCode: String,
private val fiatCurrencySymbol: String,
private val onSubscription: () -> Unit,
scope: CoroutineScope,
) {
data class HeaderConfig(
val onSortByBalanceClick: () -> Unit,
val onGroupByNetworkClick: () -> Unit,
)
private val stateFlowInternal: MutableStateFlow<OrganizeTokensState> = MutableStateFlow(getInitialState())
data class ActionsConfig(
val onApplyClick: () -> Unit,
val onCancelClick: () -> Unit,
)
private val tokenListConverter by lazy {
val tokensConverter = CryptoCurrencyToDraggableItemConverter(
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
)
val itemsConverter = TokenListToListStateConverter(
tokensConverter = tokensConverter,
groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter),
)
data class DragConfig(
val onItemDragged: (from: ItemPosition, to: ItemPosition) -> Unit,
val canDragItemOver: (dragOver: ItemPosition, dragging: ItemPosition) -> Boolean,
val onItemDragEnd: () -> Unit,
val onDragStart: (item: DraggableItem) -> Unit,
)
}
TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter)
}
@Immutable
internal sealed interface OrganizeTokensListState {
val items: PersistentList<DraggableItem>
private val inProgressStateConverter by lazy {
InProgressStateConverter()
}
data class GroupedByNetwork(
override val items: PersistentList<DraggableItem>,
) : OrganizeTokensListState
private val tokenListErrorConverter by lazy {
TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter)
}
data class Ungrouped(
override val items: PersistentList<DraggableItem.Token>,
) : OrganizeTokensListState
private val tokenListSortingErrorConverter by lazy {
TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter)
}
@Suppress("UNCHECKED_CAST")
fun updateItems(update: (PersistentList<DraggableItem>) -> List<DraggableItem>): OrganizeTokensListState {
val updatedItems = update(this.items).toPersistentList()
val stateFlow: StateFlow<OrganizeTokensState> = stateFlowInternal
.onSubscription { onSubscription() }
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(),
initialValue = getInitialState(),
)
return when (this) {
is GroupedByNetwork -> this.copy(items = updatedItems)
is Ungrouped -> this.copy(items = updatedItems as PersistentList<DraggableItem.Token>)
var tokenList: TokenList? = null
private set
fun updateStateWithTokenList(tokenList: TokenList) {
updateState { tokenListConverter.convert(tokenList) }
this.tokenList = tokenList
}
fun updateStateToDisplayProgress() {
updateState { inProgressStateConverter.convert(value = this) }
}
fun updateStateToHideProgress() {
updateState { inProgressStateConverter.convertBack(value = this) }
}
fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) {
updateState {
copy(
header = header.copy(isSortedByBalance = false),
itemsState = itemsState,
)
}
}
}
/**
* Helper class for the DND list items
*
* @property id ID of the item
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
* */
@Immutable
internal sealed interface DraggableItem {
val id: String
val roundingMode: RoundingMode
val showShadow: Boolean
/**
* Item for network group header.
*
* @property id ID of the network group
* @property networkName network group name
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
* */
data class GroupHeader(
override val id: String,
val networkName: String,
override val roundingMode: RoundingMode = RoundingMode.None,
override val showShadow: Boolean = false,
) : DraggableItem
/**
* Item for token.
*
* @property tokenItemState state of the token item
* @property groupId ID of the network group which contains this token
* @property id ID of the token
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
* */
data class Token(
val tokenItemState: TokenItemState.Draggable,
val groupId: String,
override val showShadow: Boolean = false,
override val roundingMode: RoundingMode = RoundingMode.None,
) : DraggableItem {
override val id: String = tokenItemState.id
tokenList = tokenList?.updateSorting(isSortedByBalance = false)
}
/**
* Helper item used to detect possible positions where a network group can be placed.
* Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups.
*
* @property id ID of the placeholder
* */
data class GroupPlaceholder(
override val id: String,
) : DraggableItem {
override val showShadow: Boolean = false
override val roundingMode: RoundingMode = RoundingMode.None
fun updateStateWithError(error: TokenListError) {
updateState { tokenListErrorConverter.convert(error) }
}
/**
* Update item [RoundingMode]
*
* @param mode new [RoundingMode]
*
* @return updated [DraggableItem]
* */
fun roundingMode(mode: RoundingMode): DraggableItem = when (this) {
is GroupPlaceholder -> this
is GroupHeader -> this.copy(roundingMode = mode)
is Token -> this.copy(roundingMode = mode)
fun updateStateWithError(error: TokenListSortingError) {
updateState { tokenListSortingErrorConverter.convert(error) }
}
/**
* Update item shadow visibility
*
* @param show if true then item should be elevated
*
* @return updated [DraggableItem]
* */
fun showShadow(show: Boolean): DraggableItem = when (this) {
is GroupPlaceholder -> this
is GroupHeader -> this.copy(showShadow = show)
is Token -> this.copy(showShadow = show)
private fun getInitialState(): OrganizeTokensState {
return OrganizeTokensState(
onBackClick = intents::onBackClick,
itemsState = OrganizeTokensListState.Empty,
header = OrganizeTokensState.HeaderConfig(
onSortClick = intents::onSortClick,
onGroupClick = intents::onGroupClick,
),
actions = OrganizeTokensState.ActionsConfig(
onApplyClick = intents::onApplyClick,
onCancelClick = intents::onCancelClick,
),
dndConfig = OrganizeTokensState.DragAndDropConfig(
onItemDragged = intents::onItemDragged,
onDragStart = intents::onItemDraggingStart,
onItemDragEnd = intents::onItemDraggingEnd,
canDragItemOver = intents::canDragItemOver,
),
)
}
/**
* Rounding mode of the [DraggableItem]
*
* @property showGap if true then item should have padding on rounded side
* */
@Immutable
sealed interface RoundingMode {
val showGap: Boolean
/**
* In this mode, item is not rounded
* */
object None : RoundingMode {
override val showGap: Boolean = false
}
/**
* In this mode, item should have a rounded top side
*
* @property showGap if true then item should have top padding
* */
data class Top(override val showGap: Boolean = false) : RoundingMode
/**
* In this mode, item should have a rounded bottom side
*
* @property showGap if true then item should have bottom padding
* */
data class Bottom(override val showGap: Boolean = false) : RoundingMode
/**
* In this mode, item should have a rounded all sides
*
* @property showGap if true then item should have top and bottom padding
* */
data class All(override val showGap: Boolean = false) : RoundingMode
private fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) {
stateFlowInternal.update(block)
}
}

View file

@ -8,9 +8,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.DragConfig
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.HeaderConfig
import com.tangem.feature.wallet.presentation.organizetokens.utils.*
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.*
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.router.WalletRoute
import dagger.hilt.android.lifecycle.HiltViewModel
@ -35,22 +33,22 @@ internal class OrganizeTokensViewModel @Inject constructor(savedStateHandle: Sav
UserWalletId(userWalletIdValue)
}
var uiState: OrganizeTokensStateHolder by mutableStateOf(getInitialState())
var uiState: OrganizeTokensState by mutableStateOf(getInitialState())
private set
private fun getInitialState(): OrganizeTokensStateHolder = WalletPreviewData.organizeTokensState.copy(
private fun getInitialState(): OrganizeTokensState = WalletPreviewData.organizeTokensState.copy(
itemsState = OrganizeTokensListState.Ungrouped(
items = WalletPreviewData.draggableTokens,
),
dragConfig = DragConfig(
dndConfig = OrganizeTokensState.DragAndDropConfig(
onItemDragged = this::moveItem,
canDragItemOver = this::checkCanMoveItemOver,
onItemDragEnd = this::endMoving,
onDragStart = this::startMoving,
),
header = HeaderConfig(
onSortByBalanceClick = { /* no-op */ },
onGroupByNetworkClick = this::toggleTokensByNetworkGrouping,
header = OrganizeTokensState.HeaderConfig(
onSortClick = { /* no-op */ },
onGroupClick = this::toggleTokensByNetworkGrouping,
),
)
@ -62,6 +60,7 @@ internal class OrganizeTokensViewModel @Inject constructor(savedStateHandle: Sav
is OrganizeTokensListState.Ungrouped -> OrganizeTokensListState.GroupedByNetwork(
items = WalletPreviewData.draggableItems,
)
is OrganizeTokensListState.Empty -> itemsState
}
uiState = uiState.copy(itemsState = newListState)
@ -95,6 +94,7 @@ internal class OrganizeTokensViewModel @Inject constructor(savedStateHandle: Sav
is DraggableItem.Token -> when (uiState.itemsState) {
is OrganizeTokensListState.GroupedByNetwork -> items.divideGroups(movingItem)
is OrganizeTokensListState.Ungrouped -> items.divideItems(movingItem)
is OrganizeTokensListState.Empty -> uiState.itemsState.items
}
is DraggableItem.GroupPlaceholder -> items
}

View file

@ -0,0 +1,34 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem.RoundingMode
/**
* Update item [RoundingMode]
*
* @param mode new [RoundingMode]
*
* @return updated [DraggableItem]
* */
internal fun DraggableItem.updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) {
is DraggableItem.GroupPlaceholder -> this
is DraggableItem.GroupHeader -> this.copy(roundingMode = mode)
is DraggableItem.Token -> this.copy(roundingMode = mode)
}
/**
* Update item shadow visibility
*
* @param show if true then item should be elevated
*
* @return updated [DraggableItem]
* */
internal fun DraggableItem.updateShadowVisibility(show: Boolean): DraggableItem = when (this) {
is DraggableItem.GroupPlaceholder -> this
is DraggableItem.GroupHeader -> this.copy(showShadow = show)
is DraggableItem.Token -> this.copy(showShadow = show)
}
internal fun getGroupPlaceholder(index: Int): DraggableItem.GroupPlaceholder {
return DraggableItem.GroupPlaceholder(id = "placeholder_${index.inc()}")
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
import kotlinx.collections.immutable.PersistentList
@ -59,13 +59,15 @@ internal fun PersistentList<DraggableItem>.moveItem(fromIndex: Int, toIndex: Int
internal fun List<DraggableItem>.divideItems(movingItem: DraggableItem): List<DraggableItem> {
return this.map {
it
.roundingMode(DraggableItem.RoundingMode.All(showGap = true))
.showShadow(show = it.id == movingItem.id)
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
.updateShadowVisibility(show = it.id == movingItem.id)
}
}
internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
@Suppress("UNCHECKED_CAST") // Erased type
internal fun <T : DraggableItem> List<T>.uniteItems(): List<T> {
val lastItemIndex = this.lastIndex
return this.mapIndexed { index, item ->
val mode = when (index) {
0 -> DraggableItem.RoundingMode.Top()
@ -74,9 +76,9 @@ internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
}
item
.roundingMode(mode)
.showShadow(show = false)
}
.updateRoundingMode(mode)
.updateShadowVisibility(show = false)
} as List<T>
}
// TODO: Move to domain
@ -131,51 +133,51 @@ internal fun List<DraggableItem>.divideGroups(movingItem: DraggableItem): List<D
// Case when current item is the moving item
item.id == movingItem.id -> {
item
.roundingMode(DraggableItem.RoundingMode.All(showGap = true))
.showShadow(show = true)
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
.updateShadowVisibility(show = true)
}
// Case when moving item is a token and current item is the group of the moving token
movingItem is DraggableItem.Token && item.id == movingItem.groupId -> {
item
.roundingMode(DraggableItem.RoundingMode.All(showGap = true))
.showShadow(show = true)
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
.updateShadowVisibility(show = true)
}
// Case when both moving item and current item are tokens and belong to the same group
movingItem is DraggableItem.Token &&
item is DraggableItem.Token && item.groupId == movingItem.groupId -> {
item
.roundingMode(DraggableItem.RoundingMode.All(showGap = true))
.showShadow(show = false)
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
.updateShadowVisibility(show = false)
}
// Case when current item is the first item in the list
index == 0 -> {
item
.roundingMode(DraggableItem.RoundingMode.Top())
.showShadow(show = false)
.updateRoundingMode(DraggableItem.RoundingMode.Top())
.updateShadowVisibility(show = false)
}
// Case when current item is the last item in the list
index == lastItemIndex -> {
item
.roundingMode(DraggableItem.RoundingMode.Bottom())
.showShadow(show = false)
.updateRoundingMode(DraggableItem.RoundingMode.Bottom())
.updateShadowVisibility(show = false)
}
// Case when previous item is a GroupPlaceholder
this[index - 1] is DraggableItem.GroupPlaceholder -> {
item
.roundingMode(DraggableItem.RoundingMode.Top(showGap = true))
.showShadow(show = false)
.updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true))
.updateShadowVisibility(show = false)
}
// Case when next item is a GroupPlaceholder
this[index + 1] is DraggableItem.GroupPlaceholder -> {
item
.roundingMode(DraggableItem.RoundingMode.Bottom(showGap = true))
.showShadow(show = false)
.updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true))
.updateShadowVisibility(show = false)
}
// Default case when none of the above conditions are met
else -> {
item
.roundingMode(DraggableItem.RoundingMode.None)
.showShadow(show = false)
.updateRoundingMode(DraggableItem.RoundingMode.None)
.updateShadowVisibility(show = false)
}
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.models.Network
internal fun getTokenItemId(currencyId: CryptoCurrency.ID): String = currencyId.value
internal fun getGroupHeaderId(networkId: Network.ID): String = networkId.value

View file

@ -0,0 +1,19 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
@Suppress("UNCHECKED_CAST")
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 as PersistentList<DraggableItem.Token>)
is OrganizeTokensListState.Empty -> this
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TokenList.SortType
internal fun TokenList.updateSorting(isSortedByBalance: Boolean): TokenList {
val sortType = if (isSortedByBalance) SortType.BALANCE else SortType.NONE
return when (this) {
is TokenList.GroupedByNetwork -> this.copy(sortedBy = sortType)
is TokenList.Ungrouped -> this.copy(sortedBy = sortType)
is TokenList.NotInitialized -> this
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensState
import com.tangem.utils.converter.TwoWayConverter
internal class InProgressStateConverter : TwoWayConverter<OrganizeTokensState, OrganizeTokensState> {
override fun convert(value: OrganizeTokensState): OrganizeTokensState {
return value.copy(
actions = value.actions.copy(
showApplyProgress = true,
),
)
}
override fun convertBack(value: OrganizeTokensState): OrganizeTokensState {
return value.copy(
actions = value.actions.copy(
showApplyProgress = false,
),
)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter
import com.tangem.common.Provider
import com.tangem.domain.tokens.model.TokenList
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter
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 == TokenList.SortType.BALANCE,
isGrouped = value is TokenList.GroupedByNetwork,
),
actions = state.actions.copy(
canApply = itemsState !is OrganizeTokensListState.Empty,
),
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error
import com.tangem.common.Provider
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter
import com.tangem.utils.converter.Converter
internal class TokenListErrorConverter(
private val currentState: Provider<OrganizeTokensState>,
private val inProgressStateConverter: InProgressStateConverter,
) : Converter<TokenListError, OrganizeTokensState> {
// TODO: [REDACTED_JIRA]
override fun convert(value: TokenListError): OrganizeTokensState {
return inProgressStateConverter.convertBack(currentState())
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error
import com.tangem.common.Provider
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter
import com.tangem.utils.converter.Converter
internal class TokenListSortingErrorConverter(
private val currentState: Provider<OrganizeTokensState>,
private val inProgressStateConverter: InProgressStateConverter,
) : Converter<TokenListSortingError, OrganizeTokensState> {
// TODO: [REDACTED_JIRA]
override fun convert(value: TokenListSortingError): OrganizeTokensState {
return inProgressStateConverter.convertBack(currentState())
}
}

View file

@ -0,0 +1,56 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import androidx.annotation.DrawableRes
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.organizetokens.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
internal class CryptoCurrencyToDraggableItemConverter(
private val fiatCurrencyCode: String,
private val fiatCurrencySymbol: String,
) : Converter<CryptoCurrencyStatus, DraggableItem.Token> {
private val CryptoCurrency.networkIconResId: Int?
@DrawableRes get() {
// TODO: [REDACTED_JIRA]
return if (this is CryptoCurrency.Coin) null else R.drawable.img_eth_22
}
private val CryptoCurrency.tokenIconResId: Int
@DrawableRes get() {
// TODO: [REDACTED_JIRA]
return R.drawable.img_eth_22
}
override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token {
return DraggableItem.Token(
tokenItemState = createToTokenItemState(value),
groupId = getGroupHeaderId(value.currency.networkId),
)
}
private fun createToTokenItemState(currencyStatus: CryptoCurrencyStatus): TokenItemState.Draggable {
val currency = currencyStatus.currency
return TokenItemState.Draggable(
id = getTokenItemId(currency.id),
tokenIconUrl = currency.iconUrl,
tokenIconResId = currency.tokenIconResId,
networkIconResId = currency.networkIconResId,
name = currency.name,
fiatAmount = getFormattedFiatAmount(currencyStatus),
)
}
private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus): String {
val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
return BigDecimalFormatter.formatFiatAmount(fiatAmount, fiatCurrencyCode, fiatCurrencySymbol)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.feature.wallet.presentation.organizetokens.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.id),
networkName = group.network.name,
)
private fun createTokens(group: NetworkGroup): List<DraggableItem.Token> {
return itemConverter.convertList(group.currencies.toList())
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.domain.tokens.model.TokenList
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems
import com.tangem.utils.converter.Converter
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.NotInitialized -> createEmptyListState()
}
}
private fun createListState(tokenList: TokenList.GroupedByNetwork): OrganizeTokensListState.GroupedByNetwork {
return OrganizeTokensListState.GroupedByNetwork(
items = groupsConverter.convertList(tokenList.groups)
.flatten()
.uniteItems()
.toPersistentList(),
)
}
private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped {
return OrganizeTokensListState.Ungrouped(
items = tokensConverter.convertList(tokenList.currencies)
.uniteItems()
.toPersistentList(),
)
}
private fun createEmptyListState(): OrganizeTokensListState.Empty {
return OrganizeTokensListState.Empty
}
}