Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-29 11:42:56 +03:00
commit 9abedff8b5
80 changed files with 1567 additions and 3658 deletions

View file

@ -22,6 +22,7 @@ dependencies {
implementation(deps.compose.ui.tooling)
implementation(deps.compose.shimmer)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.reorderable)
/** Other libraries */
implementation(deps.kotlin.immutable.collections)

View file

@ -7,10 +7,7 @@ import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenO
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.wallet.state.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig
import com.tangem.feature.wallet.presentation.wallet.state.*
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.util.UUID
@ -108,21 +105,22 @@ internal object WalletPreviewData {
private const val tokensSize = 3
val draggableItems = List(networksSize) { it }
.flatMap { index ->
val n = index + 1
val lastNetworkIndex = networksSize - 1
val networkNumber = index + 1
val group = DraggableItem.GroupHeader(
id = "group_$n",
networkName = "$n",
id = "group_$networkNumber",
networkName = "$networkNumber",
)
val tokens: MutableList<DraggableItem.Token> = mutableListOf()
repeat(times = tokensSize) { i ->
val nt = i + 1
val tokenNumber = i + 1
tokens.add(
DraggableItem.Token(
tokenItemState = tokenItemDragState.copy(
id = "${group.id}_token_$nt",
name = "Token $nt",
id = "${group.id}_token_$tokenNumber",
name = "Token $tokenNumber",
networkIconResId = R.drawable.img_eth_22.takeIf { i != 0 },
),
groupId = group.id,
@ -130,9 +128,14 @@ internal object WalletPreviewData {
)
}
val divider = DraggableItem.GroupPlaceholder(id = "divider_$networkNumber")
buildList {
add(group)
addAll(tokens)
if (index != lastNetworkIndex) {
add(divider)
}
}
}
.toPersistentList()
@ -149,6 +152,12 @@ internal object WalletPreviewData {
onSortByBalanceClick = {},
onGroupByNetworkClick = {},
),
dragConfig = OrganizeTokensStateHolder.DragConfig(
onItemDragged = { _, _ -> },
onDragStart = {},
canDragItemOver = { _, _ -> false },
onItemDragEnd = {},
),
actions = OrganizeTokensStateHolder.ActionsConfig(
onApplyClick = {},
onCancelClick = {},
@ -161,6 +170,14 @@ internal object WalletPreviewData {
),
)
val manageButtons = persistentListOf(
WalletManageButton.Buy(onClick = {}),
WalletManageButton.Send(onClick = {}),
WalletManageButton.Receive(onClick = {}),
WalletManageButton.Exchange(onClick = {}),
WalletManageButton.CopyAddress(onClick = {}),
)
val multicurrencyWalletScreenState = WalletStateHolder.MultiCurrencyContent(
onBackClick = {},
topBarConfig = walletTopBarConfig,
@ -220,6 +237,12 @@ internal object WalletPreviewData {
),
),
),
notifications = persistentListOf(
WalletNotification.UnreachableNetworks,
WalletNotification.LikeTangemApp(onClick = {}),
WalletNotification.NeedToBackup(onClick = {}),
WalletNotification.ScanCard(onClick = {}),
),
onOrganizeTokensClick = {},
)
@ -252,5 +275,12 @@ internal object WalletPreviewData {
),
),
),
notifications = persistentListOf(
WalletNotification.UnreachableNetworks,
WalletNotification.LikeTangemApp(onClick = {}),
WalletNotification.NeedToBackup(onClick = {}),
WalletNotification.ScanCard(onClick = {}),
),
buttons = manageButtons,
)
}

View file

