Updated on 2026-08-14
This commit is contained in:
parent
37a31042c0
commit
5a3e53cb3b
28 changed files with 1763 additions and 13 deletions
|
|
@ -39,22 +39,18 @@ fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemThe
|
|||
}
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect at the right end of the screen. Same as [BottomFade]
|
||||
* but with a horizontal gradient.
|
||||
* A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating
|
||||
* elements and floating button at the bottom of the screen.
|
||||
*/
|
||||
@Composable
|
||||
fun HorizontalFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) {
|
||||
fun BottomFade(gradientBrush: Brush, modifier: Modifier = Modifier) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size100 + bottomBarHeight)
|
||||
.background(gradientBrush),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -83,6 +84,7 @@ fun SecondaryTangemButton(
|
|||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.clip(shape.toShape(size))
|
||||
.hazeEffectTangem()
|
||||
.then(backgroundModifier),
|
||||
text = text,
|
||||
contentColor = contentColor,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel
|
||||
import com.tangem.feature.wallet.child.organizetokens.ui.OrganizeTokensContent
|
||||
|
||||
internal class OrganizeTokensComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OrganizeTokensModel = getOrCreateModel(params)
|
||||
|
||||
override fun dismiss() {
|
||||
params.callback.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
OrganizeTokensContent(
|
||||
organizeTokensUM = uiState,
|
||||
dragAndDropIntents = model.dragAndDropAdapter,
|
||||
onDismiss = ::dismiss,
|
||||
)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onDismiss()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val callback: Callback,
|
||||
)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.child.organizetokens.di
|
|||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModelLegacy
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -17,4 +18,9 @@ internal interface OrganizeTokensModule {
|
|||
@IntoMap
|
||||
@ClassKey(OrganizeTokensModelLegacy::class)
|
||||
fun bindOrganizeTokensModelLegacy(model: OrganizeTokensModelLegacy): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OrganizeTokensModel::class)
|
||||
fun bindOrganizeTokensModel(model: OrganizeTokensModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
|
||||
/**
|
||||
* Helper class for the DND list items
|
||||
*
|
||||
* @property id ID of the item
|
||||
* @property roundingModeUM item [RoundingModeUM]
|
||||
* @property isShowShadow if true then item should be elevated
|
||||
* */
|
||||
@Immutable
|
||||
internal sealed class OrganizeRowItemUM {
|
||||
abstract val id: String
|
||||
abstract val roundingModeUM: RoundingModeUM
|
||||
abstract val isShowShadow: Boolean
|
||||
|
||||
/**
|
||||
* Item for token.
|
||||
*
|
||||
* @property id ID of the token
|
||||
* @property groupId ID of the network group which contains this token
|
||||
* @property accountId ID of account which contains this token
|
||||
* @property tokenRowUM state of the token item
|
||||
* @property roundingModeUM item [RoundingModeUM]
|
||||
* @property isShowShadow if true then item should be elevated
|
||||
* */
|
||||
data class Token(
|
||||
override val isShowShadow: Boolean = false,
|
||||
override val roundingModeUM: RoundingModeUM = RoundingModeUM.None,
|
||||
val tokenRowUM: TangemTokenRowUM,
|
||||
val groupId: String,
|
||||
val accountId: String = "",
|
||||
) : OrganizeRowItemUM() {
|
||||
override val id: String = tokenRowUM.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Item for network group header.
|
||||
*
|
||||
* @property id ID of the network group
|
||||
* @property accountId ID of account which contains this network group
|
||||
* @property headerRowUM state of the network group header item
|
||||
* @property roundingModeUM item [RoundingModeUM]
|
||||
* @property isShowShadow if true then item should be elevated
|
||||
* */
|
||||
data class Network(
|
||||
override val roundingModeUM: RoundingModeUM = RoundingModeUM.None,
|
||||
override val isShowShadow: Boolean = false,
|
||||
val headerRowUM: TangemHeaderRowUM,
|
||||
val accountId: String = "",
|
||||
) : OrganizeRowItemUM() {
|
||||
override val id: String = headerRowUM.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Item for portfolio.
|
||||
*
|
||||
* @property id ID of the portfolio
|
||||
* @property headerRowUM state of the portfolio item
|
||||
* @property roundingModeUM item [RoundingModeUM]
|
||||
* @property isShowShadow if true then item should be elevated
|
||||
* */
|
||||
data class Portfolio(
|
||||
override val roundingModeUM: RoundingModeUM = RoundingModeUM.None,
|
||||
val headerRowUM: TangemHeaderRowUM,
|
||||
) : OrganizeRowItemUM() {
|
||||
override val id: String = headerRowUM.id
|
||||
override val isShowShadow: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper item used to detect possible positions where a draggable item can be placed.
|
||||
*
|
||||
* @property id ID of the placeholder for corresponding ID of the group
|
||||
* @property accountId ID of account which contains this placeholder
|
||||
* */
|
||||
data class Placeholder(
|
||||
override val id: String,
|
||||
val accountId: String = "",
|
||||
) : OrganizeRowItemUM() {
|
||||
override val isShowShadow: Boolean = false
|
||||
override val roundingModeUM: RoundingModeUM = RoundingModeUM.None
|
||||
}
|
||||
|
||||
/**
|
||||
* Update item [RoundingModeUM]
|
||||
*
|
||||
* @param mode new [RoundingModeUM]
|
||||
*
|
||||
* @return updated [DraggableItem]
|
||||
* */
|
||||
fun updateRoundingMode(mode: RoundingModeUM): OrganizeRowItemUM = when (this) {
|
||||
is Placeholder -> this
|
||||
is Portfolio -> this.copy(roundingModeUM = mode)
|
||||
is Network -> this.copy(roundingModeUM = mode)
|
||||
is Token -> this.copy(roundingModeUM = mode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update item shadow visibility
|
||||
*
|
||||
* @param show if true then item should be elevated
|
||||
*
|
||||
* @return updated [DraggableItem]
|
||||
* */
|
||||
fun updateShadowVisibility(show: Boolean): OrganizeRowItemUM = when (this) {
|
||||
is Portfolio,
|
||||
is Placeholder,
|
||||
-> this
|
||||
is Network -> this.copy(isShowShadow = show)
|
||||
is Token -> this.copy(isShowShadow = show)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
@Immutable
|
||||
internal data class OrganizeTokensUM(
|
||||
val tokenList: PersistentList<OrganizeRowItemUM>,
|
||||
val organizeMenuUM: OrganizeMenuUM,
|
||||
val isGrouped: Boolean,
|
||||
val isAccountsMode: Boolean,
|
||||
val scrollListToTop: StateEvent<Unit>,
|
||||
val cancelButton: TangemButtonUM,
|
||||
val applyButton: TangemButtonUM,
|
||||
val isBalanceHidden: Boolean,
|
||||
) {
|
||||
|
||||
data class OrganizeMenuUM(
|
||||
val isEnabled: Boolean = false,
|
||||
val isSortedByBalance: Boolean = false,
|
||||
val isGrouped: Boolean = false,
|
||||
val onSortClick: () -> Unit,
|
||||
val onGroupClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ 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.OrganizeRowItemUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM
|
||||
|
||||
internal class CryptoCurrenciesIdsResolver {
|
||||
|
|
@ -35,4 +36,23 @@ internal class CryptoCurrenciesIdsResolver {
|
|||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
fun resolve(tokenList: List<OrganizeRowItemUM>, accountStatusList: AccountStatusList?): AccountCryptoCurrencies {
|
||||
if (accountStatusList == null) return emptyMap()
|
||||
|
||||
val draggableTokens = tokenList.filterIsInstance<OrganizeRowItemUM.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.child.organizetokens.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
|
||||
internal interface OrganizeTokensIntents {
|
||||
|
|
@ -25,6 +26,7 @@ internal interface DragAndDropIntents {
|
|||
fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean
|
||||
|
||||
fun onItemDraggingStartLegacy(item: DraggableItem)
|
||||
fun onItemDraggingStart(item: OrganizeRowItemUM)
|
||||
|
||||
fun onItemDraggingEnd()
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.core.ui.ds.button.TangemButtonType
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.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.models.TokensSortType
|
||||
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
|
||||
import com.tangem.feature.wallet.child.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapter
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.transformer.OrganizeContentStateTransformer
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.transformer.OrganizeDisableBalanceSortingTransformer
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.transformer.OrganizeSortingProgressStateTransformer
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class OrganizeTokensModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
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,
|
||||
) : Model(), OrganizeTokensIntents {
|
||||
|
||||
private val params: OrganizeTokensComponent.Params = paramsContainer.require()
|
||||
|
||||
private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow()
|
||||
|
||||
private val userWalletId = params.userWalletId
|
||||
|
||||
private var cachedAccountStatusList: AccountStatusList? = null
|
||||
|
||||
private var isAccountsModeEnabled: Boolean = false
|
||||
|
||||
val uiState: StateFlow<OrganizeTokensUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
val dragAndDropAdapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
DragAndDropAdapter(uiState)
|
||||
}
|
||||
|
||||
init {
|
||||
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened())
|
||||
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.onEach { balanceSettings ->
|
||||
uiState.update {
|
||||
it.copy(
|
||||
isBalanceHidden = balanceSettings.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
bootstrapTokenList()
|
||||
bootstrapDragAndDropUpdates()
|
||||
}
|
||||
|
||||
override fun onBackClick() {
|
||||
params.callback.onDismiss()
|
||||
}
|
||||
|
||||
override fun onSortClick() {
|
||||
val list = cachedAccountStatusList ?: return
|
||||
if (list.sortType == TokensSortType.BALANCE) return
|
||||
|
||||
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance())
|
||||
|
||||
modelScope.launch {
|
||||
toggleTokenListSortingUseCase(list).fold(
|
||||
ifLeft = {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
organizeMenuUM = it.organizeMenuUM.copy(isEnabled = false),
|
||||
)
|
||||
}
|
||||
},
|
||||
ifRight = { accountStatusList ->
|
||||
uiState.update(
|
||||
OrganizeContentStateTransformer(
|
||||
accountStatusList = accountStatusList,
|
||||
isAccountsMode = isAccountsModeEnabled,
|
||||
appCurrency = selectedAppCurrencyFlow.value,
|
||||
),
|
||||
)
|
||||
cachedAccountStatusList = accountStatusList
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGroupClick() {
|
||||
val list = cachedAccountStatusList ?: return
|
||||
|
||||
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group())
|
||||
|
||||
modelScope.launch {
|
||||
toggleTokenListGroupingUseCase(list).fold(
|
||||
ifLeft = {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
organizeMenuUM = it.organizeMenuUM.copy(isEnabled = false),
|
||||
)
|
||||
}
|
||||
},
|
||||
ifRight = { accountStatusList ->
|
||||
uiState.update(
|
||||
OrganizeContentStateTransformer(
|
||||
accountStatusList = accountStatusList,
|
||||
isAccountsMode = isAccountsModeEnabled,
|
||||
appCurrency = selectedAppCurrencyFlow.value,
|
||||
),
|
||||
)
|
||||
cachedAccountStatusList = accountStatusList
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onApplyClick() {
|
||||
modelScope.launch {
|
||||
uiState.update(OrganizeSortingProgressStateTransformer(true))
|
||||
val resolver = CryptoCurrenciesIdsResolver()
|
||||
val isSortedByBalance = uiState.value.organizeMenuUM.isSortedByBalance
|
||||
val isGroupedByNetwork = uiState.value.isGrouped
|
||||
val tokensListUM = uiState.value.tokenList
|
||||
|
||||
sendAnalyticsEvent(
|
||||
isGroupedByNetwork = isGroupedByNetwork,
|
||||
isSortedByBalance = isSortedByBalance,
|
||||
)
|
||||
|
||||
val result = applyTokenListSortingUseCase(
|
||||
sortedTokensIdsByAccount = resolver.resolve(tokensListUM, cachedAccountStatusList),
|
||||
isGroupedByNetwork = isGroupedByNetwork,
|
||||
isSortedByBalance = isSortedByBalance,
|
||||
)
|
||||
|
||||
result.fold(
|
||||
ifLeft = {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
organizeMenuUM = it.organizeMenuUM.copy(isEnabled = false),
|
||||
)
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
onBackClick()
|
||||
uiState.update(OrganizeSortingProgressStateTransformer(false))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelClick() {
|
||||
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel())
|
||||
onBackClick()
|
||||
}
|
||||
|
||||
private fun bootstrapTokenList() {
|
||||
modelScope.launch {
|
||||
val accountList = singleAccountStatusListSupplier.getSyncOrNull(
|
||||
SingleAccountStatusListProducer.Params(userWalletId),
|
||||
) ?: return@launch
|
||||
|
||||
isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync()
|
||||
|
||||
uiState.update(
|
||||
transformer = OrganizeContentStateTransformer(
|
||||
accountStatusList = accountList,
|
||||
isAccountsMode = isAccountsModeEnabled,
|
||||
appCurrency = selectedAppCurrencyFlow.value,
|
||||
),
|
||||
)
|
||||
|
||||
cachedAccountStatusList = accountList
|
||||
}
|
||||
}
|
||||
|
||||
private fun bootstrapDragAndDropUpdates() {
|
||||
dragAndDropAdapter.dragAndDropUpdates
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.onEach { (type, updatedTokenList) ->
|
||||
disableSortingByBalanceIfListChanged(type)
|
||||
uiState.update { it.copy(tokenList = updatedTokenList) }
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) {
|
||||
if (dragOperationType !is DragAndDropAdapter.DragOperation.Type.End) return
|
||||
|
||||
if (uiState.value.organizeMenuUM.isSortedByBalance && dragOperationType.isItemsOrderChanged) {
|
||||
cachedAccountStatusList = cachedAccountStatusList?.copy(sortType = TokensSortType.NONE)
|
||||
uiState.update(OrganizeDisableBalanceSortingTransformer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendAnalyticsEvent(isGroupedByNetwork: Boolean, isSortedByBalance: Boolean) {
|
||||
analyticsEventsHandler.send(
|
||||
PortfolioOrganizeTokensAnalyticsEvent.Apply(
|
||||
grouping = if (isGroupedByNetwork) {
|
||||
AnalyticsParam.OnOffState.On
|
||||
} else {
|
||||
AnalyticsParam.OnOffState.Off
|
||||
},
|
||||
organizeSortType = if (isSortedByBalance) {
|
||||
AnalyticsParam.OrganizeSortType.ByBalance
|
||||
} else {
|
||||
AnalyticsParam.OrganizeSortType.Manually
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInitialState(): OrganizeTokensUM {
|
||||
return OrganizeTokensUM(
|
||||
tokenList = persistentListOf(),
|
||||
organizeMenuUM = OrganizeTokensUM.OrganizeMenuUM(
|
||||
onSortClick = ::onSortClick,
|
||||
onGroupClick = ::onGroupClick,
|
||||
),
|
||||
cancelButton = TangemButtonUM(
|
||||
text = resourceReference(R.string.common_cancel),
|
||||
onClick = ::onCancelClick,
|
||||
type = TangemButtonType.Secondary,
|
||||
),
|
||||
applyButton = TangemButtonUM(
|
||||
text = resourceReference(R.string.common_apply),
|
||||
onClick = ::onApplyClick,
|
||||
type = TangemButtonType.Primary,
|
||||
),
|
||||
scrollListToTop = consumedEvent(),
|
||||
isBalanceHidden = true,
|
||||
isGrouped = false,
|
||||
isAccountsMode = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +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.OrganizeRowItemUM
|
||||
|
||||
internal fun getGroupPlaceholderLegacy(index: Int, accountId: String = ""): DraggableItem.Placeholder {
|
||||
return DraggableItem.Placeholder(
|
||||
id = "placeholder_${accountId}_${index.inc()}",
|
||||
accountId = accountId,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun getGroupPlaceholder(index: Int, accountId: String = ""): OrganizeRowItemUM.Placeholder {
|
||||
return OrganizeRowItemUM.Placeholder(
|
||||
id = "placeholder_${accountId}_${index.inc()}",
|
||||
accountId = accountId,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
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.OrganizeRowItemUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM
|
||||
|
||||
internal fun List<DraggableItem>.uniteItemsLegacy(isAccountsMode: Boolean): List<DraggableItem> {
|
||||
|
|
@ -45,6 +46,48 @@ internal fun List<DraggableItem>.uniteItemsLegacy(isAccountsMode: Boolean): List
|
|||
}.toList()
|
||||
}
|
||||
|
||||
internal fun List<OrganizeRowItemUM>.uniteItems(isAccountsMode: Boolean): List<OrganizeRowItemUM> {
|
||||
val items = this
|
||||
val lastItemIndex = items.lastIndex
|
||||
|
||||
return items
|
||||
.asSequence()
|
||||
.mapIndexed { index, item ->
|
||||
val mode = when (index) {
|
||||
0 -> if (item is OrganizeRowItemUM.Placeholder) {
|
||||
RoundingModeUM.None
|
||||
} else {
|
||||
RoundingModeUM.Top()
|
||||
}
|
||||
lastItemIndex -> RoundingModeUM.Bottom()
|
||||
1 -> if (items.first() is OrganizeRowItemUM.Placeholder) {
|
||||
RoundingModeUM.Top()
|
||||
} else {
|
||||
RoundingModeUM.None
|
||||
}
|
||||
else -> when (item) {
|
||||
is OrganizeRowItemUM.Placeholder -> RoundingModeUM.None
|
||||
is OrganizeRowItemUM.Network -> if (isAccountsMode) {
|
||||
RoundingModeUM.None
|
||||
} else {
|
||||
RoundingModeUM.Top(isShowGap = true)
|
||||
}
|
||||
is OrganizeRowItemUM.Token -> applyRoundingModeToToken(
|
||||
isAccountsMode = isAccountsMode,
|
||||
items = items,
|
||||
index = index,
|
||||
lastItemIndex = lastItemIndex,
|
||||
)
|
||||
is OrganizeRowItemUM.Portfolio -> RoundingModeUM.Top(isShowGap = true)
|
||||
}
|
||||
}
|
||||
|
||||
item
|
||||
.updateRoundingMode(mode)
|
||||
.updateShadowVisibility(show = false)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
internal fun List<DraggableItem>.divideMovingItem(movingItem: DraggableItem): List<DraggableItem> {
|
||||
val mutableList = this.toMutableList()
|
||||
val listIterator = mutableList.listIterator()
|
||||
|
|
@ -110,4 +153,51 @@ private fun applyRoundingModeToTokenLegacy(
|
|||
RoundingModeUM.Bottom(isShowGap = true)
|
||||
}
|
||||
else -> RoundingModeUM.None
|
||||
}
|
||||
|
||||
/**
|
||||
* Applying rounding to tokens
|
||||
*
|
||||
* If is in accounts mode without grouping
|
||||
* * PORTFOLIO
|
||||
* * TOKEN
|
||||
* * TOKEN <- add rounding
|
||||
* * PORTFOLIO index + 1 is PORTFOLIO
|
||||
*
|
||||
* If is in accounts mode with grouping
|
||||
* * PORTFOLIO
|
||||
* * PLACEHOLDER
|
||||
* * GROUPING
|
||||
* * TOKEN
|
||||
* * TOKEN <- add rounding
|
||||
* * PLACEHOLDER index + 1 is PLACEHOLDER
|
||||
* * PORTFOLIO index + 2 is PORTFOLIO
|
||||
* * PLACEHOLDER
|
||||
*
|
||||
* If is not accounts mode without grouping
|
||||
* * TOKEN
|
||||
* * TOKEN <- add rounding
|
||||
*
|
||||
* If is not accounts mode with grouping
|
||||
* * PLACEHOLDER
|
||||
* * GROUPING
|
||||
* * TOKEN
|
||||
* * TOKEN <- add rounding
|
||||
* * PLACEHOLDER index + 1 is PLACEHOLDER
|
||||
*/
|
||||
private fun applyRoundingModeToToken(
|
||||
isAccountsMode: Boolean,
|
||||
items: List<OrganizeRowItemUM>,
|
||||
index: Int,
|
||||
lastItemIndex: Int,
|
||||
) = when {
|
||||
isAccountsMode && index + 1 < lastItemIndex &&
|
||||
(items[index + 1] is OrganizeRowItemUM.Portfolio ||
|
||||
items[index + 1] is OrganizeRowItemUM.Placeholder && items[index + 2] is OrganizeRowItemUM.Portfolio) -> {
|
||||
RoundingModeUM.Bottom(isShowGap = true)
|
||||
}
|
||||
(!isAccountsMode || index + 1 == lastItemIndex) && items[index + 1] is OrganizeRowItemUM.Placeholder -> {
|
||||
RoundingModeUM.Bottom(isShowGap = true)
|
||||
}
|
||||
else -> RoundingModeUM.None
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.converter
|
||||
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
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.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
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.OrganizeAccountItemConverter
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizeNetworkItemConverter
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizeTokenItemConverter
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.addIf
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class OrganizeTokensListConverter(
|
||||
private val isAccountsMode: Boolean,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<AccountStatusList, PersistentList<OrganizeRowItemUM>> {
|
||||
|
||||
private val accountItemConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OrganizeAccountItemConverter(appCurrency)
|
||||
}
|
||||
|
||||
private val tokenItemConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OrganizeTokenItemConverter(appCurrency)
|
||||
}
|
||||
|
||||
override fun convert(value: AccountStatusList): PersistentList<OrganizeRowItemUM> {
|
||||
return value.accountStatuses
|
||||
.asSequence()
|
||||
.filterCryptoPortfolio()
|
||||
.flatMap { accountStatus ->
|
||||
buildList {
|
||||
addIf(
|
||||
condition = isAccountsMode,
|
||||
create = { accountItemConverter.convert(accountStatus) },
|
||||
)
|
||||
when (val tokenList = accountStatus.tokenList) {
|
||||
is TokenList.Ungrouped -> addAll(
|
||||
elements = tokenItemConverter.convertList(
|
||||
input = tokenList.currencies.mapToAccountCryptoCurrencyStatus(accountStatus),
|
||||
),
|
||||
)
|
||||
is TokenList.GroupedByNetwork -> {
|
||||
addIf(
|
||||
condition = isAccountsMode,
|
||||
create = { getGroupPlaceholder(-1, accountStatus.accountId.value) },
|
||||
)
|
||||
tokenList.groups.asSequence().forEachIndexed { index, (groupNetwork, currencies) ->
|
||||
add(OrganizeNetworkItemConverter.convert(accountStatus.accountId to groupNetwork))
|
||||
addAll(
|
||||
elements = tokenItemConverter.convertList(
|
||||
input = currencies.mapToAccountCryptoCurrencyStatus(accountStatus),
|
||||
),
|
||||
)
|
||||
add(getGroupPlaceholder(index, accountStatus.accountId.value))
|
||||
}
|
||||
}
|
||||
TokenList.Empty -> Unit
|
||||
}
|
||||
}
|
||||
}.toList()
|
||||
.uniteItems(isAccountsMode).toPersistentList()
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.mapToAccountCryptoCurrencyStatus(
|
||||
accountStatus: AccountStatus.CryptoPortfolio,
|
||||
): List<AccountCryptoCurrencyStatus> {
|
||||
return map { cryptoCurrencyStatus ->
|
||||
AccountCryptoCurrencyStatus(
|
||||
account = accountStatus.account,
|
||||
status = cryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.converter.items
|
||||
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
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.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class OrganizeAccountItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<AccountStatus.CryptoPortfolio, OrganizeRowItemUM.Portfolio> {
|
||||
|
||||
override fun convert(value: AccountStatus.CryptoPortfolio): OrganizeRowItemUM.Portfolio {
|
||||
val accountBalance = value.tokenList.totalFiatBalance as? TotalFiatBalance.Loaded
|
||||
return OrganizeRowItemUM.Portfolio(
|
||||
headerRowUM = TangemHeaderRowUM(
|
||||
id = value.accountId.value,
|
||||
title = value.account.accountName.toUM().value,
|
||||
subtitle = stringReference(
|
||||
accountBalance?.amount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.converter.items
|
||||
|
||||
import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM
|
||||
import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object OrganizeNetworkItemConverter : Converter<Pair<AccountId, Network>, OrganizeRowItemUM.Network> {
|
||||
|
||||
override fun convert(value: Pair<AccountId, Network>): OrganizeRowItemUM.Network {
|
||||
val (accountId, groupNetwork) = value
|
||||
return OrganizeRowItemUM.Network(
|
||||
headerRowUM = TangemHeaderRowUM(
|
||||
id = groupNetwork.id.toString(),
|
||||
title = stringReference(groupNetwork.name),
|
||||
tailUM = TangemRowTailUM.Draggable(R.drawable.ic_group_drop_24),
|
||||
),
|
||||
accountId = accountId.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.converter.items
|
||||
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import com.tangem.common.getTotalCryptoAmount
|
||||
import com.tangem.common.getTotalFiatAmount
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.cryptoStyled
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.formatStyled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.common.getTokenItemId
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class OrganizeTokenItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<AccountCryptoCurrencyStatus, OrganizeRowItemUM.Token> {
|
||||
|
||||
private val iconStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
CryptoCurrencyToIconStateConverter()
|
||||
}
|
||||
|
||||
override fun convert(value: AccountCryptoCurrencyStatus): OrganizeRowItemUM.Token {
|
||||
val (account, currencyStatus) = value
|
||||
val currency = currencyStatus.currency
|
||||
|
||||
return OrganizeRowItemUM.Token(
|
||||
tokenRowUM = TangemTokenRowUM.Actionable(
|
||||
id = getTokenItemId(currency.id),
|
||||
headIconUM = TangemIconUM.Currency(iconStateConverter.convert(currencyStatus)),
|
||||
titleUM = TangemTokenRowUM.TitleUM.Content(
|
||||
text = stringReference(currency.name),
|
||||
),
|
||||
subtitleUM = TangemTokenRowUM.SubtitleUM.Empty,
|
||||
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
|
||||
text = currencyStatus.getTotalFiatAmount().formatStyled {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
|
||||
)
|
||||
},
|
||||
),
|
||||
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
|
||||
text = currencyStatus.getTotalCryptoAmount().formatStyled {
|
||||
cryptoStyled(
|
||||
cryptoCurrency = currency,
|
||||
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
|
||||
)
|
||||
},
|
||||
),
|
||||
tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
),
|
||||
groupId = currency.network.id.toString(),
|
||||
accountId = account.accountId.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.dnd
|
||||
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
|
||||
internal class DragAndDropAdapter(
|
||||
private val organizeTokensUMFlow: StateFlow<OrganizeTokensUM>,
|
||||
) : DragAndDropIntents {
|
||||
|
||||
private var draggingItem: OrganizeRowItemUM? = null
|
||||
private var draggingListState: OrganizeTokensUM? = null
|
||||
|
||||
private val tokensUM: OrganizeTokensUM
|
||||
get() = organizeTokensUMFlow.value
|
||||
|
||||
private val draggableGroupsOperations by lazy(LazyThreadSafetyMode.NONE) {
|
||||
DraggableGroupsOperations()
|
||||
}
|
||||
|
||||
val dragAndDropUpdates: StateFlow<DragOperation?>
|
||||
field = MutableStateFlow(value = null)
|
||||
|
||||
override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean {
|
||||
val tokensListUM = tokensUM.tokenList
|
||||
|
||||
val (dragOverItem, draggingItem) = findItemsToMove(
|
||||
items = tokensListUM,
|
||||
moveOverItemKey = dragOver.key,
|
||||
movedItemKey = dragging.key,
|
||||
)
|
||||
|
||||
if (dragOverItem == null || draggingItem == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val canDrag = when (draggingItem) {
|
||||
is OrganizeRowItemUM.Network -> checkCanMoveHeaderOver(
|
||||
item = draggingItem,
|
||||
moveOverItem = dragOverItem,
|
||||
)
|
||||
is OrganizeRowItemUM.Token -> checkCanMoveTokenOver(
|
||||
item = draggingItem,
|
||||
moveOverItem = dragOverItem,
|
||||
isAccountsMode = tokensUM.isAccountsMode,
|
||||
isGrouped = tokensUM.isGrouped,
|
||||
)
|
||||
is OrganizeRowItemUM.Placeholder,
|
||||
is OrganizeRowItemUM.Portfolio,
|
||||
-> false
|
||||
}
|
||||
|
||||
return canDrag
|
||||
}
|
||||
|
||||
override fun onItemDraggingStartLegacy(item: DraggableItem) {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
override fun onItemDraggingStart(item: OrganizeRowItemUM) {
|
||||
if (draggingItem != null) return
|
||||
draggingItem = item
|
||||
|
||||
dragAndDropUpdates.value = DragOperation(
|
||||
type = DragOperation.Type.Start,
|
||||
tokenList = when (item) {
|
||||
is OrganizeRowItemUM.Placeholder,
|
||||
is OrganizeRowItemUM.Portfolio,
|
||||
-> tokensUM.tokenList
|
||||
is OrganizeRowItemUM.Network -> draggableGroupsOperations
|
||||
.collapseGroup(tokensUM.tokenList, item)
|
||||
.divideMovingItem(item)
|
||||
is OrganizeRowItemUM.Token -> tokensUM.tokenList.divideMovingItem(item)
|
||||
}.toPersistentList(),
|
||||
)
|
||||
|
||||
draggingListState = tokensUM
|
||||
}
|
||||
|
||||
override fun onItemDraggingEnd() {
|
||||
val draggingItem = draggingItem ?: return
|
||||
|
||||
dragAndDropUpdates.value = DragOperation(
|
||||
type = DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged(tokensUM)),
|
||||
tokenList = when (draggingItem) {
|
||||
is OrganizeRowItemUM.Network -> draggableGroupsOperations
|
||||
.expandGroups(tokensUM.tokenList, tokensUM.isAccountsMode)
|
||||
.uniteItems(tokensUM.isAccountsMode)
|
||||
is OrganizeRowItemUM.Token -> tokensUM.tokenList.uniteItems(tokensUM.isAccountsMode)
|
||||
is OrganizeRowItemUM.Placeholder,
|
||||
is OrganizeRowItemUM.Portfolio,
|
||||
-> tokensUM.tokenList
|
||||
}.toPersistentList(),
|
||||
)
|
||||
|
||||
this.draggingItem = null
|
||||
}
|
||||
|
||||
override fun onItemDragged(from: ItemPosition, to: ItemPosition) {
|
||||
dragAndDropUpdates.value = DragOperation(
|
||||
type = DragOperation.Type.Dragged,
|
||||
tokenList = tokensUM.tokenList.mutate {
|
||||
it.add(to.index, it.removeAt(from.index))
|
||||
}.toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun findItemsToMove(
|
||||
items: List<OrganizeRowItemUM>,
|
||||
moveOverItemKey: Any?,
|
||||
movedItemKey: Any?,
|
||||
): Pair<OrganizeRowItemUM?, OrganizeRowItemUM?> {
|
||||
var moveOverItem: OrganizeRowItemUM? = null
|
||||
var movedItem: OrganizeRowItemUM? = null
|
||||
|
||||
for (item in items) {
|
||||
if (item.id == moveOverItemKey) {
|
||||
moveOverItem = item
|
||||
}
|
||||
if (item.id == movedItemKey) {
|
||||
movedItem = item
|
||||
}
|
||||
if (moveOverItem != null && movedItem != null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(moveOverItem, movedItem)
|
||||
}
|
||||
|
||||
private fun checkCanMoveHeaderOver(item: OrganizeRowItemUM.Network, moveOverItem: OrganizeRowItemUM) =
|
||||
when (moveOverItem) {
|
||||
// Header can be moved only in its account
|
||||
is OrganizeRowItemUM.Placeholder -> item.accountId == moveOverItem.accountId
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun checkCanMoveTokenOver(
|
||||
item: OrganizeRowItemUM.Token,
|
||||
moveOverItem: OrganizeRowItemUM,
|
||||
isGrouped: Boolean,
|
||||
isAccountsMode: Boolean,
|
||||
): Boolean {
|
||||
return when (moveOverItem) {
|
||||
is OrganizeRowItemUM.Network -> false // Token item can not be moved to group item
|
||||
is OrganizeRowItemUM.Token -> when {
|
||||
// Token item can be moved only in its group
|
||||
isGrouped -> item.groupId == moveOverItem.groupId
|
||||
|
||||
// Token item can be moved only in its account
|
||||
isAccountsMode -> item.accountId == moveOverItem.accountId
|
||||
|
||||
// If ungrouped and not accounts mode then item can be moved anywhere
|
||||
else -> true
|
||||
}
|
||||
is OrganizeRowItemUM.Portfolio,
|
||||
is OrganizeRowItemUM.Placeholder,
|
||||
-> false // Token item can not be moved to portfolio or placeholder
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkIsItemsOrderChanged(tokensUM: OrganizeTokensUM): Boolean {
|
||||
fun OrganizeTokensUM?.getItemsIds(): List<Any>? = this?.tokenList?.mapNotNull { item ->
|
||||
if (item is OrganizeRowItemUM.Placeholder) {
|
||||
null
|
||||
} else {
|
||||
item.id
|
||||
}
|
||||
}
|
||||
|
||||
return tokensUM.getItemsIds() != draggingListState.getItemsIds()
|
||||
}
|
||||
|
||||
private fun List<OrganizeRowItemUM>.divideMovingItem(movingItem: OrganizeRowItemUM): List<OrganizeRowItemUM> {
|
||||
val mutableList = this.toMutableList()
|
||||
val listIterator = mutableList.listIterator()
|
||||
|
||||
while (listIterator.hasNext()) {
|
||||
val item = listIterator.next()
|
||||
|
||||
if (item.id == movingItem.id) {
|
||||
val dividedItem = movingItem
|
||||
.updateRoundingMode(RoundingModeUM.All())
|
||||
.updateShadowVisibility(show = true)
|
||||
|
||||
listIterator.set(dividedItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return mutableList
|
||||
}
|
||||
|
||||
data class DragOperation(
|
||||
val type: Type,
|
||||
val tokenList: PersistentList<OrganizeRowItemUM>,
|
||||
) {
|
||||
|
||||
sealed class Type {
|
||||
|
||||
data object Start : Type()
|
||||
|
||||
data object Dragged : Type()
|
||||
|
||||
data class End(val isItemsOrderChanged: Boolean) : Type()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.dnd
|
||||
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
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
|
||||
|
|
@ -90,6 +91,8 @@ internal class DragAndDropAdapterLegacy(
|
|||
draggingListState = tokenListUM
|
||||
}
|
||||
|
||||
override fun onItemDraggingStart(item: OrganizeRowItemUM) { /* no-op */ }
|
||||
|
||||
override fun onItemDraggingEnd() {
|
||||
val draggingItem = draggingItem ?: return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.dnd
|
||||
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholderLegacy
|
||||
|
||||
internal class DraggableGroupsOperations {
|
||||
|
||||
private var groupIdToTokens: Map<String, List<OrganizeRowItemUM.Token>>? = null
|
||||
private var groupIdToTokensLegacy: Map<Int, List<DraggableItem.Token>>? = null
|
||||
|
||||
fun collapseGroupLegacy(items: List<DraggableItem>, movingGroup: DraggableItem.GroupHeader): List<DraggableItem> {
|
||||
|
|
@ -67,4 +70,67 @@ internal class DraggableGroupsOperations {
|
|||
|
||||
return expandedGroups
|
||||
}
|
||||
|
||||
fun expandGroups(items: List<OrganizeRowItemUM>, isAccountsMode: Boolean): List<OrganizeRowItemUM> {
|
||||
if (groupIdToTokens.isNullOrEmpty()) return items
|
||||
|
||||
val accountList = items.filterIsInstance<OrganizeRowItemUM.Portfolio>()
|
||||
val currentGroups = items.filterIsInstance<OrganizeRowItemUM.Network>()
|
||||
|
||||
val expandedGroups = if (isAccountsMode) {
|
||||
accountList
|
||||
.asSequence()
|
||||
.flatMap { account ->
|
||||
buildList {
|
||||
add(account)
|
||||
currentGroups
|
||||
.asSequence()
|
||||
.filter { it.accountId == account.id }
|
||||
.forEachIndexed { index, group ->
|
||||
if (index == 0) {
|
||||
add(getGroupPlaceholder(accountId = group.accountId, index = -1))
|
||||
}
|
||||
add(group)
|
||||
addAll(groupIdToTokens?.get(group.id).orEmpty())
|
||||
add(getGroupPlaceholder(accountId = group.accountId, index = index))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentGroups
|
||||
.asSequence()
|
||||
.flatMapIndexed { index, group ->
|
||||
buildList {
|
||||
if (index == 0) {
|
||||
add(getGroupPlaceholder(accountId = group.accountId, index = -1))
|
||||
}
|
||||
add(group)
|
||||
addAll(groupIdToTokens?.get(group.id).orEmpty())
|
||||
add(getGroupPlaceholder(accountId = group.accountId, index = index))
|
||||
}
|
||||
}
|
||||
}.toList()
|
||||
|
||||
groupIdToTokens = null
|
||||
|
||||
return expandedGroups
|
||||
}
|
||||
|
||||
fun collapseGroup(
|
||||
items: List<OrganizeRowItemUM>,
|
||||
movingGroup: OrganizeRowItemUM.Network,
|
||||
): List<OrganizeRowItemUM> {
|
||||
if (!groupIdToTokens.isNullOrEmpty()) return items
|
||||
|
||||
groupIdToTokens = items
|
||||
.asSequence()
|
||||
.filterIsInstance<OrganizeRowItemUM.Token>()
|
||||
.groupBy { it.groupId }
|
||||
|
||||
val itemsWithoutGroupTokens = items.filterNot {
|
||||
it is OrganizeRowItemUM.Token && it.groupId == movingGroup.id
|
||||
}
|
||||
|
||||
return itemsWithoutGroupTokens
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.transformer
|
||||
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.converter.OrganizeTokensListConverter
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class OrganizeContentStateTransformer(
|
||||
private val accountStatusList: AccountStatusList,
|
||||
private val isAccountsMode: Boolean,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Transformer<OrganizeTokensUM> {
|
||||
|
||||
private val tokenListConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OrganizeTokensListConverter(
|
||||
isAccountsMode = isAccountsMode,
|
||||
appCurrency = appCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
override fun transform(prevState: OrganizeTokensUM): OrganizeTokensUM {
|
||||
val isGrouping = accountStatusList.groupType == TokensGroupType.NETWORK
|
||||
val isSortedByBalance = accountStatusList.sortType == TokensSortType.BALANCE
|
||||
|
||||
return prevState.copy(
|
||||
isGrouped = isGrouping,
|
||||
isAccountsMode = isAccountsMode,
|
||||
tokenList = tokenListConverter.convert(value = accountStatusList),
|
||||
organizeMenuUM = prevState.organizeMenuUM.copy(
|
||||
isEnabled = prevState.tokenList.isNotEmpty(),
|
||||
isSortedByBalance = isSortedByBalance,
|
||||
isGrouped = isGrouping,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.transformer
|
||||
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal object OrganizeDisableBalanceSortingTransformer : Transformer<OrganizeTokensUM> {
|
||||
override fun transform(prevState: OrganizeTokensUM): OrganizeTokensUM {
|
||||
return prevState.copy(
|
||||
organizeMenuUM = prevState.organizeMenuUM.copy(
|
||||
isSortedByBalance = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.model.transformer
|
||||
|
||||
import com.tangem.core.ui.ds.button.TangemButtonState
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class OrganizeSortingProgressStateTransformer(
|
||||
private val isSortingInProgress: Boolean,
|
||||
) : Transformer<OrganizeTokensUM> {
|
||||
override fun transform(prevState: OrganizeTokensUM): OrganizeTokensUM {
|
||||
return prevState.copy(
|
||||
organizeMenuUM = prevState.organizeMenuUM.copy(
|
||||
isEnabled = !isSortingInProgress,
|
||||
),
|
||||
cancelButton = prevState.cancelButton.copy(
|
||||
state = if (isSortingInProgress) {
|
||||
TangemButtonState.Disabled
|
||||
} else {
|
||||
TangemButtonState.Default
|
||||
},
|
||||
),
|
||||
applyButton = prevState.applyButton.copy(
|
||||
state = if (isSortingInProgress) {
|
||||
TangemButtonState.Loading
|
||||
} else {
|
||||
TangemButtonState.Default
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.ui
|
||||
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds.contextmenu.TangemContextMenu
|
||||
import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun OrganizeDropDownMenu(
|
||||
organizeMenuUM: OrganizeTokensUM.OrganizeMenuUM,
|
||||
showDropdownMenu: Boolean,
|
||||
onDropdownDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TangemContextMenu(
|
||||
expanded = showDropdownMenu,
|
||||
onDismissRequest = onDropdownDismiss,
|
||||
offset = DpOffset.Zero,
|
||||
modifier = modifier,
|
||||
) {
|
||||
TangemContextMenuCheckboxItem(
|
||||
title = TextReference.Res(R.string.organize_tokens_sort_by_balance),
|
||||
isChecked = organizeMenuUM.isSortedByBalance,
|
||||
onClick = organizeMenuUM.onSortClick,
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = TangemTheme.colors2.border.neutral.quaternary,
|
||||
)
|
||||
TangemContextMenuCheckboxItem(
|
||||
title = TextReference.Res(R.string.organize_tokens_group),
|
||||
isChecked = organizeMenuUM.isGrouped,
|
||||
onClick = organizeMenuUM.onGroupClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
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.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.ds.button.TangemButton
|
||||
import com.tangem.core.ui.ds.row.header.TangemHeaderRow
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRow
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarActionContent
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.reordarable.ReorderableItem
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
|
||||
import com.tangem.core.ui.utils.lazyListItemPosition
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents
|
||||
import com.tangem.feature.wallet.child.organizetokens.ui.preview.OrganizeTokensPreview
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
|
||||
@Composable
|
||||
internal fun OrganizeTokensContent(
|
||||
organizeTokensUM: OrganizeTokensUM,
|
||||
dragAndDropIntents: DragAndDropIntents,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var isShowDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val hazeState = rememberHazeState()
|
||||
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = onDismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
addBottomInsets = false,
|
||||
containerColor = TangemTheme.colors2.surface.level2,
|
||||
title = {
|
||||
TangemTopBar(
|
||||
title = resourceReference(R.string.organize_tokens_title),
|
||||
endContent = {
|
||||
TangemTopBarActionContent(
|
||||
actionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_exchange_mini_24,
|
||||
isActionable = true,
|
||||
onClick = { isShowDropdownMenu = true },
|
||||
ghostModeProgress = 0f,
|
||||
),
|
||||
iconSize = TangemTheme.dimens2.x7,
|
||||
)
|
||||
OrganizeDropDownMenu(
|
||||
organizeMenuUM = organizeTokensUM.organizeMenuUM,
|
||||
showDropdownMenu = isShowDropdownMenu,
|
||||
onDropdownDismiss = { isShowDropdownMenu = false },
|
||||
modifier = Modifier.hazeEffectTangem(hazeState),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
content = {
|
||||
TokenList(
|
||||
organizeTokensUM = organizeTokensUM,
|
||||
dragAndDropIntents = dragAndDropIntents,
|
||||
modifier = Modifier.hazeSourceTangem(hazeState),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun TokenList(
|
||||
organizeTokensUM: OrganizeTokensUM,
|
||||
dragAndDropIntents: DragAndDropIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val tokensListState = rememberLazyListState()
|
||||
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val tokenList = organizeTokensUM.tokenList
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors2.surface.level2),
|
||||
) {
|
||||
val onDragEnd: (Int, Int) -> Unit = remember {
|
||||
{ _, _ ->
|
||||
dragAndDropIntents.onItemDraggingEnd()
|
||||
}
|
||||
}
|
||||
val reorderableListState = rememberReorderableLazyListState(
|
||||
onMove = dragAndDropIntents::onItemDragged,
|
||||
listState = tokensListState,
|
||||
canDragOver = dragAndDropIntents::canDragItemOver,
|
||||
onDragEnd = onDragEnd,
|
||||
)
|
||||
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
val listContentPadding = PaddingValues(
|
||||
top = TangemTheme.dimens2.x1,
|
||||
bottom = TangemTheme.dimens2.x1 + bottomBarHeight,
|
||||
start = TangemTheme.dimens2.x3,
|
||||
end = TangemTheme.dimens2.x3,
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.reorderable(reorderableListState)
|
||||
.testTag(OrganizeTokensScreenTestTags.TOKENS_LAZY_LIST)
|
||||
.hazeSourceTangem(zIndex = 1f),
|
||||
state = reorderableListState.listState,
|
||||
contentPadding = listContentPadding,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = tokenList,
|
||||
key = { _, item -> item.id },
|
||||
) { index, item ->
|
||||
|
||||
val onDragStart = remember(item) {
|
||||
{
|
||||
dragAndDropIntents.onItemDraggingStart(item)
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
}
|
||||
|
||||
DraggableItem(
|
||||
index = index,
|
||||
item = item,
|
||||
reorderableState = reorderableListState,
|
||||
onDragStart = onDragStart,
|
||||
isBalanceHidden = organizeTokensUM.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BottomFade(
|
||||
gradientBrush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
TangemTheme.colors2.surface.level2.copy(0.9f),
|
||||
TangemTheme.colors2.surface.level2,
|
||||
),
|
||||
),
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
|
||||
BottomButtons(organizeTokensUM = organizeTokensUM)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyItemScope.DraggableItem(
|
||||
index: Int,
|
||||
item: OrganizeRowItemUM,
|
||||
reorderableState: ReorderableLazyListState,
|
||||
onDragStart: () -> Unit,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
var isDragging by remember {
|
||||
mutableStateOf(value = false)
|
||||
}
|
||||
|
||||
val itemModifier = Modifier.applyShapeAndShadow(item.roundingModeUM, item.isShowShadow)
|
||||
|
||||
ReorderableItem(
|
||||
reorderableState = reorderableState,
|
||||
index = index,
|
||||
key = item.id,
|
||||
) { isItemDragging ->
|
||||
isDragging = isItemDragging
|
||||
|
||||
val modifierWithBackground = itemModifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.semantics { lazyListItemPosition = index }
|
||||
|
||||
when (item) {
|
||||
is OrganizeRowItemUM.Network -> TangemHeaderRow(
|
||||
modifier = modifierWithBackground,
|
||||
reorderableState = reorderableState,
|
||||
headerRowUM = item.headerRowUM,
|
||||
)
|
||||
is OrganizeRowItemUM.Portfolio -> TangemHeaderRow(
|
||||
modifier = modifierWithBackground,
|
||||
headerRowUM = item.headerRowUM,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
is OrganizeRowItemUM.Token -> TangemTokenRow(
|
||||
modifier = modifierWithBackground,
|
||||
tokenRowUM = item.tokenRowUM,
|
||||
reorderableState = reorderableState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
// Should be presented in the list but remain invisible
|
||||
is OrganizeRowItemUM.Placeholder -> Box(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(isDragging) {
|
||||
onDispose {
|
||||
if (isDragging) {
|
||||
onDragStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.BottomButtons(organizeTokensUM: OrganizeTokensUM) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(horizontal = TangemTheme.dimens2.x4)
|
||||
.padding(bottom = bottomBarHeight + TangemTheme.dimens2.x4),
|
||||
) {
|
||||
TangemButton(
|
||||
buttonUM = organizeTokensUM.cancelButton,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
TangemButton(
|
||||
buttonUM = organizeTokensUM.applyButton,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.applyShapeAndShadow(roundingMode: RoundingModeUM, showShadow: Boolean): Modifier {
|
||||
return composed {
|
||||
val radius by animateDpAsState(
|
||||
targetValue = when (roundingMode) {
|
||||
is RoundingModeUM.None -> TangemTheme.dimens2.x0
|
||||
is RoundingModeUM.All -> TangemTheme.dimens2.x3
|
||||
is RoundingModeUM.Bottom,
|
||||
is RoundingModeUM.Top,
|
||||
-> TangemTheme.dimens2.x4
|
||||
},
|
||||
label = "item_shape_radius",
|
||||
)
|
||||
val elevation by animateDpAsState(
|
||||
targetValue = if (showShadow) {
|
||||
TangemTheme.dimens2.x2
|
||||
} else {
|
||||
TangemTheme.dimens2.x0
|
||||
},
|
||||
label = "item_elevation",
|
||||
)
|
||||
|
||||
this
|
||||
.padding(paddingValues = getItemGap(roundingMode))
|
||||
.shadow(
|
||||
elevation = elevation,
|
||||
shape = getItemShape(roundingMode, radius),
|
||||
clip = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getItemGap(roundingMode: RoundingModeUM): PaddingValues {
|
||||
val paddingValue = TangemTheme.dimens2.x1
|
||||
|
||||
return if (roundingMode.isShowGap) {
|
||||
when (roundingMode) {
|
||||
is RoundingModeUM.None -> PaddingValues(all = TangemTheme.dimens2.x0)
|
||||
is RoundingModeUM.All -> PaddingValues(vertical = paddingValue)
|
||||
is RoundingModeUM.Top -> PaddingValues(top = paddingValue)
|
||||
is RoundingModeUM.Bottom -> PaddingValues(bottom = paddingValue)
|
||||
}
|
||||
} else {
|
||||
PaddingValues(all = TangemTheme.dimens2.x0)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
private fun getItemShape(roundingMode: RoundingModeUM, radius: Dp): Shape {
|
||||
return when (roundingMode) {
|
||||
is RoundingModeUM.None -> RectangleShape
|
||||
is RoundingModeUM.Top -> RoundedCornerShape(
|
||||
topStart = radius,
|
||||
topEnd = radius,
|
||||
)
|
||||
is RoundingModeUM.Bottom -> RoundedCornerShape(
|
||||
bottomStart = radius,
|
||||
bottomEnd = radius,
|
||||
)
|
||||
is RoundingModeUM.All -> RoundedCornerShape(
|
||||
size = radius,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun OrganizeTokensContent_Preview(
|
||||
@PreviewParameter(OrganizeTokensContentPreviewProvider::class) params: OrganizeTokensUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
OrganizeTokensContent(
|
||||
organizeTokensUM = params,
|
||||
dragAndDropIntents = object : DragAndDropIntents {
|
||||
override fun onItemDragged(from: ItemPosition, to: ItemPosition) {}
|
||||
|
||||
override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean = false
|
||||
|
||||
override fun onItemDraggingStartLegacy(item: DraggableItem) {}
|
||||
|
||||
override fun onItemDraggingStart(item: OrganizeRowItemUM) {}
|
||||
|
||||
override fun onItemDraggingEnd() {}
|
||||
},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class OrganizeTokensContentPreviewProvider : PreviewParameterProvider<OrganizeTokensUM> {
|
||||
override val values: Sequence<OrganizeTokensUM>
|
||||
get() = sequenceOf(
|
||||
OrganizeTokensPreview.defaultState,
|
||||
OrganizeTokensPreview.defaultState.copy(isGrouped = false),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package com.tangem.feature.wallet.child.organizetokens.ui.preview
|
||||
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.button.TangemButtonType
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM
|
||||
import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
|
||||
import com.tangem.feature.wallet.child.organizetokens.entity.RoundingModeUM
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.util.UUID
|
||||
|
||||
internal object OrganizeTokensPreview {
|
||||
|
||||
private const val networksSize = 10
|
||||
private const val tokensSize = 3
|
||||
|
||||
private val draggableToken = TangemTokenRowUM.Actionable(
|
||||
id = UUID.randomUUID().toString(),
|
||||
headIconUM = TangemIconUM.Currency(
|
||||
CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_polygon_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
),
|
||||
titleUM = TangemTokenRowUM.TitleUM.Content(stringReference(value = "Polygon")),
|
||||
subtitleUM = TangemTokenRowUM.SubtitleUM.Empty,
|
||||
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
|
||||
text = stringReference("$ 42,900.13"),
|
||||
),
|
||||
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
|
||||
text = stringReference("733,71097 POL"),
|
||||
),
|
||||
tailUM = TangemRowTailUM.Draggable(R.drawable.ic_group_drop_24),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
|
||||
private val tokenList = List(networksSize) { it }
|
||||
.flatMap { index ->
|
||||
val lastNetworkIndex = networksSize - 1
|
||||
val lastTokenIndex = tokensSize - 1
|
||||
val networkNumber = index + 1
|
||||
|
||||
val group = OrganizeRowItemUM.Network(
|
||||
headerRowUM = TangemHeaderRowUM(
|
||||
id = networkNumber.toString(),
|
||||
title = stringReference(value = "$networkNumber"),
|
||||
),
|
||||
roundingModeUM = when (index) {
|
||||
0 -> RoundingModeUM.Top()
|
||||
lastNetworkIndex -> RoundingModeUM.Bottom()
|
||||
else -> RoundingModeUM.None
|
||||
},
|
||||
accountId = "account_$networkNumber",
|
||||
)
|
||||
|
||||
val tokens: MutableList<OrganizeRowItemUM.Token> = mutableListOf()
|
||||
repeat(times = tokensSize) { i ->
|
||||
val tokenNumber = i + 1
|
||||
tokens.add(
|
||||
OrganizeRowItemUM.Token(
|
||||
tokenRowUM = draggableToken.copy(
|
||||
id = "${group.id}_token_$tokenNumber",
|
||||
titleUM = TangemTokenRowUM.TitleUM.Content(
|
||||
text = stringReference(value = "Token $tokenNumber from $networkNumber network"),
|
||||
),
|
||||
),
|
||||
groupId = group.id,
|
||||
accountId = "account_$networkNumber",
|
||||
roundingModeUM = when {
|
||||
i == lastTokenIndex && index == lastNetworkIndex -> RoundingModeUM.Bottom()
|
||||
else -> RoundingModeUM.None
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val divider = OrganizeRowItemUM.Placeholder(
|
||||
id = "divider_$networkNumber",
|
||||
accountId = "account_$networkNumber",
|
||||
)
|
||||
|
||||
buildList {
|
||||
add(group)
|
||||
addAll(tokens)
|
||||
if (index != lastNetworkIndex) {
|
||||
add(divider)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toPersistentList()
|
||||
|
||||
val defaultState by lazy {
|
||||
OrganizeTokensUM(
|
||||
tokenList = tokenList,
|
||||
organizeMenuUM = OrganizeTokensUM.OrganizeMenuUM(
|
||||
onSortClick = {},
|
||||
onGroupClick = {},
|
||||
),
|
||||
cancelButton = TangemButtonUM(
|
||||
text = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
type = TangemButtonType.Secondary,
|
||||
),
|
||||
applyButton = TangemButtonUM(
|
||||
text = resourceReference(R.string.common_apply),
|
||||
onClick = {},
|
||||
type = TangemButtonType.Primary,
|
||||
),
|
||||
scrollListToTop = consumedEvent(),
|
||||
isAccountsMode = true,
|
||||
isBalanceHidden = true,
|
||||
isGrouped = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableDialogComponent
|
||||
import com.tangem.domain.tokens.model.details.TokenAction
|
||||
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
|
||||
import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent
|
||||
import com.tangem.feature.wallet.child.wallet.model.WalletModel
|
||||
import com.tangem.feature.wallet.navigation.WalletRoute
|
||||
|
|
@ -146,6 +147,15 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
is WalletDialogConfig.OrganizeTokens -> {
|
||||
OrganizeTokensComponent(
|
||||
appComponentContext = childByContext(componentContext),
|
||||
params = OrganizeTokensComponent.Params(
|
||||
userWalletId = dialogConfig.userWalletId,
|
||||
callback = model.innerWalletRouter.organizeCallbacks,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ package com.tangem.feature.wallet.presentation.router
|
|||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -18,6 +20,7 @@ import com.tangem.domain.redux.ReduxStateHolder
|
|||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.domain.tokens.model.details.NavigationAction
|
||||
import com.tangem.domain.tokens.model.details.TokenAction
|
||||
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
|
||||
import com.tangem.feature.wallet.navigation.WalletRoute
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
|
||||
|
|
@ -32,6 +35,7 @@ internal class DefaultWalletRouter @Inject constructor(
|
|||
private val router: AppRouter,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
) : InnerWalletRouter {
|
||||
|
||||
override val dialogNavigation: SlotNavigation<WalletDialogConfig> = SlotNavigation()
|
||||
|
|
@ -41,8 +45,17 @@ internal class DefaultWalletRouter @Inject constructor(
|
|||
onBufferOverflow = BufferOverflow.DROP_LATEST,
|
||||
)
|
||||
|
||||
override val organizeCallbacks: OrganizeTokensComponent.Callback
|
||||
get() = OrganizeCallbacks()
|
||||
|
||||
override fun openOrganizeTokensScreen(userWalletId: UserWalletId) {
|
||||
navigateToFlow.tryEmit(WalletRoute.OrganizeTokens(userWalletId))
|
||||
if (designFeatureToggles.isRedesignEnabled) {
|
||||
dialogNavigation.activate(
|
||||
configuration = WalletDialogConfig.OrganizeTokens(userWalletId),
|
||||
)
|
||||
} else {
|
||||
navigateToFlow.tryEmit(WalletRoute.OrganizeTokens(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun openDetailsScreen(selectedWalletId: UserWalletId) {
|
||||
|
|
@ -165,4 +178,10 @@ internal class DefaultWalletRouter @Inject constructor(
|
|||
override fun openQrScanner() {
|
||||
router.push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.MainScreen))
|
||||
}
|
||||
|
||||
inner class OrganizeCallbacks : OrganizeTokensComponent.Callback {
|
||||
override fun onDismiss() {
|
||||
dialogNavigation.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.pay.TangemPayDetailsConfig
|
||||
import com.tangem.domain.tokens.model.details.NavigationAction
|
||||
import com.tangem.domain.tokens.model.details.TokenAction
|
||||
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
|
||||
import com.tangem.feature.wallet.navigation.WalletRoute
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
|
||||
|
|
@ -34,6 +35,8 @@ internal interface InnerWalletRouter {
|
|||
|
||||
val navigateToFlow: SharedFlow<WalletRoute>
|
||||
|
||||
val organizeCallbacks: OrganizeTokensComponent.Callback
|
||||
|
||||
/** Open organize tokens screen */
|
||||
fun openOrganizeTokensScreen(userWalletId: UserWalletId)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,4 +41,7 @@ internal sealed interface WalletDialogConfig {
|
|||
|
||||
@Serializable
|
||||
data class KycRejected(val walletId: UserWalletId, val customerId: String) : WalletDialogConfig
|
||||
|
||||
@Serializable
|
||||
data class OrganizeTokens(val userWalletId: UserWalletId) : WalletDialogConfig
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue