diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 8da459c047..af8d079514 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.shimmer) implementation(deps.compose.coil) + implementation(deps.compose.reorderableV2) /* DI */ implementation(deps.hilt.android) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 86ecb2a42f..fb299d1548 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R import com.tangem.features.details.ui.UserWalletListBlock import kotlinx.collections.immutable.persistentListOf @@ -51,6 +52,11 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), isWalletSavingInProgress = true, onAddNewWalletClick = {}, + walletReorderUM = WalletReorderUM( + isDragEnabled = true, + onMove = { _, _ -> }, + onDragStopped = {}, + ), ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index a8ef5eb141..375c63345a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -11,4 +11,11 @@ internal data class UserWalletListUM( val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, + val walletReorderUM: WalletReorderUM, +) + +internal data class WalletReorderUM( + val isDragEnabled: Boolean, + val onMove: (fromIndex: Int, toIndex: Int) -> Unit, + val onDragStopped: () -> Unit, ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index fbf60da951..5c011e92c8 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -13,15 +13,19 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.ApplyUserWalletListSortingUseCase import com.tangem.domain.wallets.usecase.UnlockWalletUseCase import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -38,6 +42,8 @@ internal class UserWalletListModel @Inject constructor( private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val unlockWalletUseCase: UnlockWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val walletFeatureToggles: WalletFeatureToggles, + private val applyUserWalletListSortingUseCase: ApplyUserWalletListSortingUseCase, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -55,6 +61,11 @@ internal class UserWalletListModel @Inject constructor( isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, onAddNewWalletClick = ::onAddNewWalletClick, + walletReorderUM = WalletReorderUM( + isDragEnabled = false, + onMove = ::onWalletReorder, + onDragStopped = ::onWalletDragStopped, + ), ), ) @@ -78,6 +89,11 @@ internal class UserWalletListModel @Inject constructor( userWallets = userWallets, isWalletSavingInProgress = isWalletSavingInProgress, addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), + walletReorderUM = WalletReorderUM( + isDragEnabled = walletFeatureToggles.isWalletReorderFeatureEnabled && userWallets.size > 1, + onMove = ::onWalletReorder, + onDragStopped = ::onWalletDragStopped, + ), ) } @@ -109,4 +125,23 @@ internal class UserWalletListModel @Inject constructor( } } } + + private fun onWalletReorder(fromIndex: Int, toIndex: Int) { + state.update { prevState -> + val wallets = prevState.userWallets.toMutableList() + wallets.add(toIndex, wallets.removeAt(fromIndex)) + prevState.copy(userWallets = wallets.toPersistentList()) + } + } + + private fun onWalletDragStopped() { + val userWalletIds = state.value.userWallets.map { UserWalletId(it.id) } + + modelScope.launch { + applyUserWalletListSortingUseCase(userWalletIds) + .onLeft { error -> + Timber.e("Failed to apply wallet list sorting: $error") + } + } + } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index e253ffec97..2cff0e2d16 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -2,17 +2,25 @@ package com.tangem.features.details.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.key +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -20,6 +28,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -28,26 +37,84 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.component.preview.PreviewUserWalletListComponent import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.ReorderableLazyListState +import sh.calvin.reorderable.rememberReorderableLazyListState @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + val reorderableListState = rememberReorderableLazyListState( + lazyListState = listState, + onMove = { from, to -> state.walletReorderUM.onMove(from.index, to.index) }, + ) + + val listHeight = WALLET_ITEM_HEIGHT * state.userWallets.size + ADD_WALLET_BUTTON_HEIGHT + BlockCard( modifier = modifier, ) { - state.userWallets.forEach { state -> - key(state.id) { - UserWalletItem( + LazyColumn( + state = listState, + modifier = Modifier + .heightIn(max = listHeight), + ) { + items( + items = state.userWallets, + key = { it.id }, + ) { walletState -> + WalletItem( + model = walletState, + reorderableListState = reorderableListState, + walletReorderUM = state.walletReorderUM, modifier = Modifier.fillMaxWidth(), - state = state, + ) + } + item(key = "add_wallet_button") { + AddWalletButton( + text = state.addNewWalletText, + isInProgress = state.isWalletSavingInProgress, + onClick = state.onAddNewWalletClick, ) } } - AddWalletButton( - text = state.addNewWalletText, - isInProgress = state.isWalletSavingInProgress, - onClick = state.onAddNewWalletClick, - ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun LazyItemScope.WalletItem( + model: UserWalletItemUM, + reorderableListState: ReorderableLazyListState, + walletReorderUM: WalletReorderUM, + modifier: Modifier = Modifier, +) { + ReorderableItem( + state = reorderableListState, + key = model.id, + modifier = modifier, + ) { isDragging -> + val elevation by animateDpAsState(if (isDragging) 9.dp else 0.dp) + val scale by animateFloatAsState(if (isDragging) 1.04f else 1f) + val shapeRadius by animateDpAsState(if (isDragging) 16.dp else 0.dp) + + Surface( + shape = RoundedCornerShape(shapeRadius), + shadowElevation = elevation, + modifier = Modifier.scale(scale), + ) { + UserWalletItem( + state = model, + modifier = Modifier + .longPressDraggableHandle( + enabled = walletReorderUM.isDragEnabled, + onDragStopped = walletReorderUM.onDragStopped, + ) + .background(color = TangemTheme.colors.background.primary), + ) + } } } @@ -109,6 +176,9 @@ private fun AddWalletButton( } } +private val WALLET_ITEM_HEIGHT = 72.dp +private val ADD_WALLET_BUTTON_HEIGHT = 60.dp + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index d1bd0b0529..256d99fb51 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -487,6 +487,7 @@ internal class WalletModel @Inject constructor( is WalletsUpdateActionResolver.Action.ReloadWallets -> { reloadWarnings(action) } + is WalletsUpdateActionResolver.Action.ReorderWallets -> reorderWallets(action) WalletsUpdateActionResolver.Action.EmptyWallets -> { Timber.w("Wallets list is empty!") } @@ -496,6 +497,25 @@ internal class WalletModel @Inject constructor( } } + private fun reorderWallets(action: WalletsUpdateActionResolver.Action.ReorderWallets) { + val currentWalletId = stateHolder.getSelectedWalletId() + val currentIndex = stateHolder.getWalletIndexByWalletId(userWalletId = currentWalletId) + + stateHolder.update( + transformer = ReorderWalletsTransformer( + wallets = action.wallets, + ), + ) + + val newIndex = stateHolder.getWalletIndexByWalletId(userWalletId = currentWalletId) + + if (currentIndex != null && newIndex != null && currentIndex != newIndex) { + scrollToWallet(prevIndex = currentIndex, newIndex = newIndex) { + stateHolder.update { it.copy(selectedWalletIndex = newIndex) } + } + } + } + private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWallets) { action.wallets.forEach { walletScreenContentLoader.load( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 4946ef99e9..1f63082dad 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -82,6 +82,9 @@ internal class WalletsUpdateActionResolver @Inject constructor( isWalletsCountChanged(state, wallets) -> { getChangeWalletsListAction(state, wallets, selectedWallet) } + isWalletsOrderChanged(state, wallets) -> { + Action.ReorderWallets(wallets = wallets) + } isAnotherWalletSelected(state, selectedWallet) -> { Action.ReinitializeNewWallet( prevWalletId = state.getPrevSelectedWallet().id, @@ -121,6 +124,13 @@ internal class WalletsUpdateActionResolver @Inject constructor( return prevWalletsSize != walletsSize } + private fun isWalletsOrderChanged(state: WalletScreenState, wallets: List): Boolean { + val prevWalletIds = state.wallets.map { it.walletCardState.id } + val newWalletIds = wallets.map { it.walletId } + + return prevWalletIds != newWalletIds + } + private fun getChangeWalletsListAction( state: WalletScreenState, wallets: List, @@ -394,6 +404,15 @@ internal class WalletsUpdateActionResolver @Inject constructor( val wallets: List, ) : Action() + data class ReorderWallets( + val wallets: List, + ) : Action() { + + override fun toString(): String { + return "ReorderWallets(wallets = ${wallets.joinToString { it.walletId.toString() }})" + } + } + data object EmptyWallets : Action() data object Unknown : Action() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt new file mode 100644 index 0000000000..334c41892c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import kotlinx.collections.immutable.toImmutableList + +/** + * Transformer that reorders wallets according to the new order + * + * @property wallets wallets in the new order + */ +internal class ReorderWalletsTransformer( + private val wallets: List, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + val walletIdToIndex = wallets.withIndex().associate { it.value.walletId to it.index } + + val reorderedWallets = prevState.wallets + .sortedBy { walletIdToIndex[it.walletCardState.id] ?: Int.MAX_VALUE } + .toImmutableList() + + return if (reorderedWallets != prevState.wallets) { + prevState.copy(wallets = reorderedWallets) + } else { + prevState + } + } +} \ No newline at end of file