@ -14,6 +14,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
internal fun NetworkGroupItem(networkName: String, modifier: Modifier = Modifier) {
@ -24,16 +26,33 @@ internal fun NetworkGroupItem(networkName: String, modifier: Modifier = Modifier
}
@Composable
internal fun DraggableNetworkGroupItem(networkName: String, modifier: Modifier = Modifier) {
internal fun DraggableNetworkGroupItem(
networkName: String,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
InternalNetworkGroupItem(
modifier = modifier,
networkName = networkName,
endIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_group_drop_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
Box(
modifier = Modifier
.size(TangemTheme.dimens.size32)
.let {
if (reorderableTokenListState != null) {
it.detectReorder(reorderableTokenListState)
} else {
it
}
},
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_group_drop_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
},
)
}

View file

@ -34,6 +34,8 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
private const val DOTS = "•••"
val TOKEN_ITEM_HEIGHT: Dp
@ -46,7 +48,7 @@ internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) {
when (state) {
is TokenItemState.Content -> ContentTokenItem(state, modifier)
is TokenItemState.Loading -> LoadingTokenItem(modifier)
is TokenItemState.Draggable -> DraggableTokenItem(state, modifier)
is TokenItemState.Draggable -> DraggableTokenItem(state, modifier, reorderableTokenListState = null)
is TokenItemState.Unreachable -> UnreachableTokenItem(state, modifier)
}
}
@ -71,7 +73,11 @@ private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier
}
@Composable
internal fun DraggableTokenItem(state: TokenItemState.Draggable, modifier: Modifier = Modifier) {
internal fun DraggableTokenItem(
state: TokenItemState.Draggable,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
InternalTokenItem(
modifier = modifier,
name = state.name,
@ -81,13 +87,25 @@ internal fun DraggableTokenItem(state: TokenItemState.Draggable, modifier: Modif
amount = state.fiatAmount,
hasPending = false,
options = { ref ->
Icon(
Box(
modifier = Modifier
.constrainAsOptionsItem(scope = this, ref),
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
.size(TangemTheme.dimens.size32)
.constrainAsOptionsItem(scope = this, ref)
.let {
if (reorderableTokenListState != null) {
it.detectReorder(reorderableTokenListState)
} else {
it
}
},
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
},
)
}

View file

@ -9,10 +9,7 @@ import androidx.compose.material.AppBarDefaults
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@ -24,14 +21,19 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.BackgroundActionButton
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.buttons.actions.ActionConfig
import com.tangem.core.ui.components.buttons.actions.BackgroundActionButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem
import com.tangem.feature.wallet.presentation.common.component.DraggableTokenItem
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable
@Composable
internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Modifier = Modifier) {
@ -47,6 +49,7 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Mo
modifier = Modifier.padding(paddingValues),
listState = tokensListState,
state = state.itemsState,
dragConfig = state.dragConfig,
)
},
floatingActionButtonPosition = FabPosition.Center,
@ -57,28 +60,56 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Mo
)
}
// TODO: Fix list animations
@Composable
private fun TokenList(listState: LazyListState, state: OrganizeTokensListState, modifier: Modifier = Modifier) {
private fun TokenList(
listState: LazyListState,
state: OrganizeTokensListState,
dragConfig: OrganizeTokensStateHolder.DragConfig,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
val reorderableListState = rememberReorderableLazyListState(
onMove = dragConfig.onItemDragged,
listState = listState,
canDragOver = dragConfig.canDragItemOver,
onDragEnd = { _, _ -> dragConfig.onItemDragEnd() },
)
val items = state.items
val lastItemIndex = items.lastIndex
LazyColumn(
modifier = Modifier
.reorderable(reorderableListState)
.align(Alignment.TopCenter)
.padding(horizontal = TangemTheme.dimens.spacing16),
state = listState,
state = reorderableListState.listState,
contentPadding = PaddingValues(
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing92,
),
) {
itemsIndexed(
items = state.items,
items = items,
key = { _, item -> item.id },
) { index, item ->
val onDragStart = remember(item) {
{ dragConfig.onDragStart(item) }
}
DraggableItem(
item = item,
index = index,
lastItemIndex = state.items.lastIndex,
lastItemIndex = lastItemIndex,
reorderableState = reorderableListState,
onDragStart = onDragStart,
)
if (item is DraggableItem.GroupPlaceholder) {
// This item should be displayed in the list but remain invisible
Box(modifier = Modifier.fillMaxWidth())
}
}
}
@ -87,19 +118,39 @@ private fun TokenList(listState: LazyListState, state: OrganizeTokensListState,
}
@Composable
private fun DraggableItem(index: Int, item: DraggableItem, lastItemIndex: Int) {
val itemModifier = Modifier
.clipFirstAndLastItems(index, lastItemIndex)
private fun LazyItemScope.DraggableItem(
index: Int,
item: DraggableItem,
lastItemIndex: Int,
reorderableState: ReorderableLazyListState,
onDragStart: () -> Unit,
) {
ReorderableItem(
reorderableState = reorderableState,
index = index,
key = item.id,
) { isDragging ->
when (item) {
is DraggableItem.GroupHeader -> DraggableNetworkGroupItem(
modifier = itemModifier,
networkName = item.networkName,
)
is DraggableItem.Token -> DraggableTokenItem(
modifier = itemModifier,
state = item.tokenItemState,
)
if (isDragging) {
onDragStart()
}
val itemModifier = Modifier
.clipFirstLastAndDraggingItems(index, lastItemIndex, isDragging)
when (item) {
is DraggableItem.GroupHeader -> DraggableNetworkGroupItem(
modifier = itemModifier,
networkName = item.networkName,
reorderableTokenListState = reorderableState,
)
is DraggableItem.Token -> DraggableTokenItem(
modifier = itemModifier,
state = item.tokenItemState,
reorderableTokenListState = reorderableState,
)
is DraggableItem.GroupPlaceholder -> Unit
}
}
}
@ -164,16 +215,20 @@ private fun TopBar(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
BackgroundActionButton(
config = ActionConfig(
text = stringResource(id = R.string.organize_tokens_sort_by_balance),
iconResId = R.drawable.ic_sort_24,
onClick = config.onSortByBalanceClick,
),
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.organize_tokens_sort_by_balance),
iconResId = R.drawable.ic_sort_24,
onClick = config.onSortByBalanceClick,
)
BackgroundActionButton(
config = ActionConfig(
text = stringResource(id = R.string.organize_tokens_group),
iconResId = R.drawable.ic_group_24,
onClick = config.onGroupByNetworkClick,
),
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.organize_tokens_group),
iconResId = R.drawable.ic_group_24,
onClick = config.onGroupByNetworkClick,
)
}
}
@ -201,29 +256,36 @@ private fun Actions(config: OrganizeTokensStateHolder.ActionsConfig, modifier: M
}
}
private fun Modifier.clipFirstAndLastItems(index: Int, lastItemIndex: Int): Modifier = composed {
when (index) {
0 -> {
this
.clip(
private fun Modifier.clipFirstLastAndDraggingItems(index: Int, lastItemIndex: Int, isDragging: Boolean): Modifier =
composed {
when {
isDragging -> {
val elevation by animateDpAsState(
targetValue = TangemTheme.dimens.elevation12,
label = "dragging_item_shadow_elevation",
)
this.shadow(elevation, shape = TangemTheme.shapes.roundedCornersXMedium)
}
index == 0 -> {
this.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastItemIndex -> {
this
.clip(
}
index == lastItemIndex -> {
this.clip(
RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),
)
}
else -> this
}
else -> this
}
}
// region Preview

View file

@ -2,12 +2,14 @@ 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.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import org.burnoutcrew.reorderable.ItemPosition
internal data class OrganizeTokensStateHolder(
val header: HeaderConfig,
val itemsState: OrganizeTokensListState,
val dragConfig: DragConfig,
val actions: ActionsConfig,
) {
@ -20,44 +22,79 @@ internal data class OrganizeTokensStateHolder(
val onApplyClick: () -> Unit,
val onCancelClick: () -> Unit,
)
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,
)
}
@Immutable
internal sealed class OrganizeTokensListState {
abstract val items: ImmutableList<DraggableItem>
internal sealed interface OrganizeTokensListState {
val items: PersistentList<DraggableItem>
data class GroupedByNetwork(
override val items: ImmutableList<DraggableItem>,
) : OrganizeTokensListState()
override val items: PersistentList<DraggableItem>,
) : OrganizeTokensListState
data class Ungrouped(
override val items: ImmutableList<DraggableItem.Token>,
) : OrganizeTokensListState()
override val items: PersistentList<DraggableItem.Token>,
) : OrganizeTokensListState
@Suppress("UNCHECKED_CAST")
inline fun updateItems(update: (List<DraggableItem>) -> List<DraggableItem>): OrganizeTokensListState {
fun updateItems(update: (PersistentList<DraggableItem>) -> List<DraggableItem>): OrganizeTokensListState {
val updatedItems = update(this.items).toPersistentList()
return when (this) {
is GroupedByNetwork -> this.copy(items = updatedItems)
is Ungrouped -> this.copy(items = updatedItems as ImmutableList<DraggableItem.Token>)
is Ungrouped -> this.copy(items = updatedItems as PersistentList<DraggableItem.Token>)
}
}
}
/**
* Helper class for the DND list items
*
* @property id ID of the item
* */
@Immutable
internal sealed interface DraggableItem {
val id: String
/**
* Item for network group header.
*
* @property id ID of the network group
* @property networkName network group name
* */
data class GroupHeader(
override val id: String,
val networkName: String,
) : 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
* */
data class Token(
val tokenItemState: TokenItemState.Draggable,
val groupId: String,
) : 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
}

View file

@ -5,10 +5,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
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.checkCanMoveHeaderOver
import com.tangem.feature.wallet.presentation.organizetokens.utils.checkCanMoveTokenOver
import com.tangem.feature.wallet.presentation.organizetokens.utils.findItemsToMove
import com.tangem.feature.wallet.presentation.organizetokens.utils.moveItem
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toPersistentList
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
import kotlin.properties.Delegates
@ -16,6 +22,10 @@ import kotlin.properties.Delegates
@HiltViewModel
internal class OrganizeTokensViewModel @Inject constructor() : ViewModel() {
// TODO: Move to domain
@Volatile
private var groupIdToTokens: Map<String, List<DraggableItem.Token>>? = null
var router: InnerWalletRouter by Delegates.notNull()
var uiState: OrganizeTokensStateHolder by mutableStateOf(getInitialState())
@ -25,6 +35,12 @@ internal class OrganizeTokensViewModel @Inject constructor() : ViewModel() {
itemsState = OrganizeTokensListState.Ungrouped(
items = WalletPreviewData.draggableTokens,
),
dragConfig = DragConfig(
onItemDragged = this::moveItem,
canDragItemOver = this::checkCanMoveItemOver,
onItemDragEnd = this::expandGroups,
onDragStart = this::collapseGroup,
),
header = HeaderConfig(
onSortByBalanceClick = { /* no-op */ },
onGroupByNetworkClick = this::toggleTokensByNetworkGrouping,
@ -43,4 +59,69 @@ internal class OrganizeTokensViewModel @Inject constructor() : ViewModel() {
uiState = uiState.copy(itemsState = newListState)
}
private fun checkCanMoveItemOver(moveOverItemPosition: ItemPosition, movedItemPosition: ItemPosition): Boolean {
val items = (uiState.itemsState as? OrganizeTokensListState.GroupedByNetwork)
?.items
?: return true // If ungrouped then item can be moved anywhere
val (moveOverItem, movedItem) = items.findItemsToMove(moveOverItemPosition.key, movedItemPosition.key)
if (moveOverItem == null || movedItem == null) {
return false
}
return when (movedItem) {
is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(moveOverItemPosition, moveOverItem, items.lastIndex)
is DraggableItem.Token -> checkCanMoveTokenOver(movedItem, moveOverItem)
is DraggableItem.GroupPlaceholder -> false
}
}
private fun collapseGroup(item: DraggableItem) {
if (!groupIdToTokens.isNullOrEmpty() || item is DraggableItem.Token) return
val itemsState = uiState.itemsState as? OrganizeTokensListState.GroupedByNetwork ?: return
groupIdToTokens = itemsState.items
.asSequence()
.filterIsInstance<DraggableItem.Token>()
.groupBy(DraggableItem.Token::groupId)
uiState = uiState.copy(
itemsState = itemsState.updateItems { items ->
items.filterNot { it is DraggableItem.Token && it.groupId == item.id }
},
)
}
private fun expandGroups() {
if (groupIdToTokens.isNullOrEmpty()) return
val itemsState = uiState.itemsState as? OrganizeTokensListState.GroupedByNetwork ?: return
val currentGroups = itemsState.items.filterIsInstance<DraggableItem.GroupHeader>()
val newItems = currentGroups
.flatMapIndexed { index, group ->
mutableListOf<DraggableItem>(group)
.also { it.addAll(groupIdToTokens?.get(group.id).orEmpty()) }
.also {
if (index != currentGroups.lastIndex) {
it.add(DraggableItem.GroupPlaceholder(id = "group_divider_$index"))
}
}
}
uiState = uiState.copy(
itemsState = itemsState.updateItems { newItems },
)
groupIdToTokens = null
}
private fun moveItem(from: ItemPosition, to: ItemPosition) {
uiState = uiState.copy(
itemsState = uiState.itemsState.updateItems {
it.moveItem(from.index, to.index)
},
)
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
import kotlinx.collections.immutable.PersistentList
import org.burnoutcrew.reorderable.ItemPosition
internal fun List<DraggableItem>.findItemsToMove(
moveOverItemKey: Any?,
movedItemKey: Any?,
): Pair<DraggableItem?, DraggableItem?> {
var moveOverItem: DraggableItem? = null
var movedItem: DraggableItem? = null
for (item in this) {
if (item.id == moveOverItemKey) {
moveOverItem = item
}
if (item.id == movedItemKey) {
movedItem = item
}
if (moveOverItem != null && movedItem != null) {
break
}
}
return Pair(moveOverItem, movedItem)
}
internal fun checkCanMoveHeaderOver(
moveOverItemPosition: ItemPosition,
moveOverItem: DraggableItem,
lastItemIndex: Int,
): Boolean {
return when {
moveOverItemPosition.index == 0 -> true
moveOverItemPosition.index == lastItemIndex -> true
moveOverItem is DraggableItem.GroupPlaceholder -> true
else -> false
}
}
internal fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean {
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.GroupPlaceholder -> false
}
}
internal fun PersistentList<DraggableItem>.moveItem(fromIndex: Int, toIndex: Int): PersistentList<DraggableItem> {
val fromItem = this[fromIndex]
return this
.removeAt(fromIndex)
.add(toIndex, fromItem)
}

View file

@ -0,0 +1,79 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.buttons.actions.ActionConfig
import com.tangem.feature.wallet.impl.R
/**
* Wallet manage button state
*
* @param config action config
*
[REDACTED_AUTHOR]
*/
sealed class WalletManageButton(val config: ActionConfig) {
/**
* Buy
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class Buy(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Buy",
iconResId = R.drawable.ic_plus_24,
onClick = onClick,
),
)
/**
* Send
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class Send(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
onClick = onClick,
),
)
/**
* Receive
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class Receive(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Receive",
iconResId = R.drawable.ic_arrow_down_24,
onClick = onClick,
),
)
/**
* Exchange
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class Exchange(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Exchange",
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = onClick,
),
)
/**
* Copy address
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class CopyAddress(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Copy address",
iconResId = R.drawable.ic_copy_24,
onClick = onClick,
),
)
}

View file

@ -0,0 +1,64 @@
package com.tangem.feature.wallet.presentation.wallet.state
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.buttons.actions.ActionButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import kotlinx.collections.immutable.ImmutableList
/**
* Wallet manage buttons
*
* @param buttons manage buttons
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun WalletManageButtons(buttons: ImmutableList<WalletManageButton>) {
Row(
modifier = Modifier
.horizontalScroll(state = rememberScrollState())
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
) {
buttons.forEach { button ->
key(button.config.text) {
ActionButton(config = button.config)
}
}
}
}
@Preview
@Composable
private fun Preview_WalletManageButtons_Light(
@PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList<WalletManageButton>,
) {
TangemTheme(isDark = false) {
WalletManageButtons(buttons = buttons)
}
}
@Preview
@Composable
private fun Preview_WalletManageButtons_Dark(
@PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList<WalletManageButton>,
) {
TangemTheme(isDark = true) {
WalletManageButtons(buttons = buttons)
}
}
private class WalletManageButtonProvider : CollectionPreviewParameterProvider<ImmutableList<WalletManageButton>>(
collection = listOf(WalletPreviewData.manageButtons),
)

View file

@ -0,0 +1,65 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.notifications.NotificationState
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.feature.wallet.impl.R
/**
* Wallet notification component state
*
* @property state state
*
[REDACTED_AUTHOR]
*/
sealed class WalletNotification(open val state: NotificationState) {
/**
* "Backup the card" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class NeedToBackup(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Backup your card",
iconResId = R.drawable.ic_alert_circle_24,
onClick = onClick,
tint = TangemColorPalette.Amaranth,
),
)
/** "Unreachable networks" notification */
object UnreachableNetworks : WalletNotification(
state = NotificationState.Simple(
title = "Some networks are unreachable",
iconResId = R.drawable.img_attention_20,
tint = null,
),
)
/**
* "Like Tangem App" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class LikeTangemApp(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Like Tangem App?",
iconResId = R.drawable.ic_star_24,
onClick = onClick,
tint = TangemColorPalette.Tangerine,
),
)
/**
* "Scan the card" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class ScanCard(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Scan your card to continue",
iconResId = R.drawable.ic_tangem_24,
onClick = onClick,
),
)
}

View file

@ -10,6 +10,7 @@ import kotlinx.collections.immutable.ImmutableList
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
* @property notifications notifications
*
[REDACTED_AUTHOR]
*/
@ -19,16 +20,19 @@ internal sealed class WalletStateHolder(
open val selectedWallet: WalletCardState,
open val wallets: ImmutableList<WalletCardState>,
open val contentItems: ImmutableList<WalletContentItemState>,
open val notifications: ImmutableList<WalletNotification>,
) {
/**
* Multi currency wallet content state
*
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
* @property notifications notifications
* @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked
*/
data class MultiCurrencyContent(
override val onBackClick: () -> Unit,
@ -36,8 +40,9 @@ internal sealed class WalletStateHolder(
override val selectedWallet: WalletCardState,
override val wallets: ImmutableList<WalletCardState>,
override val contentItems: ImmutableList<WalletContentItemState.MultiCurrencyItem>,
override val notifications: ImmutableList<WalletNotification>,
val onOrganizeTokensClick: () -> Unit,
) : WalletStateHolder(onBackClick, topBarConfig, selectedWallet, wallets, contentItems)
) : WalletStateHolder(onBackClick, topBarConfig, selectedWallet, wallets, contentItems, notifications)
/**
* Single currency wallet content state
@ -47,6 +52,8 @@ internal sealed class WalletStateHolder(
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
* @property notifications notifications
* @property buttons manage buttons
*/
data class SingleCurrencyContent(
override val onBackClick: () -> Unit,
@ -54,5 +61,7 @@ internal sealed class WalletStateHolder(
override val selectedWallet: WalletCardState,
override val wallets: ImmutableList<WalletCardState>,
override val contentItems: ImmutableList<WalletContentItemState.SingleCurrencyItem>,
) : WalletStateHolder(onBackClick, topBarConfig, selectedWallet, wallets, contentItems)
override val notifications: ImmutableList<WalletNotification>,
val buttons: ImmutableList<WalletManageButton>,
) : WalletStateHolder(onBackClick, topBarConfig, selectedWallet, wallets, contentItems, notifications)
}

View file

@ -1,28 +1,26 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.RoundedActionButton
import com.tangem.core.ui.components.buttons.actions.ActionConfig
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
@ -30,9 +28,11 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem
import com.tangem.feature.wallet.presentation.common.component.TokenItem
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButtons
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletCardsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar
import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration
/**
* Wallet screen
@ -49,15 +49,29 @@ internal fun WalletScreen(state: WalletStateHolder, modifier: Modifier = Modifie
topBar = { WalletTopBar(config = state.topBarConfig) },
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
val lastContentItemIndex = remember(state.contentItems) { state.contentItems.lastIndex }
LazyColumn(
modifier = modifier
.padding(scaffoldPaddings)
.padding(paddingValues = scaffoldPaddings)
.fillMaxSize(),
contentPadding = PaddingValues(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing8,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item { WalletCardsList(wallets = state.wallets) }
item {
WalletCardsList(
wallets = state.wallets,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
)
}
if (state is WalletStateHolder.SingleCurrencyContent) {
item { WalletManageButtons(buttons = state.buttons) }
}
items(items = state.notifications, itemContent = { Notification(state = it.state) })
itemsIndexed(
items = state.contentItems,
@ -70,36 +84,19 @@ internal fun WalletScreen(state: WalletStateHolder, modifier: Modifier = Modifie
is WalletContentItemState.SingleCurrencyItem.Transaction -> index
}
},
) { index, item ->
val itemModifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.clipFirstAndLastItems(index, lastContentItemIndex)
when (item) {
is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> {
NetworkGroupItem(networkName = item.networkName, modifier = itemModifier)
}
is WalletContentItemState.MultiCurrencyItem.Token -> {
TokenItem(state = item.state, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.Title -> {
SingleCurrencyTitle(config = item, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.TransactionGroupTitle -> {
TransactionGroupTitle(config = item, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.Transaction -> {
Transaction(state = item.state, modifier = itemModifier)
}
}
}
itemContent = { index, item ->
ContentItem(currentIndex = index, lastIndex = state.contentItems.lastIndex, item = item)
},
)
if (state is WalletStateHolder.MultiCurrencyContent) {
item {
RoundedActionButton(
text = stringResource(id = R.string.organize_tokens_title),
iconResId = R.drawable.ic_filter_24,
onClick = state.onOrganizeTokensClick,
config = ActionConfig(
text = stringResource(id = R.string.organize_tokens_title),
iconResId = R.drawable.ic_filter_24,
onClick = state.onOrganizeTokensClick,
),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing14),
)
}
@ -108,6 +105,29 @@ internal fun WalletScreen(state: WalletStateHolder, modifier: Modifier = Modifie
}
}
@Composable
private fun ContentItem(currentIndex: Int, lastIndex: Int, item: WalletContentItemState) {
val itemModifier = Modifier.walletContentItemDecoration(currentIndex, lastIndex)
when (item) {
is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> {
NetworkGroupItem(networkName = item.networkName, modifier = itemModifier)
}
is WalletContentItemState.MultiCurrencyItem.Token -> {
TokenItem(state = item.state, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.Title -> {
SingleCurrencyTitle(config = item, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.TransactionGroupTitle -> {
TransactionGroupTitle(config = item, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.Transaction -> {
Transaction(state = item.state, modifier = itemModifier)
}
}
}
@Composable
private fun SingleCurrencyTitle(
config: WalletContentItemState.SingleCurrencyItem.Title,
@ -166,31 +186,6 @@ private fun TransactionGroupTitle(
)
}
private fun Modifier.clipFirstAndLastItems(index: Int, lastItemIndex: Int): Modifier = composed {
when (index) {
0 -> {
this
.padding(top = TangemTheme.dimens.spacing14)
.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastItemIndex -> {
this
.clip(
RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),
)
}
else -> this
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable

View file

@ -23,21 +23,21 @@ import kotlinx.collections.immutable.persistentListOf
/**
* Wallets list
*
* @param wallets list of wallet state
* @param wallets list of wallet state
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletCardsList(wallets: ImmutableList<WalletCardState>) {
internal fun WalletCardsList(wallets: ImmutableList<WalletCardState>, modifier: Modifier = Modifier) {
val horizontalCardPadding = TangemTheme.dimens.spacing16
val itemWidth = LocalConfiguration.current.screenWidthDp.dp - horizontalCardPadding * 2
val lazyListState = rememberLazyListState()
LazyRow(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
modifier = modifier.background(color = TangemTheme.colors.background.secondary),
state = lazyListState,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) {

View file

@ -0,0 +1,36 @@
package com.tangem.feature.wallet.presentation.wallet.ui.decorations
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import com.tangem.core.ui.res.TangemTheme
/**
[REDACTED_AUTHOR]
*/
internal fun Modifier.walletContentItemDecoration(currentIndex: Int, lastItemIndex: Int): Modifier = composed {
when (currentIndex) {
0 -> {
this
.padding(top = TangemTheme.dimens.spacing14)
.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastItemIndex -> {
this
.clip(
RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),
)
}
else -> this
}
}