Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-27 14:34:43 +03:00
parent 229dc89bcb
commit 3398db66e2
13 changed files with 609 additions and 156 deletions

View file

@ -14,7 +14,8 @@ import androidx.compose.ui.util.fastForEach
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock
import com.tangem.core.ui.R
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlockLegacy
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -45,13 +46,21 @@ fun LazyListScope.txHistoryItems(
)
}
is TxHistoryState.Empty -> {
nonContentItem(state = EmptyTransactionsBlockState.Empty(state.onExploreClick), modifier = modifier)
nonContentItem(
state = EmptyTransactionsBlockState.Empty(
onExplore = state.onExploreClick,
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
modifier = modifier,
)
}
is TxHistoryState.Error -> {
nonContentItem(
state = EmptyTransactionsBlockState.FailedToLoad(
onReload = state.onReloadClick,
onExplore = state.onExploreClick,
reloadIconResId = R.drawable.ic_refresh_24,
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
modifier = modifier,
)
@ -64,7 +73,10 @@ fun LazyListScope.txHistoryItems(
}
nonContentItem(
state = EmptyTransactionsBlockState.NotImplemented(onExplore = state.onExploreClick),
state = EmptyTransactionsBlockState.NotImplemented(
onExplore = state.onExploreClick,
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
modifier = modifier,
)
}
@ -121,7 +133,7 @@ fun PendingTxsBlock(pendingTxs: ImmutableList<TransactionState>, isBalanceHidden
private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
item(key = state::class.java, contentType = state::class.java) {
EmptyTransactionBlock(
EmptyTransactionBlockLegacy(
state = state,
modifier = modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)

View file

@ -1,65 +1,74 @@
package com.tangem.core.ui.components.transactions.empty
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.buttons.actions.ActionButton
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.ds.button.TangemButton
import com.tangem.core.ui.ds.button.TangemButtonIconPosition
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.EmptyTransactionBlockTestTags
/**
* Placeholder for transaction's block without content
*
* @param state component state
* @param modifier modifier
*/
@Composable
fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(color = TangemTheme.colors.background.primary)
.padding(vertical = TangemTheme.dimens.spacing24)
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens2.x6)
.testTag(EmptyTransactionBlockTestTags.BLOCK),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
TangemIcon(
tangemIconUM = TangemIconUM.Icon(
iconRes = state.iconRes,
tintReference = { TangemTheme.colors2.graphic.neutral.tertiary },
),
modifier = Modifier
.size(TangemTheme.dimens.size64)
.size(TangemTheme.dimens2.x16)
.testTag(EmptyTransactionBlockTestTags.ICON),
painter = painterResource(id = state.iconRes),
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens2.x4))
Text(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing32)
.padding(horizontal = TangemTheme.dimens2.x8)
.testTag(EmptyTransactionBlockTestTags.TEXT),
textAlign = TextAlign.Center,
text = state.text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography2.calloutRegular15,
color = TangemTheme.colors2.text.neutral.tertiary,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens2.x8))
Buttons(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing18)
.padding(horizontal = TangemTheme.dimens2.x4)
.testTag(EmptyTransactionBlockTestTags.EXPLORE_BUTTON),
state = state.buttonsState,
)
@ -76,41 +85,70 @@ private fun Buttons(state: EmptyTransactionsBlockState.ButtonsState, modifier: M
@Composable
private fun SingleButton(state: EmptyTransactionsBlockState.ButtonsState.SingleButton, modifier: Modifier = Modifier) {
ActionButton(modifier = modifier, config = state.actionButtonConfig)
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
) {
TangemButton(buttonUM = state.actionButtonConfig.toButtonUM())
}
}
@Composable
private fun PairButtons(state: EmptyTransactionsBlockState.ButtonsState.PairButtons, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
ActionButton(
TangemButton(
modifier = Modifier.weight(1F),
config = state.firstButtonConfig,
buttonUM = state.firstButtonConfig.toButtonUM(),
)
ActionButton(
TangemButton(
modifier = Modifier.weight(1F),
config = state.secondButtonConfig,
buttonUM = state.secondButtonConfig.toButtonUM(),
)
}
}
private fun ActionButtonConfig.toButtonUM(): TangemButtonUM = TangemButtonUM(
text = text,
tangemIconUM = TangemIconUM.Icon(iconRes = iconResId),
type = TangemButtonType.Secondary,
size = TangemButtonSize.X12,
isEnabled = isEnabled,
isLoading = isInProgress,
onClick = onClick,
shape = TangemButtonShape.Rounded,
iconPosition = TangemButtonIconPosition.End,
)
@Composable
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun EmptyTransactionBlockPreview(
@PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState,
) {
TangemThemePreview {
TangemThemePreviewRedesign {
EmptyTransactionBlock(state = state)
}
}
private class EmptyTransactionBlockStateProvider : CollectionPreviewParameterProvider<EmptyTransactionsBlockState>(
collection = listOf(
EmptyTransactionsBlockState.Empty {},
EmptyTransactionsBlockState.FailedToLoad(onReload = {}, onExplore = {}),
EmptyTransactionsBlockState.NotImplemented(onExplore = {}),
),
)
private class EmptyTransactionBlockStateProvider :
CollectionPreviewParameterProvider<EmptyTransactionsBlockState>(
collection = listOf(
EmptyTransactionsBlockState.Empty(
onExplore = {},
exploreIconResId = R.drawable.ic_compass_24,
),
EmptyTransactionsBlockState.FailedToLoad(
onReload = {},
onExplore = {},
reloadIconResId = R.drawable.ic_refresh_24,
exploreIconResId = R.drawable.ic_compass_24,
),
EmptyTransactionsBlockState.NotImplemented(
onExplore = {},
exploreIconResId = R.drawable.ic_compass_24,
),
),
)

View file

@ -0,0 +1,129 @@
package com.tangem.core.ui.components.transactions.empty
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.actions.ActionButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.EmptyTransactionBlockTestTags
/**
* Placeholder for transaction's block without content
*
* @param state component state
* @param modifier modifier
*/
@Composable
fun EmptyTransactionBlockLegacy(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(color = TangemTheme.colors.background.primary)
.padding(vertical = TangemTheme.dimens.spacing24)
.testTag(EmptyTransactionBlockTestTags.BLOCK),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size64)
.testTag(EmptyTransactionBlockTestTags.ICON),
painter = painterResource(id = state.iconRes),
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
Text(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing32)
.testTag(EmptyTransactionBlockTestTags.TEXT),
textAlign = TextAlign.Center,
text = state.text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
Buttons(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing18)
.testTag(EmptyTransactionBlockTestTags.EXPLORE_BUTTON),
state = state.buttonsState,
)
}
}
@Composable
private fun Buttons(state: EmptyTransactionsBlockState.ButtonsState, modifier: Modifier = Modifier) {
when (state) {
is EmptyTransactionsBlockState.ButtonsState.SingleButton -> SingleButton(state = state, modifier = modifier)
is EmptyTransactionsBlockState.ButtonsState.PairButtons -> PairButtons(state = state, modifier = modifier)
}
}
@Composable
private fun SingleButton(state: EmptyTransactionsBlockState.ButtonsState.SingleButton, modifier: Modifier = Modifier) {
ActionButton(modifier = modifier, config = state.actionButtonConfig)
}
@Composable
private fun PairButtons(state: EmptyTransactionsBlockState.ButtonsState.PairButtons, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
ActionButton(
modifier = Modifier.weight(1F),
config = state.firstButtonConfig,
)
ActionButton(
modifier = Modifier.weight(1F),
config = state.secondButtonConfig,
)
}
}
@Composable
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun EmptyTransactionBlockLegacyPreview(
@PreviewParameter(EmptyTransactionBlockLegacyStateProvider::class) state: EmptyTransactionsBlockState,
) {
TangemThemePreview {
EmptyTransactionBlockLegacy(state = state)
}
}
private class EmptyTransactionBlockLegacyStateProvider :
CollectionPreviewParameterProvider<EmptyTransactionsBlockState>(
collection = listOf(
EmptyTransactionsBlockState.Empty(
onExplore = {},
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
EmptyTransactionsBlockState.FailedToLoad(
onReload = {},
onExplore = {},
reloadIconResId = R.drawable.ic_refresh_24,
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
EmptyTransactionsBlockState.NotImplemented(
onExplore = {},
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
),
)

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.components.transactions.empty
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
@ -21,17 +22,19 @@ sealed class EmptyTransactionsBlockState(
data class FailedToLoad(
val onReload: () -> Unit,
val onExplore: () -> Unit,
@DrawableRes val reloadIconResId: Int,
@DrawableRes val exploreIconResId: Int,
) : EmptyTransactionsBlockState(
buttonsState = ButtonsState.PairButtons(
firstButtonConfig = ActionButtonConfig(
text = TextReference.Res(R.string.common_reload),
iconResId = R.drawable.ic_refresh_24,
iconResId = reloadIconResId,
onClick = onReload,
isEnabled = true,
),
secondButtonConfig = ActionButtonConfig(
text = TextReference.Res(R.string.common_explore),
iconResId = R.drawable.ic_arrow_top_right_24,
iconResId = exploreIconResId,
onClick = onExplore,
isEnabled = true,
),
@ -40,11 +43,14 @@ sealed class EmptyTransactionsBlockState(
text = TextReference.Res(R.string.transaction_history_error_failed_to_load),
)
data class Empty(val onExplore: (() -> Unit)) : EmptyTransactionsBlockState(
data class Empty(
val onExplore: () -> Unit,
@DrawableRes val exploreIconResId: Int,
) : EmptyTransactionsBlockState(
buttonsState = ButtonsState.SingleButton(
actionButtonConfig = ActionButtonConfig(
text = TextReference.Res(R.string.common_explore),
iconResId = R.drawable.ic_arrow_top_right_24,
iconResId = exploreIconResId,
onClick = onExplore,
isEnabled = true,
),
@ -53,11 +59,14 @@ sealed class EmptyTransactionsBlockState(
text = TextReference.Res(R.string.transaction_history_empty_transactions),
)
data class NotImplemented(val onExplore: () -> Unit) : EmptyTransactionsBlockState(
data class NotImplemented(
val onExplore: () -> Unit,
@DrawableRes val exploreIconResId: Int,
) : EmptyTransactionsBlockState(
buttonsState = ButtonsState.SingleButton(
actionButtonConfig = ActionButtonConfig(
text = TextReference.Res(R.string.common_explore_transaction_history),
iconResId = R.drawable.ic_arrow_top_right_24,
iconResId = exploreIconResId,
onClick = onExplore,
isEnabled = true,
),

View file

@ -99,6 +99,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
tokenDetailsUM = tokenDetailsUM,
tokenMarketBlockComponent = tokenMarketBlockComponent,
yieldSupplyComponent = yieldSupplyComponent,
txHistoryComponent = txHistoryComponent,
modifier = modifier,
)
} else {

View file

@ -13,6 +13,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import com.tangem.common.ui.earn.EarnBlock
import com.tangem.common.ui.notifications.notifications
@ -21,6 +24,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
@ -56,10 +60,14 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlockHeight
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeTint
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
private val TopBarHeight: Dp = 64.dp
private val MarketBlockHorizontalPadding: Dp = 14.dp
@ -69,6 +77,7 @@ internal fun TokenDetailsScreen(
tokenDetailsUM: TokenDetailsUM,
tokenMarketBlockComponent: TokenMarketBlockComponent?,
yieldSupplyComponent: YieldSupplyComponent,
txHistoryComponent: TxHistoryComponent,
modifier: Modifier = Modifier,
) {
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
@ -110,6 +119,7 @@ internal fun TokenDetailsScreen(
TokenDetailsBody(
tokenDetailsUM = tokenDetailsUM,
yieldSupplyComponent = yieldSupplyComponent,
txHistoryComponent = txHistoryComponent,
rootBackground = rootBackground,
bottomContentPadding = marketBlockHeight,
modifier = Modifier
@ -197,13 +207,18 @@ private fun BoxScope.TokenDetailsMarketBlockOverlay(
private fun TokenDetailsBody(
tokenDetailsUM: TokenDetailsUM,
yieldSupplyComponent: YieldSupplyComponent,
txHistoryComponent: TxHistoryComponent,
rootBackground: Color,
bottomContentPadding: Dp,
modifier: Modifier = Modifier,
itemModifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
val txHistoryState by txHistoryComponent.txHistoryState.collectAsStateWithLifecycle()
LazyColumn(
modifier = modifier,
state = listState,
contentPadding = PaddingValues(bottom = bottomContentPadding),
) {
notifications(
@ -222,7 +237,9 @@ private fun TokenDetailsBody(
item(key = "yield_supply_block") {
yieldSupplyComponent.Content(modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x2))
}
// TODO [REDACTED_TASK_KEY] Token Details Make Transaction History
with(txHistoryComponent) {
txHistoryContent(listState = listState, state = txHistoryState)
}
}
}
@ -273,6 +290,15 @@ private fun TokenDetailsScreen_Preview() {
@Composable
override fun Content(modifier: Modifier) = Unit
},
txHistoryComponent = object : TxHistoryComponent {
override val txHistoryState: StateFlow<TxHistoryUM> = MutableStateFlow(
value = TxHistoryUM.Empty(isBalanceHidden = false, onExploreClick = {}),
)
override fun LazyListScope.txHistoryContentLegacy(listState: LazyListState, state: TxHistoryUM) = Unit
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) = Unit
},
)
}
}

View file

@ -151,7 +151,9 @@ internal fun TokenDetailsScreenLegacy(
modifier = itemModifier,
)
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryComponentState) }
with(txHistoryComponent) {
txHistoryContentLegacy(listState = listState, state = txHistoryComponentState)
}
}
}
@ -179,6 +181,8 @@ private fun TokenDetailsScreenPreview(
value = TxHistoryUM.Empty(isBalanceHidden = false, onExploreClick = {}),
)
override fun LazyListScope.txHistoryContentLegacy(listState: LazyListState, state: TxHistoryUM) = Unit
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) = Unit
},
yieldSupplyComponent = object : YieldSupplyComponent {

View file

@ -21,6 +21,7 @@ dependencies {
/** Compose */
implementation(deps.compose.runtime)
implementation(deps.compose.foundation)
implementation(deps.compose.ui.tooling)
/** Other */
implementation(deps.kotlin.immutable.collections)

View file

@ -14,6 +14,8 @@ interface TxHistoryComponent {
val txHistoryState: StateFlow<TxHistoryUM>
fun LazyListScope.txHistoryContentLegacy(listState: LazyListState, state: TxHistoryUM)
fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM)
data class Params(

View file

@ -15,6 +15,7 @@ sealed interface TxHistoryUM {
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_1")),
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_2")),
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_3")),
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_4")),
)
}

View file

@ -1,139 +1,214 @@
package com.tangem.features.txhistory.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.transactions.PendingTxsBlock
import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.components.transactions.TxHistoryGroupTitle
import com.tangem.core.ui.components.transactions.TxHistoryTitle
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.txhistory.entity.TxHistoryUM
private const val LOAD_ITEMS_BUFFER = 20
private val LoadingTitleShimmerWidth = 52.dp
private val LoadingPrimaryShimmerWidth = 110.dp
private val LoadingSecondaryShimmerWidth = 52.dp
private val LoadingEndTopShimmerWidth = 107.dp
private val LoadingEndBottomShimmerWidth = 52.dp
private const val LOADING_TRANSACTION_MIN_ALPHA = 0.1f
fun LazyListScope.txHistoryItems(listState: LazyListState, state: TxHistoryUM) {
when (state) {
is TxHistoryUM.Content -> contentItems(listState, state)
is TxHistoryUM.Empty -> nonContentItem(state = EmptyTransactionsBlockState.Empty(state.onExploreClick))
is TxHistoryUM.Error -> nonContentItem(
state = EmptyTransactionsBlockState.FailedToLoad(
onReload = state.onReloadClick,
onExplore = state.onExploreClick,
),
)
is TxHistoryUM.Empty -> emptyItem(state)
is TxHistoryUM.Error -> errorItem(state)
is TxHistoryUM.Loading -> loadingItems(state)
is TxHistoryUM.NotSupported -> {
if (state.pendingTransactions.isNotEmpty()) {
item(key = "PendingTxsBlock", contentType = "PendingTxsBlock") {
PendingTxsBlock(pendingTxs = state.pendingTransactions, isBalanceHidden = state.isBalanceHidden)
}
}
nonContentItem(
state = EmptyTransactionsBlockState.NotImplemented(onExplore = state.onExploreClick),
)
}
is TxHistoryUM.NotSupported -> notSupportedItem(state)
}
}
private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
item(key = state::class.java, contentType = state::class.java) {
EmptyTransactionBlock(
state = state,
modifier = modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12)
.fillMaxWidth(),
)
@Suppress("UNUSED_PARAMETER")
private fun LazyListScope.contentItems(listState: LazyListState, state: TxHistoryUM.Content) {
item(key = "tx_history_content", contentType = "tx_history_content") {
TxHistoryContentBlock(state = state)
}
}
private fun LazyListScope.emptyItem(state: TxHistoryUM.Empty) {
item(key = "tx_history_empty", contentType = "tx_history_empty") {
TxHistoryEmptyBlock(state = state)
}
}
private fun LazyListScope.errorItem(state: TxHistoryUM.Error) {
item(key = "tx_history_error", contentType = "tx_history_error") {
TxHistoryErrorBlock(state = state)
}
}
private fun LazyListScope.loadingItems(state: TxHistoryUM.Loading) {
itemsIndexed(
items = state.items,
key = { _, item ->
when (item) {
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> item.itemKey
is TxHistoryUM.TxHistoryItemUM.Title -> item.onExploreClick.hashCode()
is TxHistoryUM.TxHistoryItemUM.Transaction ->
item.state.txHash + (item.state as? TransactionState.Content)?.hashCode()
}
},
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TxHistoryListItem(
state = item,
isBalanceHidden = true,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
),
)
},
item(key = "tx_history_loading", contentType = "tx_history_loading") {
TxHistoryLoadingBlock(state = state)
}
}
private fun LazyListScope.notSupportedItem(state: TxHistoryUM.NotSupported) {
item(key = "tx_history_not_supported", contentType = "tx_history_not_supported") {
TxHistoryNotSupportedBlock(state = state)
}
}
@Suppress("UNUSED_PARAMETER")
@Composable
private fun TxHistoryContentBlock(state: TxHistoryUM.Content, modifier: Modifier = Modifier) {
// TODO [REDACTED_TASK_KEY] redesign Content state
}
@Composable
private fun TxHistoryEmptyBlock(state: TxHistoryUM.Empty, modifier: Modifier = Modifier) {
EmptyTransactionBlock(
state = EmptyTransactionsBlockState.Empty(
onExplore = state.onExploreClick,
exploreIconResId = R.drawable.ic_compass_24,
),
modifier = modifier,
)
}
private fun LazyListScope.contentItems(listState: LazyListState, state: TxHistoryUM.Content) {
itemsIndexed(
items = state.items,
key = { _, item ->
when (item) {
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> item.itemKey
is TxHistoryUM.TxHistoryItemUM.Title -> item.onExploreClick.hashCode()
is TxHistoryUM.TxHistoryItemUM.Transaction ->
item.state.txHash + (item.state as? TransactionState.Content)?.hashCode()
}
},
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TxHistoryListItem(
state = item,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
),
)
},
@Composable
private fun TxHistoryErrorBlock(state: TxHistoryUM.Error, modifier: Modifier = Modifier) {
EmptyTransactionBlock(
state = EmptyTransactionsBlockState.FailedToLoad(
onReload = state.onReloadClick,
onExplore = state.onExploreClick,
reloadIconResId = R.drawable.ic_refresh_24,
exploreIconResId = R.drawable.ic_compass_24,
),
modifier = modifier,
)
item {
InfiniteListHandler(
listState = listState,
buffer = LOAD_ITEMS_BUFFER,
onLoadMore = state.loadMore,
)
}
@Composable
private fun TxHistoryLoadingBlock(state: TxHistoryUM.Loading, modifier: Modifier = Modifier) {
val transactionCount = state.items.count { it is TxHistoryUM.TxHistoryItemUM.Transaction }
Column(modifier = modifier.fillMaxWidth()) {
var transactionIndex = 0
state.items.forEach { item ->
when (item) {
is TxHistoryUM.TxHistoryItemUM.Title -> TxHistoryLoadingTitle()
is TxHistoryUM.TxHistoryItemUM.Transaction -> {
val fraction = if (transactionCount <= 1) {
0f
} else {
transactionIndex.toFloat() / (transactionCount - 1)
}
val alpha = lerp(start = 1f, stop = LOADING_TRANSACTION_MIN_ALPHA, fraction = fraction)
TxHistoryLoadingTransaction(modifier = Modifier.alpha(alpha))
transactionIndex++
}
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> Unit
}
}
}
}
@Composable
internal fun TxHistoryListItem(
state: TxHistoryUM.TxHistoryItemUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> {
TxHistoryGroupTitle(config = state.legacyGroupTitle, modifier = modifier)
}
is TxHistoryUM.TxHistoryItemUM.Title -> {
TxHistoryTitle(onExploreClick = state.onExploreClick, modifier = modifier)
}
is TxHistoryUM.TxHistoryItemUM.Transaction -> {
Transaction(
state = state.state,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
private fun TxHistoryLoadingTitle(modifier: Modifier = Modifier) {
RectangleShimmer(
modifier = modifier
.padding(
top = TangemTheme.dimens2.x6,
bottom = TangemTheme.dimens2.x3,
start = TangemTheme.dimens2.x4,
)
}
.size(width = LoadingTitleShimmerWidth, height = TangemTheme.dimens2.x4),
radius = TangemTheme.dimens2.x2,
)
}
@Composable
private fun TxHistoryLoadingTransaction(modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier.fillMaxWidth(),
contentPadding = PaddingValues(
horizontal = TangemTheme.dimens2.x4,
vertical = TangemTheme.dimens2.x3,
),
content = {
CircleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x3)
.size(TangemTheme.dimens2.x10),
)
RectangleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.START_TOP)
.size(width = LoadingPrimaryShimmerWidth, height = TangemTheme.dimens2.x5),
radius = TangemTheme.dimens2.x2,
)
RectangleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.size(width = LoadingSecondaryShimmerWidth, height = TangemTheme.dimens2.x4),
radius = TangemTheme.dimens2.x2,
)
RectangleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.END_TOP)
.size(width = LoadingEndTopShimmerWidth, height = TangemTheme.dimens2.x5),
radius = TangemTheme.dimens2.x2,
)
RectangleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.END_BOTTOM)
.size(width = LoadingEndBottomShimmerWidth, height = TangemTheme.dimens2.x4),
radius = TangemTheme.dimens2.x2,
)
},
)
}
@Composable
private fun TxHistoryNotSupportedBlock(state: TxHistoryUM.NotSupported, modifier: Modifier = Modifier) {
EmptyTransactionBlock(
state = EmptyTransactionsBlockState.NotImplemented(
onExplore = state.onExploreClick,
exploreIconResId = R.drawable.ic_compass_24,
),
modifier = modifier,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TxHistoryLoadingBlock_Preview() {
TangemThemePreviewRedesign {
TxHistoryLoadingBlock(
state = TxHistoryUM.Loading(
isBalanceHidden = false,
onExploreClick = {},
),
)
}
}
}
// endregion Preview

View file

@ -0,0 +1,150 @@
package com.tangem.features.txhistory.ui
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.R
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.transactions.PendingTxsBlock
import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.components.transactions.TxHistoryGroupTitle
import com.tangem.core.ui.components.transactions.TxHistoryTitle
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlockLegacy
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.txhistory.entity.TxHistoryUM
private const val LOAD_ITEMS_BUFFER = 20
fun LazyListScope.txHistoryItemsLegacy(listState: LazyListState, state: TxHistoryUM) {
when (state) {
is TxHistoryUM.Content -> contentItems(listState, state)
is TxHistoryUM.Empty -> nonContentItem(
state = EmptyTransactionsBlockState.Empty(
onExplore = state.onExploreClick,
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
)
is TxHistoryUM.Error -> nonContentItem(
state = EmptyTransactionsBlockState.FailedToLoad(
onReload = state.onReloadClick,
onExplore = state.onExploreClick,
reloadIconResId = R.drawable.ic_refresh_24,
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
)
is TxHistoryUM.Loading -> loadingItems(state)
is TxHistoryUM.NotSupported -> {
if (state.pendingTransactions.isNotEmpty()) {
item(key = "PendingTxsBlock", contentType = "PendingTxsBlock") {
PendingTxsBlock(pendingTxs = state.pendingTransactions, isBalanceHidden = state.isBalanceHidden)
}
}
nonContentItem(
state = EmptyTransactionsBlockState.NotImplemented(
onExplore = state.onExploreClick,
exploreIconResId = R.drawable.ic_arrow_top_right_24,
),
)
}
}
}
private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
item(key = state::class.java, contentType = state::class.java) {
EmptyTransactionBlockLegacy(
state = state,
modifier = modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12)
.fillMaxWidth(),
)
}
}
private fun LazyListScope.loadingItems(state: TxHistoryUM.Loading) {
itemsIndexed(
items = state.items,
key = { _, item ->
when (item) {
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> item.itemKey
is TxHistoryUM.TxHistoryItemUM.Title -> item.onExploreClick.hashCode()
is TxHistoryUM.TxHistoryItemUM.Transaction ->
item.state.txHash + (item.state as? TransactionState.Content)?.hashCode()
}
},
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TxHistoryListItemLegacy(
state = item,
isBalanceHidden = true,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
),
)
},
)
}
private fun LazyListScope.contentItems(listState: LazyListState, state: TxHistoryUM.Content) {
itemsIndexed(
items = state.items,
key = { _, item ->
when (item) {
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> item.itemKey
is TxHistoryUM.TxHistoryItemUM.Title -> item.onExploreClick.hashCode()
is TxHistoryUM.TxHistoryItemUM.Transaction ->
item.state.txHash + (item.state as? TransactionState.Content)?.hashCode()
}
},
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TxHistoryListItemLegacy(
state = item,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
),
)
},
)
item {
InfiniteListHandler(
listState = listState,
buffer = LOAD_ITEMS_BUFFER,
onLoadMore = state.loadMore,
)
}
}
@Composable
internal fun TxHistoryListItemLegacy(
state: TxHistoryUM.TxHistoryItemUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> {
TxHistoryGroupTitle(config = state.legacyGroupTitle, modifier = modifier)
}
is TxHistoryUM.TxHistoryItemUM.Title -> {
TxHistoryTitle(onExploreClick = state.onExploreClick, modifier = modifier)
}
is TxHistoryUM.TxHistoryItemUM.Transaction -> {
Transaction(
state = state.state,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
}
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.model.TxHistoryModel
import com.tangem.features.txhistory.ui.txHistoryItems
import com.tangem.features.txhistory.ui.txHistoryItemsLegacy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -22,6 +23,10 @@ internal class DefaultTxHistoryComponent @AssistedInject constructor(
override val txHistoryState: StateFlow<TxHistoryUM>
get() = model.uiState
override fun LazyListScope.txHistoryContentLegacy(listState: LazyListState, state: TxHistoryUM) {
txHistoryItemsLegacy(listState, state)
}
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) {
txHistoryItems(listState, state)
}