Updated on 2026-08-14
This commit is contained in:
parent
aea7f5f415
commit
c83ce2ada3
53 changed files with 1062 additions and 238 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
|
||||
import com.tangem.domain.nft.FetchNFTCollectionsUseCase
|
||||
import com.tangem.domain.nft.GetNFTCollectionsUseCase
|
||||
import com.tangem.domain.nft.repository.NFTRepository
|
||||
|
|
@ -37,4 +38,12 @@ internal object NFTDomainModule {
|
|||
nftRepository = nftRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFetchNFTCollectionAssetsUseCase(nftRepository: NFTRepository): FetchNFTCollectionAssetsUseCase {
|
||||
return FetchNFTCollectionAssetsUseCase(
|
||||
nftRepository = nftRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
|||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.nft.component.NFTCollectionsComponent
|
||||
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
|
||||
import com.tangem.features.onramp.component.*
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
|
|
@ -80,6 +81,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory,
|
||||
private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory,
|
||||
private val testerRouter: TesterRouter,
|
||||
private val routingFeatureToggles: RoutingFeatureToggles,
|
||||
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
|
||||
|
|
@ -397,6 +399,12 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = walletComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.NFTCollections ->
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId),
|
||||
componentFactory = nftCollectionsComponentFactory,
|
||||
)
|
||||
is AppRoute.OnboardingNote,
|
||||
is AppRoute.SaveWallet,
|
||||
is AppRoute.OnboardingOther,
|
||||
|
|
@ -729,6 +737,12 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = storiesComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.NFTCollections ->
|
||||
route.asComponentChild(
|
||||
contextProvider = contextProvider(route, contextFactory),
|
||||
params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId),
|
||||
componentFactory = nftCollectionsComponentFactory,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -277,4 +277,9 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val nextScreen: AppRoute,
|
||||
val screenSource: String,
|
||||
) : AppRoute(path = "/stories$storyId")
|
||||
|
||||
@Serializable
|
||||
data class NFTCollections(
|
||||
val userWalletId: UserWalletId,
|
||||
) : AppRoute(path = "/nft_collections/${userWalletId.stringValue}")
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ internal class DefaultNFTRuntimeStore(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow<NFTAsset> =
|
||||
override fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow<NFTAsset?> =
|
||||
collectionsRuntimeStore
|
||||
.get()
|
||||
.combine(getSalePrice(assetId)) { collectionsData, price ->
|
||||
|
|
@ -50,13 +50,17 @@ internal class DefaultNFTRuntimeStore(
|
|||
.getCollection(collectionId)
|
||||
?.getAsset(assetId)
|
||||
?.mergeWithPrice(price)
|
||||
?: NFTAsset.Error(assetId)
|
||||
}
|
||||
|
||||
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTSalePrice> = pricesRuntimeStore
|
||||
.get()
|
||||
.map { it[assetId] ?: NFTSalePrice.Empty(assetId) }
|
||||
|
||||
override suspend fun getSalePriceSync(assetId: NFTAsset.Identifier): NFTSalePrice = pricesRuntimeStore
|
||||
.getSyncOrNull()
|
||||
?.let { it[assetId] }
|
||||
?: NFTSalePrice.Empty(assetId)
|
||||
|
||||
override suspend fun saveCollections(collections: NFTCollections) {
|
||||
collectionsRuntimeStore.store(collections)
|
||||
}
|
||||
|
|
@ -72,8 +76,13 @@ internal class DefaultNFTRuntimeStore(
|
|||
?.collections
|
||||
?.firstOrNull { it.id == collectionId }
|
||||
|
||||
private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier): NFTAsset? =
|
||||
assets.firstOrNull { it.id == assetId }
|
||||
private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier): NFTAsset? = when (val assets = assets) {
|
||||
is NFTCollection.Assets.Empty,
|
||||
is NFTCollection.Assets.Loading,
|
||||
is NFTCollection.Assets.Failed,
|
||||
-> null
|
||||
is NFTCollection.Assets.Value -> assets.items.firstOrNull { it.id == assetId }
|
||||
}
|
||||
|
||||
private fun NFTCollections.mergeWithPrices(prices: Map<NFTAsset.Identifier, NFTSalePrice>): NFTCollections =
|
||||
when (val content = this.content) {
|
||||
|
|
@ -89,18 +98,23 @@ internal class DefaultNFTRuntimeStore(
|
|||
copy(
|
||||
collections = this.collections?.map { data ->
|
||||
data.copy(
|
||||
assets = data.assets.map { asset ->
|
||||
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
|
||||
assets = when (val assets = data.assets) {
|
||||
is NFTCollection.Assets.Empty,
|
||||
is NFTCollection.Assets.Loading,
|
||||
is NFTCollection.Assets.Failed,
|
||||
-> assets
|
||||
is NFTCollection.Assets.Value -> assets.copy(
|
||||
items = assets.items.map { asset ->
|
||||
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
source = this.source,
|
||||
)
|
||||
|
||||
private fun NFTAsset.mergeWithPrice(price: NFTSalePrice): NFTAsset = when (this) {
|
||||
is NFTAsset.Error -> this
|
||||
is NFTAsset.Value -> copy(
|
||||
salePrice = price,
|
||||
)
|
||||
}
|
||||
private fun NFTAsset.mergeWithPrice(price: NFTSalePrice): NFTAsset = copy(
|
||||
salePrice = price,
|
||||
)
|
||||
}
|
||||
|
|
@ -16,7 +16,9 @@ interface NFTRuntimeStore {
|
|||
|
||||
fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow<NFTAsset?>
|
||||
|
||||
fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTSalePrice?>
|
||||
fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTSalePrice>
|
||||
|
||||
suspend fun getSalePriceSync(assetId: NFTAsset.Identifier): NFTSalePrice
|
||||
|
||||
suspend fun saveCollections(collections: NFTCollections)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ object NFTSdkAssetConverter : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
|
|||
val (network, asset) = value
|
||||
val assetId = NFTSdkAssetIdentifierConverter.convert(asset.identifier)
|
||||
val collectionId = NFTSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier)
|
||||
return NFTAsset.Value(
|
||||
return NFTAsset(
|
||||
id = assetId,
|
||||
collectionId = collectionId,
|
||||
network = network,
|
||||
|
|
@ -25,23 +25,22 @@ object NFTSdkAssetConverter : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
|
|||
assetId = assetId,
|
||||
value = it.value,
|
||||
symbol = it.symbol,
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
} ?: NFTSalePrice.Empty(assetId = assetId),
|
||||
rarity = asset.rarity?.let {
|
||||
NFTAsset.Value.Rarity(
|
||||
NFTAsset.Rarity(
|
||||
rank = it.rank,
|
||||
label = it.label,
|
||||
)
|
||||
},
|
||||
media = asset.media?.let {
|
||||
NFTAsset.Value.Media(
|
||||
NFTAsset.Media(
|
||||
url = it.url,
|
||||
mimetype = it.mimetype,
|
||||
)
|
||||
},
|
||||
traits = asset.traits.map {
|
||||
NFTAsset.Value.Trait(
|
||||
NFTAsset.Trait(
|
||||
name = it.name,
|
||||
value = it.value,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.datasource.local.nft.converter
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
|
@ -23,6 +24,16 @@ object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NF
|
|||
}
|
||||
.filter {
|
||||
it.id !is NFTAsset.Identifier.Unknown
|
||||
}
|
||||
.let {
|
||||
if (it.isEmpty()) {
|
||||
NFTCollection.Assets.Empty
|
||||
} else {
|
||||
NFTCollection.Assets.Value(
|
||||
items = it,
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.utils
|
||||
package com.tangem.core.ui.components.fields
|
||||
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.withDebounce
|
||||
|
|
@ -12,7 +12,7 @@ import javax.inject.Inject
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class InputManager @Inject constructor() {
|
||||
class InputManager @Inject constructor() {
|
||||
|
||||
val query: Flow<String>
|
||||
get() = _query
|
||||
|
|
@ -9,6 +9,7 @@ import com.tangem.datasource.local.nft.NFTRuntimeStore
|
|||
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
|
||||
import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter
|
||||
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter
|
||||
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.nft.models.NFTCollections
|
||||
|
|
@ -24,9 +25,12 @@ import kotlinx.coroutines.coroutineScope
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
|
||||
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
|
||||
|
||||
@Suppress("LargeClass")
|
||||
internal class DefaultNFTRepository @Inject constructor(
|
||||
private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory,
|
||||
private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory,
|
||||
|
|
@ -34,10 +38,11 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : NFTRepository {
|
||||
|
||||
private val jobs = mutableMapOf<Network, JobHolder>()
|
||||
private val networkJobs = ConcurrentHashMap<Network, JobHolder>()
|
||||
private val collectionJobs = ConcurrentHashMap<NFTCollection.Identifier, JobHolder>()
|
||||
|
||||
private val nftRuntimeStores = mutableMapOf<String, NFTRuntimeStore>()
|
||||
private val nftPersistenceStores = mutableMapOf<String, NFTPersistenceStore>()
|
||||
private val nftRuntimeStores = ConcurrentHashMap<String, NFTRuntimeStore>()
|
||||
private val nftPersistenceStores = ConcurrentHashMap<String, NFTPersistenceStore>()
|
||||
|
||||
override fun observeCollections(userWalletId: UserWalletId, networks: List<Network>): Flow<List<NFTCollections>> =
|
||||
flow { emitAll(observeCollectionsInternal(userWalletId, networks)) }
|
||||
|
|
@ -64,15 +69,9 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
launch(dispatchers.io) {
|
||||
Either.catch {
|
||||
expireCollections(userWalletId, network)
|
||||
walletManagersFacade.getNFTCollections(userWalletId, network)
|
||||
}.onLeft {
|
||||
saveFailedStateInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
error = it,
|
||||
)
|
||||
}.onRight {
|
||||
val mergedCollections = it.mergeWithStoredAssets(userWalletId, network)
|
||||
|
||||
val collections = walletManagersFacade.getNFTCollections(userWalletId, network)
|
||||
val mergedCollections = collections.mergeWithStoredAssets(userWalletId, network)
|
||||
|
||||
saveCollectionsInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -84,14 +83,112 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
network = network,
|
||||
collections = mergedCollections,
|
||||
)
|
||||
}.onLeft {
|
||||
saveFailedStateInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
error = it,
|
||||
)
|
||||
}
|
||||
}.saveIn(getJobHolder(network))
|
||||
}.saveIn(getNetworkJobHolder(network))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.joinAll()
|
||||
}
|
||||
|
||||
override suspend fun refreshAssets(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionId: NFTCollection.Identifier,
|
||||
) = coroutineScope {
|
||||
launch(dispatchers.io) {
|
||||
Either.catch {
|
||||
expireAssets(userWalletId, network, collectionId)
|
||||
|
||||
val sdkCollectionId = NFTSdkCollectionIdentifierConverter.convertBack(collectionId)
|
||||
|
||||
val assets = walletManagersFacade.getNFTAssets(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
collectionIdentifier = sdkCollectionId,
|
||||
)
|
||||
|
||||
assets.forEach {
|
||||
val assetId = NFTSdkAssetIdentifierConverter.convert(it.identifier)
|
||||
val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId)
|
||||
if (price is NFTSalePrice.Error) {
|
||||
refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
getNFTPersistenceStore(userWalletId, network)
|
||||
.getCollectionsSync()
|
||||
?.map {
|
||||
if (it.identifier == sdkCollectionId) {
|
||||
it.copy(assets = assets)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
?.let {
|
||||
saveCollectionsInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
collections = it,
|
||||
)
|
||||
saveCollectionsInPersistence(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
collections = it,
|
||||
)
|
||||
}
|
||||
}.onLeft {
|
||||
saveFailedStateInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
error = it,
|
||||
)
|
||||
}
|
||||
}.saveIn(getCollectionJobHolder(collectionId)).join()
|
||||
}
|
||||
|
||||
private suspend fun refreshSalePrice(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
sdkCollectionId: SdkNFTCollection.Identifier,
|
||||
sdkAssetId: SdkNFTAsset.Identifier,
|
||||
) = coroutineScope {
|
||||
launch(dispatchers.io) {
|
||||
val assetId = NFTSdkAssetIdentifierConverter.convert(sdkAssetId)
|
||||
|
||||
Either.catch {
|
||||
saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Loading(assetId))
|
||||
|
||||
val sdkSalePrice =
|
||||
walletManagersFacade.getNFTSalePrice(userWalletId, network, sdkCollectionId, sdkAssetId)
|
||||
|
||||
val salePrice = if (sdkSalePrice == null) {
|
||||
NFTSalePrice.Empty(assetId)
|
||||
} else {
|
||||
NFTSalePrice.Value(
|
||||
assetId = assetId,
|
||||
value = sdkSalePrice.value,
|
||||
symbol = sdkSalePrice.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
saveSalePriceInRuntime(userWalletId, network, salePrice)
|
||||
|
||||
sdkSalePrice?.let {
|
||||
saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it)
|
||||
}
|
||||
}.onLeft {
|
||||
saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Error(assetId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun expireCollections(userWalletId: UserWalletId, network: Network) {
|
||||
val runtimeStore = getNFTRuntimeStore(userWalletId, network)
|
||||
val expiredCollections = runtimeStore
|
||||
|
|
@ -100,6 +197,23 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
runtimeStore.saveCollections(expiredCollections)
|
||||
}
|
||||
|
||||
private suspend fun expireAssets(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionId: NFTCollection.Identifier,
|
||||
) {
|
||||
val runtimeStore = getNFTRuntimeStore(userWalletId, network)
|
||||
val storedCollections = runtimeStore.getCollectionsSync()
|
||||
val expiredCollections = storedCollections
|
||||
.changeCollectionAssetsStatusSource(collectionId, StatusSource.CACHE)
|
||||
.let {
|
||||
storedCollections.copy(
|
||||
content = it,
|
||||
)
|
||||
}
|
||||
runtimeStore.saveCollections(expiredCollections)
|
||||
}
|
||||
|
||||
private suspend fun saveCollectionsInRuntime(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
|
|
@ -126,7 +240,9 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
getNFTRuntimeStore(userWalletId, network).let { store ->
|
||||
val storedCollections = store.getCollectionsSync()
|
||||
val content = storedCollections.content
|
||||
val updatedCollections = if (content is NFTCollections.Content.Collections && content.collections != null) {
|
||||
val updatedCollections = if (content is NFTCollections.Content.Collections &&
|
||||
!content.collections.isNullOrEmpty()
|
||||
) {
|
||||
// if there is any cached collections in store, then mark them as not actual and emit anyway
|
||||
storedCollections.changeStatusSource(StatusSource.ONLY_CACHE)
|
||||
} else {
|
||||
|
|
@ -148,15 +264,35 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
getNFTPersistenceStore(userWalletId, network).saveCollections(collections)
|
||||
}
|
||||
|
||||
private fun getJobHolder(network: Network): JobHolder = jobs[network] ?: run {
|
||||
private suspend fun saveSalePriceInRuntime(userWalletId: UserWalletId, network: Network, salePrice: NFTSalePrice) {
|
||||
getNFTRuntimeStore(userWalletId, network).saveSalePrice(salePrice)
|
||||
}
|
||||
|
||||
private suspend fun saveSalePriceInPersistence(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
assetId: SdkNFTAsset.Identifier,
|
||||
salePrice: SdkNFTAsset.SalePrice,
|
||||
) {
|
||||
getNFTPersistenceStore(userWalletId, network).saveSalePrice(assetId, salePrice)
|
||||
}
|
||||
|
||||
private fun getNetworkJobHolder(network: Network): JobHolder = networkJobs.getOrPut(network) {
|
||||
JobHolder().also {
|
||||
jobs[network] = it
|
||||
networkJobs[network] = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCollectionJobHolder(collectionId: NFTCollection.Identifier): JobHolder =
|
||||
collectionJobs.getOrPut(collectionId) {
|
||||
JobHolder().also {
|
||||
collectionJobs[collectionId] = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNFTPersistenceStore(userWalletId: UserWalletId, network: Network): NFTPersistenceStore {
|
||||
val storeId = (userWalletId to network).formatted()
|
||||
return nftPersistenceStores[storeId] ?: run {
|
||||
return nftPersistenceStores.getOrPut(storeId) {
|
||||
nftPersistenceStoreFactory.provide(userWalletId, network).also {
|
||||
nftPersistenceStores[storeId] = it
|
||||
}
|
||||
|
|
@ -165,7 +301,7 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
|
||||
private suspend fun getNFTRuntimeStore(userWalletId: UserWalletId, network: Network): NFTRuntimeStore {
|
||||
val storeId = (userWalletId to network).formatted()
|
||||
return nftRuntimeStores[storeId] ?: run {
|
||||
return nftRuntimeStores.getOrPut(storeId) {
|
||||
nftRuntimeStoreFactory.provide(network).also {
|
||||
nftRuntimeStores[storeId] = it
|
||||
val storedCollections = getStoredCollections(userWalletId, network)
|
||||
|
|
@ -213,7 +349,6 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
assetId = assetId,
|
||||
value = price.value,
|
||||
symbol = price.symbol,
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -227,6 +362,37 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
private fun NFTCollections.changeCollectionAssetsStatusSource(
|
||||
collectionId: NFTCollection.Identifier,
|
||||
source: StatusSource,
|
||||
) = when (val content = content) {
|
||||
is NFTCollections.Content.Collections ->
|
||||
content
|
||||
.copy(
|
||||
collections = content
|
||||
.collections
|
||||
?.map {
|
||||
if (it.id == collectionId) {
|
||||
it.changeAssetsStatusSource(source)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
},
|
||||
)
|
||||
is NFTCollections.Content.Error -> content
|
||||
}
|
||||
|
||||
private fun NFTCollection.changeAssetsStatusSource(source: StatusSource) = copy(
|
||||
assets = when (val assets = this.assets) {
|
||||
is NFTCollection.Assets.Empty -> NFTCollection.Assets.Loading
|
||||
is NFTCollection.Assets.Loading -> assets
|
||||
is NFTCollection.Assets.Failed -> assets
|
||||
is NFTCollection.Assets.Value -> assets.copy(
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private suspend fun List<SdkNFTCollection>.mergeWithStoredAssets(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
|
|
|
|||
|
|
@ -667,7 +667,7 @@ class DefaultWalletManagersFacade(
|
|||
return walletManager.getAssets(address, collectionIdentifier)
|
||||
}
|
||||
|
||||
override suspend fun getAsset(
|
||||
override suspend fun getNFTAsset(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionIdentifier: NFTCollection.Identifier,
|
||||
|
|
@ -682,6 +682,21 @@ class DefaultWalletManagersFacade(
|
|||
return walletManager.getAsset(collectionIdentifier, assetIdentifier)
|
||||
}
|
||||
|
||||
override suspend fun getNFTSalePrice(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionIdentifier: NFTCollection.Identifier,
|
||||
assetIdentifier: NFTAsset.Identifier,
|
||||
): NFTAsset.SalePrice? {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return null
|
||||
return walletManager.getSalePrice(collectionIdentifier, assetIdentifier)
|
||||
}
|
||||
|
||||
override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||
val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true
|
||||
|
|
|
|||
|
|
@ -259,13 +259,20 @@ interface WalletManagersFacade {
|
|||
collectionIdentifier: NFTCollection.Identifier,
|
||||
): List<NFTAsset>
|
||||
|
||||
suspend fun getAsset(
|
||||
suspend fun getNFTAsset(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionIdentifier: NFTCollection.Identifier,
|
||||
assetIdentifier: NFTAsset.Identifier,
|
||||
): NFTAsset?
|
||||
|
||||
suspend fun getNFTSalePrice(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionIdentifier: NFTCollection.Identifier,
|
||||
assetIdentifier: NFTAsset.Identifier,
|
||||
): NFTAsset.SalePrice?
|
||||
|
||||
/**
|
||||
* If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized]
|
||||
* value. Otherwise always return true
|
||||
|
|
|
|||
|
|
@ -3,43 +3,35 @@ package com.tangem.domain.nft.models
|
|||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
||||
sealed class NFTAsset {
|
||||
abstract val id: Identifier
|
||||
data class NFTAsset(
|
||||
val id: Identifier,
|
||||
val collectionId: NFTCollection.Identifier,
|
||||
val network: Network,
|
||||
val contractType: String,
|
||||
val owner: String?,
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val salePrice: NFTSalePrice,
|
||||
val rarity: Rarity?,
|
||||
val media: Media?,
|
||||
val traits: List<Trait>,
|
||||
val source: StatusSource,
|
||||
) {
|
||||
|
||||
data class Value(
|
||||
override val id: Identifier,
|
||||
val collectionId: NFTCollection.Identifier,
|
||||
val network: Network,
|
||||
val contractType: String,
|
||||
val owner: String?,
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val salePrice: NFTSalePrice,
|
||||
val rarity: Rarity?,
|
||||
val media: Media?,
|
||||
val traits: List<Trait>,
|
||||
val source: StatusSource,
|
||||
) : NFTAsset() {
|
||||
data class Media(
|
||||
val mimetype: String?,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
data class Media(
|
||||
val mimetype: String,
|
||||
val url: String,
|
||||
)
|
||||
data class Rarity(
|
||||
val rank: String,
|
||||
val label: String,
|
||||
)
|
||||
|
||||
data class Rarity(
|
||||
val rank: String,
|
||||
val label: String,
|
||||
)
|
||||
|
||||
data class Trait(
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
}
|
||||
|
||||
data class Error(
|
||||
override val id: Identifier,
|
||||
) : NFTAsset()
|
||||
data class Trait(
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
|
||||
sealed class Identifier {
|
||||
data class EVM(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.nft.models
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
||||
data class NFTCollection(
|
||||
|
|
@ -9,8 +10,18 @@ data class NFTCollection(
|
|||
val description: String?,
|
||||
val logoUrl: String?,
|
||||
val count: Int,
|
||||
val assets: List<NFTAsset> = emptyList(),
|
||||
val assets: Assets,
|
||||
) {
|
||||
sealed class Assets {
|
||||
data object Empty : Assets()
|
||||
data object Loading : Assets()
|
||||
data object Failed : Assets()
|
||||
data class Value(
|
||||
val items: List<NFTAsset>,
|
||||
val source: StatusSource,
|
||||
) : Assets()
|
||||
}
|
||||
|
||||
sealed class Identifier {
|
||||
data class EVM(val tokenAddress: String) : Identifier()
|
||||
data class TON(val contractAddress: String?) : Identifier()
|
||||
|
|
|
|||
|
|
@ -27,4 +27,31 @@ data class NFTCollections(
|
|||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun List<NFTCollections>.allCollectionsFailed() = this.all {
|
||||
it.content is NFTCollections.Content.Error
|
||||
}
|
||||
|
||||
fun List<NFTCollections>.anyCollectionFailed() = this.any {
|
||||
it.content is NFTCollections.Content.Error ||
|
||||
it.content is NFTCollections.Content.Collections &&
|
||||
it.content.source == StatusSource.ONLY_CACHE
|
||||
}
|
||||
|
||||
fun List<NFTCollections>.allLoadedCollectionsEmpty() = this
|
||||
.map { it.content }
|
||||
.filterIsInstance<NFTCollections.Content.Collections>()
|
||||
.all { it.collections.isNullOrEmpty() }
|
||||
|
||||
fun List<NFTCollections>.allCollectionsLoaded() = this.all {
|
||||
val content = it.content
|
||||
content is NFTCollections.Content.Collections &&
|
||||
content.source != StatusSource.CACHE
|
||||
}
|
||||
|
||||
fun List<NFTCollections>.allCollectionsEmpty() = this.all {
|
||||
val content = it.content
|
||||
content is NFTCollections.Content.Collections &&
|
||||
content.collections.isNullOrEmpty()
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.domain.nft.models
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class NFTSalePrice {
|
||||
|
|
@ -10,6 +9,10 @@ sealed class NFTSalePrice {
|
|||
override val assetId: NFTAsset.Identifier,
|
||||
) : NFTSalePrice()
|
||||
|
||||
data class Loading(
|
||||
override val assetId: NFTAsset.Identifier,
|
||||
) : NFTSalePrice()
|
||||
|
||||
data class Error(
|
||||
override val assetId: NFTAsset.Identifier,
|
||||
) : NFTSalePrice()
|
||||
|
|
@ -18,6 +21,5 @@ sealed class NFTSalePrice {
|
|||
override val assetId: NFTAsset.Identifier,
|
||||
val value: BigDecimal,
|
||||
val symbol: String,
|
||||
val source: StatusSource,
|
||||
) : NFTSalePrice()
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.nft
|
||||
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.nft.repository.NFTRepository
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
class FetchNFTCollectionAssetsUseCase(
|
||||
private val nftRepository: NFTRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier) {
|
||||
nftRepository.refreshAssets(userWalletId, network, collectionId)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ class GetNFTCollectionsUseCase(
|
|||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun launch(userWalletId: UserWalletId): Flow<List<NFTCollections>> = currenciesRepository
|
||||
operator fun invoke(userWalletId: UserWalletId): Flow<List<NFTCollections>> = currenciesRepository
|
||||
.getWalletCurrenciesUpdates(userWalletId)
|
||||
.flatMapLatest {
|
||||
val networks = it
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.nft.repository
|
||||
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.nft.models.NFTCollections
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
|
@ -9,4 +10,6 @@ interface NFTRepository {
|
|||
fun observeCollections(userWalletId: UserWalletId, networks: List<Network>): Flow<List<NFTCollections>>
|
||||
|
||||
suspend fun refreshCollections(userWalletId: UserWalletId, networks: List<Network>)
|
||||
|
||||
suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier)
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ dependencies {
|
|||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -10,5 +10,6 @@ internal data class NFTCollectionUM(
|
|||
val logoUrl: String?,
|
||||
val description: TextReference,
|
||||
val assets: NFTCollectionAssetsListUM,
|
||||
val isExpanded: Boolean,
|
||||
val onExpandClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
internal data class NFTCollectionsStateUM(
|
||||
val onBackClick: () -> Unit,
|
||||
val content: NFTCollectionsUM,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -125,6 +125,7 @@ internal object WalletScreenPreviewData {
|
|||
collectionsCount = 1,
|
||||
assetsCount = 3,
|
||||
isFlickering = false,
|
||||
onItemClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -56,4 +56,7 @@ internal interface InnerWalletRouter {
|
|||
|
||||
/** Open scan failed dialog */
|
||||
fun openScanFailedDialog(onTryAgain: () -> Unit)
|
||||
|
||||
/** Open NFT collections screen */
|
||||
fun openNFTCollectionsScreen(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -713,7 +713,6 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi
|
|||
nftCollections(
|
||||
modifier = itemModifier,
|
||||
state = it.nftState,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue