Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-01 13:10:07 +05:00
parent aea7f5f415
commit c83ce2ada3
53 changed files with 1062 additions and 238 deletions

View file

@ -12,6 +12,7 @@ dependencies {
/* Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.wallets.models)
/* Project - Core */
implementation(projects.core.decompose)

View file

@ -0,0 +1,14 @@
package com.tangem.features.nft.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
interface NFTCollectionsComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, NFTCollectionsComponent>
}

View file

@ -27,7 +27,10 @@ dependencies {
implementation(projects.core.datasource)
/** Domain modules */
implementation(projects.domain.nft)
implementation(projects.domain.nft.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
/** Common */
implementation(projects.common.ui)

View file

@ -1,10 +1,17 @@
package com.tangem.features.nft
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.model.Model
import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent
import com.tangem.features.nft.collections.model.NFTCollectionsModel
import com.tangem.features.nft.component.NFTCollectionsComponent
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@ -16,4 +23,17 @@ internal object NFTFeatureModule {
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): NFTFeatureToggles {
return DefaultNFTFeatureToggles(featureTogglesManager)
}
}
@Module
@InstallIn(SingletonComponent::class)
internal interface NFTFeatureModuleBinds {
@Binds
@Singleton
fun bindComponentFactory(impl: DefaultNFTCollectionsComponent.Factory): NFTCollectionsComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTCollectionsModel::class)
fun bindModel(model: NFTCollectionsModel): Model
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.nft.collections
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.nft.collections.model.NFTCollectionsModel
import com.tangem.features.nft.collections.ui.NFTCollections
import com.tangem.features.nft.component.NFTCollectionsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTCollectionsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTCollectionsComponent.Params,
) : NFTCollectionsComponent, AppComponentContext by context {
private val model: NFTCollectionsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
NFTCollections(state, modifier)
}
@AssistedFactory
interface Factory : NFTCollectionsComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTCollectionsComponent.Params,
): DefaultNFTCollectionsComponent
}
}

View file

@ -5,12 +5,8 @@ import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class NFTCollectionAssetsListUM {
data object Collapsed : NFTCollectionAssetsListUM()
@Immutable
sealed class Expanded : NFTCollectionAssetsListUM() {
data class Loading(val itemsCount: Int) : Expanded()
data class Failed(val onRetryClick: () -> Unit) : Expanded()
data class Content(val items: ImmutableList<NFTCollectionAssetUM>) : Expanded()
}
data object Init : NFTCollectionAssetsListUM()
data class Loading(val itemsCount: Int) : NFTCollectionAssetsListUM()
data class Failed(val onRetryClick: () -> Unit) : NFTCollectionAssetsListUM()
data class Content(val items: ImmutableList<NFTCollectionAssetUM>) : NFTCollectionAssetsListUM()
}

View file

@ -10,5 +10,6 @@ internal data class NFTCollectionUM(
val logoUrl: String?,
val description: TextReference,
val assets: NFTCollectionAssetsListUM,
val isExpanded: Boolean,
val onExpandClick: () -> Unit,
)

View file

@ -0,0 +1,6 @@
package com.tangem.features.nft.collections.entity
internal data class NFTCollectionsStateUM(
val onBackClick: () -> Unit,
val content: NFTCollectionsUM,
)

View file

@ -2,4 +2,7 @@ package com.tangem.features.nft.collections.entity
import com.tangem.core.ui.components.notifications.NotificationConfig
internal data class NFTCollectionsWarningUM(val config: NotificationConfig)
internal data class NFTCollectionsWarningUM(
val id: String,
val config: NotificationConfig,
)

View file

@ -0,0 +1,34 @@
package com.tangem.features.nft.collections.entity.transformer
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class ChangeCollectionExpandedStateTransformer(
private val collectionId: NFTCollection.Identifier,
private val onFirstExpanded: () -> Unit,
) : Transformer<NFTCollectionsStateUM> {
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
content = when (prevState.content) {
is NFTCollectionsUM.Empty,
is NFTCollectionsUM.Loading,
is NFTCollectionsUM.Failed,
-> prevState.content
is NFTCollectionsUM.Content -> prevState.content.copy(
collections = prevState.content.collections.map {
if (it.id == collectionId.toString()) {
if (!it.isExpanded) {
onFirstExpanded()
}
it.copy(isExpanded = !it.isExpanded)
} else {
it
}
}.toPersistentList(),
)
},
)
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.nft.collections.entity.transformer
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.utils.transformer.Transformer
internal class ToggleSearchBarTransformer(private val isActive: Boolean) : Transformer<NFTCollectionsStateUM> {
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
content = when (val content = prevState.content) {
is NFTCollectionsUM.Content -> content.copy(
search = content.search.copy(
isActive = isActive,
),
)
is NFTCollectionsUM.Empty,
is NFTCollectionsUM.Loading,
is NFTCollectionsUM.Failed,
-> content
},
)
}

View file

@ -0,0 +1,134 @@
package com.tangem.features.nft.collections.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.nft.models.*
import com.tangem.features.nft.collections.entity.*
import com.tangem.features.nft.impl.R
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Suppress("LongParameterList")
internal class UpdateDataStateTransformer(
private val nftCollections: List<NFTCollections>,
private val searchQuery: String,
private val onReceiveClick: () -> Unit,
private val onRetryClick: () -> Unit,
private val onExpandCollectionClick: (NFTCollection) -> Unit,
private val onRetryAssetsClick: (NFTCollection) -> Unit,
private val onAssetClick: (NFTAsset) -> Unit,
private val initialSearchBarFactory: () -> SearchBarUM,
) : Transformer<NFTCollectionsStateUM> {
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
content = when {
nftCollections.allCollectionsFailed() ->
NFTCollectionsUM.Failed(onRetryClick, onReceiveClick)
nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() ->
NFTCollectionsUM.Failed(onRetryClick, onReceiveClick)
nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
NFTCollectionsUM.Empty(onReceiveClick)
!nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
NFTCollectionsUM.Loading(onReceiveClick)
else -> {
NFTCollectionsUM.Content(
search = if (prevState.content is NFTCollectionsUM.Content) {
prevState.content.search.copy(
query = searchQuery,
)
} else {
initialSearchBarFactory()
},
collections = nftCollections
.map { it.content }
.asSequence()
.filterIsInstance<NFTCollections.Content.Collections>()
.map { it.collections.orEmpty().transform(prevState, searchQuery) }
.flatten()
.toPersistentList(),
warnings = transformNotifications(),
onReceiveClick = onReceiveClick,
)
}
},
)
private fun List<NFTCollection>.transform(
state: NFTCollectionsStateUM,
query: String,
): ImmutableList<NFTCollectionUM> = mapNotNull {
if (query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true) {
NFTCollectionUM(
id = it.id.toString(),
networkIconId = getActiveIconRes(it.network.id.value),
name = it.name.orEmpty(),
description = TextReference.PluralRes(
R.plurals.nft_collections_count,
it.count,
wrappedList(it.count),
),
logoUrl = it.logoUrl,
assets = it.transformAssets(),
onExpandClick = {
onExpandCollectionClick(it)
},
isExpanded = it.isExpanded(state),
)
} else {
null
}
}.toPersistentList()
private fun transformNotifications(): ImmutableList<NFTCollectionsWarningUM> = buildList {
if (nftCollections.anyCollectionFailed()) {
add(
NFTCollectionsWarningUM(
id = "loading troubles",
config = NotificationConfig(
title = TextReference.Res(R.string.nft_collections_warning_title),
subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle),
iconResId = R.drawable.ic_alert_triangle_20,
),
),
)
}
}.toPersistentList()
private fun NFTCollection.transformAssets(): NFTCollectionAssetsListUM = when (val assets = this.assets) {
is NFTCollection.Assets.Empty -> NFTCollectionAssetsListUM.Init
is NFTCollection.Assets.Loading -> NFTCollectionAssetsListUM.Loading(count)
is NFTCollection.Assets.Failed -> NFTCollectionAssetsListUM.Failed { onRetryAssetsClick(this) }
is NFTCollection.Assets.Value -> NFTCollectionAssetsListUM.Content(
items = assets
.items
.map { it.transform() }
.toPersistentList(),
)
}
private fun NFTAsset.transform(): NFTCollectionAssetUM = NFTCollectionAssetUM(
id = id.toString(),
name = name.orEmpty(),
imageUrl = media?.url,
price = when (val salePrice = salePrice) {
is NFTSalePrice.Empty -> NFTSalePriceUM.Failed
is NFTSalePrice.Loading -> NFTSalePriceUM.Loading
is NFTSalePrice.Error -> NFTSalePriceUM.Failed
is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString())
},
onItemClick = {
onAssetClick(this)
},
)
private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean =
(state.content as? NFTCollectionsUM.Content)
?.collections
?.firstOrNull { it.id == id.toString() }
?.isExpanded
?: false
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.nft.collections.entity.transformer
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.utils.transformer.Transformer
internal class UpdateSearchQueryTransformer(private val newQuery: String) : Transformer<NFTCollectionsStateUM> {
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
content = when (val content = prevState.content) {
is NFTCollectionsUM.Content -> content.copy(
search = content.search.copy(
query = newQuery,
),
)
is NFTCollectionsUM.Empty,
is NFTCollectionsUM.Loading,
is NFTCollectionsUM.Failed,
-> content
},
)
}

View file

@ -0,0 +1,138 @@
package com.tangem.features.nft.collections.model
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.decompose.navigation.Router
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.features.nft.collections.entity.*
import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer
import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer
import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer
import com.tangem.features.nft.collections.entity.transformer.UpdateSearchQueryTransformer
import com.tangem.features.nft.component.NFTCollectionsComponent
import com.tangem.features.nft.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class NFTCollectionsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val searchManager: InputManager,
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase,
paramsContainer: ParamsContainer,
) : Model() {
val state: StateFlow<NFTCollectionsStateUM> get() = _state
private val _state = MutableStateFlow(
value = NFTCollectionsStateUM(
onBackClick = ::navigateBack,
content = NFTCollectionsUM.Loading(::onReceiveClick),
),
)
private val params: NFTCollectionsComponent.Params = paramsContainer.require()
init {
subscribeToNFTCollections()
}
private fun subscribeToNFTCollections() {
combine(
flow = getNFTCollectionsUseCase(params.userWalletId),
flow2 = searchManager.query.distinctUntilChanged(),
) { nftCollections, query ->
_state.update {
UpdateDataStateTransformer(
nftCollections = nftCollections,
searchQuery = query,
onReceiveClick = ::onReceiveClick,
onRetryClick = ::onRetryClick,
onExpandCollectionClick = ::onExpandCollectionClick,
onRetryAssetsClick = ::onRetryAssetsClick,
onAssetClick = ::onAssetClick,
initialSearchBarFactory = ::getInitialSearchBar,
).transform(it)
}
}
.launchIn(modelScope)
}
private fun onSearchQueryChange(newQuery: String) {
modelScope.launch {
_state.update { UpdateSearchQueryTransformer(newQuery).transform(it) }
searchManager.update(newQuery)
}
}
private fun getInitialSearchBar(): SearchBarUM = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
isActive = false,
onQueryChange = ::onSearchQueryChange,
onActiveChange = ::toggleSearchBar,
)
private fun toggleSearchBar(isActive: Boolean) {
_state.update {
ToggleSearchBarTransformer(isActive).transform(it)
}
}
private fun onExpandCollectionClick(collection: NFTCollection) {
_state.update {
ChangeCollectionExpandedStateTransformer(
collectionId = collection.id,
onFirstExpanded = { onFirstExpanded(collection) },
).transform(it)
}
}
private fun onFirstExpanded(collection: NFTCollection) {
loadCollectionAssets(collection)
}
private fun onRetryAssetsClick(collection: NFTCollection) {
loadCollectionAssets(collection)
}
private fun onRetryClick() {
// TODO refresh all
}
@Suppress("UnusedPrivateMember")
private fun onAssetClick(asset: NFTAsset) {
// TODO move to details
}
private fun onReceiveClick() {
// TODO move to receive
}
private fun navigateBack() {
router.pop()
}
private fun loadCollectionAssets(collection: NFTCollection) {
modelScope.launch {
fetchNFTCollectionAssetsUseCase(
userWalletId = params.userWalletId,
network = collection.network,
collectionId = collection.id,
)
}
}
}

View file

@ -13,12 +13,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge
import com.tangem.core.ui.extensions.TextReference
@ -28,13 +31,14 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM
import com.tangem.features.nft.collections.entity.NFTCollectionUM
import com.tangem.features.nft.impl.R
import kotlinx.collections.immutable.persistentListOf
private const val CHEVRON_ROTATION_EXPANDED = 180f
private const val CHEVRON_ROTATION_COLLAPSED = 0f
@Composable
internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier) {
val isExpanded = state.assets is NFTCollectionAssetsListUM.Expanded
val isExpanded = state.isExpanded
Column(
modifier = modifier,
@ -87,8 +91,12 @@ private fun Logo(state: NFTCollectionUM) {
SubcomposeAsyncImage(
modifier = Modifier
.align(Alignment.CenterStart)
.size(TangemTheme.dimens.size36),
model = state.logoUrl,
.size(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(LocalContext.current)
.data(state.logoUrl)
.crossfade(true)
.build(),
loading = {
RectangleShimmer(radius = TangemTheme.dimens.radius8)
},
@ -99,6 +107,7 @@ private fun Logo(state: NFTCollectionUM) {
.background(TangemTheme.colors.field.primary),
)
},
contentScale = ContentScale.Crop,
contentDescription = null,
)
CurrencyIconTopBadge(
@ -160,7 +169,8 @@ private class NFTCollectionProvider : CollectionPreviewParameterProvider<NFTColl
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Collapsed,
assets = NFTCollectionAssetsListUM.Content(persistentListOf()),
isExpanded = false,
onExpandClick = { },
),
NFTCollectionUM(
@ -169,10 +179,11 @@ private class NFTCollectionProvider : CollectionPreviewParameterProvider<NFTColl
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Expanded.Loading(
assets = NFTCollectionAssetsListUM.Loading(
itemsCount = 3,
),
onExpandClick = { },
isExpanded = true,
),
),
)

View file

@ -2,7 +2,6 @@ package com.tangem.features.nft.collections.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
@ -10,11 +9,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH2
@ -26,13 +28,16 @@ import com.tangem.features.nft.collections.entity.NFTSalePriceUM
@Composable
internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clickable { state.onItemClick() },
modifier = modifier,
) {
SubcomposeAsyncImage(
modifier = Modifier
.aspectRatio(1f),
model = state.imageUrl,
.aspectRatio(1f)
.clip(TangemTheme.shapes.roundedCornersXMedium),
model = ImageRequest.Builder(LocalContext.current)
.data(state.imageUrl)
.crossfade(true)
.build(),
loading = {
RectangleShimmer(radius = TangemTheme.dimens.radius16)
},
@ -43,6 +48,7 @@ internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier
.background(TangemTheme.colors.field.primary),
)
},
contentScale = ContentScale.Crop,
contentDescription = null,
)
SpacerH12()

View file

@ -0,0 +1,49 @@
package com.tangem.features.nft.collections.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.features.nft.impl.R
@Composable
internal fun NFTCollections(state: NFTCollectionsStateUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.secondary,
topBar = {
AppBarWithBackButton(
modifier = Modifier.statusBarsPadding(),
onBackClick = state.onBackClick,
text = stringResourceSafe(id = R.string.nft_collections_title),
iconRes = R.drawable.ic_back_24,
)
},
content = { innerPadding ->
AnimatedContent(
targetState = state.content,
contentKey = { it::class },
label = "NFT Collections",
) {
val contentModifier = Modifier
.padding(innerPadding)
.fillMaxSize()
when (val content = it) {
is NFTCollectionsUM.Content -> NFTCollectionsContent(content, contentModifier)
is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content, contentModifier)
is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content, contentModifier)
is NFTCollectionsUM.Loading -> Unit
}
}
},
)
}

View file

@ -1,12 +1,16 @@
package com.tangem.features.nft.collections.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -24,17 +28,12 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.collections.entity.*
import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM
import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM
import com.tangem.features.nft.collections.entity.NFTCollectionUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.features.nft.collections.entity.NFTSalePriceUM
import com.tangem.features.nft.impl.R
import kotlinx.collections.immutable.persistentListOf
@Suppress("LongMethod")
@Composable
internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Modifier = Modifier) {
internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
Box(
@ -48,29 +47,34 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo
),
) {
Column(
modifier = Modifier,
modifier = Modifier
.fillMaxWidth(),
) {
SearchBar(
state = state.search,
state = content.search,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
)
state.warnings.fastForEach {
NFTCollectionWarning(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing16),
state = it,
)
content.warnings.fastForEach {
key(it.id) {
NFTCollectionWarning(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing16),
state = it,
)
}
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(
top = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing60,
)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary),
state = listState,
) {
state.collections.fastForEach { collection ->
content.collections.fastForEach { collection ->
item(key = collection.id) {
NFTCollection(
modifier = Modifier.fillMaxWidth(),
@ -78,23 +82,26 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo
)
}
when (val assets = collection.assets) {
is NFTCollectionAssetsListUM.Collapsed -> Unit
is NFTCollectionAssetsListUM.Expanded.Loading -> {
is NFTCollectionAssetsListUM.Init -> Unit
is NFTCollectionAssetsListUM.Loading -> {
assetsListLoading(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
)
}
is NFTCollectionAssetsListUM.Expanded.Failed -> {
is NFTCollectionAssetsListUM.Failed -> {
assetsListFailed(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
)
}
is NFTCollectionAssetsListUM.Expanded.Content -> {
is NFTCollectionAssetsListUM.Content -> {
assetsListContent(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
)
}
}
@ -106,14 +113,15 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo
.fillMaxWidth()
.align(Alignment.BottomCenter),
text = stringResourceSafe(R.string.nft_collections_receive),
onClick = { },
onClick = content.onReceiveClick,
)
}
}
private fun LazyListScope.assetsListLoading(
collectionId: String,
content: NFTCollectionAssetsListUM.Expanded.Loading,
content: NFTCollectionAssetsListUM.Loading,
expanded: Boolean,
) {
val itemsCount = content.itemsCount
val rowCount = (itemsCount + 1) / 2
@ -121,60 +129,71 @@ private fun LazyListScope.assetsListLoading(
item(
key = "loading_${collectionId}_$rowIndex",
) {
val paddingValues = PaddingValues(
start = TangemTheme.dimens.spacing6,
top = TangemTheme.dimens.spacing6,
end = TangemTheme.dimens.spacing6,
bottom = TangemTheme.dimens.spacing20,
)
Row(
modifier = Modifier
.padding(TangemTheme.dimens.spacing6)
.animateItem(),
) {
NFTCollectionAssetLoading(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
AnimatedVisibility(visible = expanded) {
val paddingValues = PaddingValues(
start = TangemTheme.dimens.spacing6,
top = TangemTheme.dimens.spacing6,
end = TangemTheme.dimens.spacing6,
bottom = TangemTheme.dimens.spacing20,
)
if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) {
Box(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
)
} else {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing6)
.animateContentSize(),
) {
NFTCollectionAssetLoading(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
)
if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) {
Box(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
)
} else {
NFTCollectionAssetLoading(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
)
}
}
}
}
}
}
private fun LazyListScope.assetsListFailed(collectionId: String, content: NFTCollectionAssetsListUM.Expanded.Failed) {
private fun LazyListScope.assetsListFailed(
collectionId: String,
content: NFTCollectionAssetsListUM.Failed,
expanded: Boolean,
) {
item(
key = "failed_$collectionId",
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size142),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(
onRetryClick = content.onRetryClick,
)
AnimatedVisibility(visible = expanded) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size142)
.animateContentSize(),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(
onRetryClick = content.onRetryClick,
)
}
}
}
}
private fun LazyListScope.assetsListContent(
collectionId: String,
content: NFTCollectionAssetsListUM.Expanded.Content,
content: NFTCollectionAssetsListUM.Content,
expanded: Boolean,
) {
val items = content.items
val itemsCount = items.size
@ -185,36 +204,43 @@ private fun LazyListScope.assetsListContent(
item(
key = "content_${collectionId}_${item1.id}_${item2?.id}",
) {
val paddingValues = PaddingValues(
start = TangemTheme.dimens.spacing6,
top = TangemTheme.dimens.spacing6,
end = TangemTheme.dimens.spacing6,
bottom = TangemTheme.dimens.spacing20,
)
Row(
modifier = Modifier
.padding(TangemTheme.dimens.spacing6)
.animateItem(),
) {
NFTCollectionAsset(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
state = item1,
AnimatedVisibility(visible = expanded) {
val paddingValues = PaddingValues(
start = TangemTheme.dimens.spacing6,
top = TangemTheme.dimens.spacing6,
end = TangemTheme.dimens.spacing6,
bottom = TangemTheme.dimens.spacing20,
)
if (item2 == null) {
Box(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
)
} else {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing6)
.animateContentSize(),
) {
NFTCollectionAsset(
modifier = Modifier
.weight(1f)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clickable { item1.onItemClick() }
.padding(paddingValues),
state = item2,
state = item1,
)
if (item2 == null) {
Box(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
)
} else {
NFTCollectionAsset(
modifier = Modifier
.weight(1f)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clickable { item2.onItemClick() }
.padding(paddingValues),
state = item2,
)
}
}
}
}
@ -228,7 +254,7 @@ private fun LazyListScope.assetsListContent(
private fun Preview_NFTCollectionsContent() {
TangemThemePreview {
NFTCollectionsContent(
state = NFTCollectionsUM.Content(
content = NFTCollectionsUM.Content(
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
@ -243,7 +269,8 @@ private fun Preview_NFTCollectionsContent() {
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Collapsed,
assets = NFTCollectionAssetsListUM.Content(persistentListOf()),
isExpanded = false,
onExpandClick = { },
),
NFTCollectionUM(
@ -252,9 +279,10 @@ private fun Preview_NFTCollectionsContent() {
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Expanded.Loading(
assets = NFTCollectionAssetsListUM.Loading(
itemsCount = 1,
),
isExpanded = true,
onExpandClick = { },
),
NFTCollectionUM(
@ -263,9 +291,10 @@ private fun Preview_NFTCollectionsContent() {
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Expanded.Failed(
assets = NFTCollectionAssetsListUM.Failed(
onRetryClick = { },
),
isExpanded = true,
onExpandClick = { },
),
NFTCollectionUM(
@ -274,7 +303,7 @@ private fun Preview_NFTCollectionsContent() {
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Expanded.Content(
assets = NFTCollectionAssetsListUM.Content(
items = persistentListOf(
NFTCollectionAssetUM(
id = "item1",
@ -299,11 +328,13 @@ private fun Preview_NFTCollectionsContent() {
),
),
),
isExpanded = true,
onExpandClick = { },
),
),
warnings = persistentListOf(
NFTCollectionsWarningUM(
id = "loading troubles",
config = NotificationConfig(
title = TextReference.Res(R.string.nft_collections_warning_title),
subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle),

View file

@ -34,7 +34,7 @@ internal fun NFTCollectionsFailed(state: NFTCollectionsUM.Failed, modifier: Modi
.fillMaxWidth()
.align(Alignment.BottomCenter),
text = stringResourceSafe(R.string.nft_collections_receive),
onClick = { },
onClick = state.onReceiveClick,
)
}
}

View file

@ -24,6 +24,7 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.utils)
/** Project - Common */
implementation(projects.common.routing)

View file

@ -10,6 +10,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.demo.IsDemoCardUseCase
@ -25,7 +26,6 @@ import com.tangem.features.onramp.main.entity.*
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
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.components.fields.InputManager
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.FetchOnrampCountriesUseCase
@ -20,7 +21,6 @@ import com.tangem.features.onramp.selectcountry.entity.CountryListUMController
import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsErrorTransformer
import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsLoadingTransformer
import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsTransformer
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.features.onramp.utils.sendOnrampErrorEvent

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
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.components.fields.InputManager
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.FetchOnrampCurrenciesUseCase
@ -20,7 +21,6 @@ import com.tangem.features.onramp.selectcurrency.entity.CurrencyListController
import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsErrorTransformer
import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsLoadingTransformer
import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsTransformer
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.features.onramp.utils.sendOnrampErrorEvent

View file

@ -3,6 +3,7 @@ package com.tangem.features.onramp.swap.availablepairs.model
import arrow.core.getOrElse
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.extensions.capitalize
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
@ -31,7 +32,6 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
@ -63,7 +63,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
init {
initializeSearchBardCallbacks()
initializeSearchBarCallbacks()
subscribeOnUpdateState()
subscribeOnAvailablePairsUpdates()
@ -82,7 +82,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
}
private fun initializeSearchBardCallbacks() {
private fun initializeSearchBarCallbacks() {
tokenListUMController.update(
transformer = UpdateSearchBarCallbacksTransformer(
onQueryChange = ::onSearchQueryChange,

View file

@ -4,6 +4,7 @@ import arrow.core.getOrElse
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -28,7 +29,6 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer

View file

@ -1,34 +0,0 @@
package com.tangem.features.onramp.utils
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.withDebounce
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
/**
* Search tokens manager
*
[REDACTED_AUTHOR]
*/
internal class InputManager @Inject constructor() {
val query: Flow<String>
get() = _query
private val _query = MutableStateFlow(value = "")
private val jobHolder = JobHolder()
suspend fun update(value: String) {
coroutineScope {
if (value.isEmpty()) {
jobHolder.cancel()
_query.value = value
} else {
withDebounce(jobHolder) { _query.value = value }
}
}
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
@ -55,6 +56,8 @@ internal interface WalletContentClickIntents {
fun onConfirmDisposeExpressStatus()
fun onDisposeExpressStatus()
fun onNFTClick(userWalletId: UserWalletId)
}
@Suppress("LongParameterList")
@ -235,4 +238,8 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
stateHolder.update(CloseBottomSheetTransformer(userWalletId))
}
override fun onNFTClick(userWalletId: UserWalletId) {
router.openNFTCollectionsScreen(userWalletId)
}
}

View file

@ -125,6 +125,7 @@ internal object WalletScreenPreviewData {
collectionsCount = 1,
assetsCount = 3,
isFlickering = false,
onItemClick = { },
),
)
}

View file

@ -115,4 +115,8 @@ internal class DefaultWalletRouter @Inject constructor(
override fun openScanFailedDialog(onTryAgain: () -> Unit) {
reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain))
}
override fun openNFTCollectionsScreen(userWalletId: UserWalletId) {
router.push(AppRoute.NFTCollections(userWalletId))
}
}

View file

@ -56,4 +56,7 @@ internal interface InnerWalletRouter {
/** Open scan failed dialog */
fun openScanFailedDialog(onTryAgain: () -> Unit)
/** Open NFT collections screen */
fun openNFTCollectionsScreen(userWalletId: UserWalletId)
}

View file

@ -10,7 +10,9 @@ sealed class WalletNFTItemUM {
data object Loading : WalletNFTItemUM()
data object Empty : WalletNFTItemUM()
data class Empty(
val onItemClick: () -> Unit,
) : WalletNFTItemUM()
data object Failed : WalletNFTItemUM()
@ -19,6 +21,7 @@ sealed class WalletNFTItemUM {
val collectionsCount: Int,
val assetsCount: Int,
val isFlickering: Boolean,
val onItemClick: () -> Unit,
) : WalletNFTItemUM() {
@Immutable

View file

@ -1,7 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.StatusSource
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.nft.models.*
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -10,16 +10,21 @@ import kotlinx.collections.immutable.toPersistentList
internal class SetNFTCollectionsTransformer(
userWalletId: UserWalletId,
private val nftCollections: List<NFTCollections>,
private val onItemClick: () -> Unit,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState = when (prevState) {
is WalletState.MultiCurrency.Content -> prevState.copy(
nftState = when {
allCollectionsFailed() -> WalletNFTItemUM.Failed
anyCollectionFailed() && allLoadedCollectionsEmpty() -> WalletNFTItemUM.Failed
allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Empty
!allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Loading
else -> createContentNFTItemUM()
nftCollections.allCollectionsFailed() ->
WalletNFTItemUM.Failed
nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() ->
WalletNFTItemUM.Failed
nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
WalletNFTItemUM.Empty(onItemClick)
!nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
WalletNFTItemUM.Loading
else -> createContentNFTItemUM(onItemClick)
},
)
is WalletState.SingleCurrency.Content,
@ -31,7 +36,7 @@ internal class SetNFTCollectionsTransformer(
-> prevState
}
private fun createContentNFTItemUM(): WalletNFTItemUM.Content {
private fun createContentNFTItemUM(onItemClick: () -> Unit): WalletNFTItemUM.Content {
val collectionsContent = nftCollections
.map { it.content }
.filterIsInstance<NFTCollections.Content.Collections>()
@ -61,34 +66,10 @@ internal class SetNFTCollectionsTransformer(
assetsCount = collections
.sumOf { it.count },
isFlickering = isFlickering,
onItemClick = onItemClick,
)
}
private fun allCollectionsFailed() = nftCollections.all {
it.content is NFTCollections.Content.Error
}
private fun anyCollectionFailed() = nftCollections.any {
it.content is NFTCollections.Content.Error
}
private fun allLoadedCollectionsEmpty() = nftCollections
.map { it.content }
.filterIsInstance<NFTCollections.Content.Collections>()
.all { it.collections.isNullOrEmpty() }
private fun allCollectionsLoaded() = nftCollections.all {
val content = it.content
content is NFTCollections.Content.Collections &&
content.source != StatusSource.CACHE
}
private fun allCollectionsEmpty() = nftCollections.all {
val content = it.content
content is NFTCollections.Content.Collections &&
content.collections.isNullOrEmpty()
}
companion object {
private const val NFT_COLLECTIONS_MAX_PREVIEWS_COUNT = 4
}

View file

@ -11,13 +11,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@Suppress("UnusedPrivateMember")
internal class WalletNFTListSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,
private val walletsRepository: WalletsRepository,
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
clickIntents: WalletClickIntents,
private val clickIntents: WalletClickIntents,
) : WalletSubscriber() {
@OptIn(ExperimentalCoroutinesApi::class)
@ -27,8 +26,7 @@ internal class WalletNFTListSubscriber(
.flatMapLatest { nftEnabled ->
// if NFT is enabled for this wallet, then start observing changes from store and apply transformer if need
if (nftEnabled) {
getNFTCollectionsUseCase
.launch(userWallet.walletId)
getNFTCollectionsUseCase(userWallet.walletId)
.shareIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(),
@ -36,7 +34,11 @@ internal class WalletNFTListSubscriber(
)
.onEach {
stateHolder.update(
SetNFTCollectionsTransformer(userWallet.walletId, it),
SetNFTCollectionsTransformer(
userWalletId = userWallet.walletId,
nftCollections = it,
onItemClick = { clickIntents.onNFTClick(userWallet.walletId) },
),
)
}
} else {

View file

@ -713,7 +713,6 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi
nftCollections(
modifier = itemModifier,
state = it.nftState,
onClick = {},
)
}
}

View file

@ -34,19 +34,19 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun WalletNFTItem(state: WalletNFTItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = { }) {
internal fun WalletNFTItem(state: WalletNFTItemUM, modifier: Modifier = Modifier) {
when (state) {
is WalletNFTItemUM.Hidden -> Unit
is WalletNFTItemUM.Empty -> WalletNFTItemEmpty(
modifier = modifier,
onClick = onClick,
onClick = state.onItemClick,
)
is WalletNFTItemUM.Failed -> WalletNFTItemFailed(modifier = modifier)
is WalletNFTItemUM.Loading -> WalletNFTItemLoading(modifier = modifier)
is WalletNFTItemUM.Content -> WalletNFTItemContent(
state = state,
onClick = onClick,
onClick = state.onItemClick,
modifier = modifier,
)
}
@ -396,16 +396,15 @@ private fun RowContentContainer(
@Composable
private fun Preview_WalletNFTItem(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) {
TangemThemePreview {
WalletNFTItem(
state = state,
onClick = {},
)
WalletNFTItem(state = state)
}
}
private class WalletNFTItemProvider : CollectionPreviewParameterProvider<WalletNFTItemUM>(
collection = listOf(
WalletNFTItemUM.Empty,
WalletNFTItemUM.Empty(
onItemClick = { },
),
WalletNFTItemUM.Loading,
WalletNFTItemUM.Failed,
WalletNFTItemUM.Content(
@ -415,6 +414,7 @@ private class WalletNFTItemProvider : CollectionPreviewParameterProvider<WalletN
assetsCount = 125,
collectionsCount = 11,
isFlickering = true,
onItemClick = { },
),
WalletNFTItemUM.Content(
previews = persistentListOf(
@ -424,6 +424,7 @@ private class WalletNFTItemProvider : CollectionPreviewParameterProvider<WalletN
assetsCount = 125,
collectionsCount = 11,
isFlickering = false,
onItemClick = { },
),
WalletNFTItemUM.Content(
previews = persistentListOf(
@ -434,6 +435,7 @@ private class WalletNFTItemProvider : CollectionPreviewParameterProvider<WalletN
assetsCount = 125,
collectionsCount = 11,
isFlickering = false,
onItemClick = { },
),
WalletNFTItemUM.Content(
previews = persistentListOf(
@ -445,6 +447,7 @@ private class WalletNFTItemProvider : CollectionPreviewParameterProvider<WalletN
assetsCount = 125,
collectionsCount = 11,
isFlickering = false,
onItemClick = { },
),
WalletNFTItemUM.Content(
previews = persistentListOf(
@ -456,6 +459,7 @@ private class WalletNFTItemProvider : CollectionPreviewParameterProvider<WalletN
assetsCount = 125,
collectionsCount = 11,
isFlickering = true,
onItemClick = { },
),
),
)

View file

@ -7,12 +7,11 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletNFTItem
private const val NFT_COLLECTIONS_CONTENT_TYPE = "NFTCollections"
internal fun LazyListScope.nftCollections(state: WalletNFTItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
internal fun LazyListScope.nftCollections(state: WalletNFTItemUM, modifier: Modifier = Modifier) {
item(key = NFT_COLLECTIONS_CONTENT_TYPE, contentType = NFT_COLLECTIONS_CONTENT_TYPE) {
WalletNFTItem(
modifier = modifier,
state = state,
onClick = onClick,
)
}
}