Updated on 2026-08-14
This commit is contained in:
parent
52e7b5e58c
commit
27921d8c3a
23 changed files with 674 additions and 497 deletions
|
|
@ -24,6 +24,7 @@ data class EarnTokenResponse(
|
||||||
@Json(name = "symbol") val symbol: String,
|
@Json(name = "symbol") val symbol: String,
|
||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
@Json(name = "address") val address: String,
|
@Json(name = "address") val address: String,
|
||||||
|
@Json(name = "decimalCount") val decimalCount: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.tangem.data.earn.datastore
|
package com.tangem.data.earn.datastore
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||||
import com.tangem.domain.models.earn.EarnNetworks
|
import com.tangem.domain.models.earn.EarnNetworks
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -8,6 +7,6 @@ import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class EarnNetworksStore @Inject constructor() :
|
class EarnNetworksStore @Inject constructor() :
|
||||||
RuntimeStateStore<EarnNetworks> by RuntimeStateStore(
|
RuntimeStateStore<EarnNetworks?> by RuntimeStateStore(
|
||||||
defaultValue = Either.Right(emptyList()),
|
defaultValue = null,
|
||||||
)
|
)
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.tangem.data.earn.datastore
|
package com.tangem.data.earn.datastore
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||||
import com.tangem.domain.models.earn.EarnTopToken
|
import com.tangem.domain.models.earn.EarnTopToken
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -8,6 +7,6 @@ import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class EarnTopTokensStore @Inject constructor() :
|
class EarnTopTokensStore @Inject constructor() :
|
||||||
RuntimeStateStore<EarnTopToken> by RuntimeStateStore(
|
RuntimeStateStore<EarnTopToken?> by RuntimeStateStore(
|
||||||
defaultValue = Either.Right(emptyList()),
|
defaultValue = null,
|
||||||
)
|
)
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package com.tangem.data.earn.di
|
package com.tangem.data.earn.di
|
||||||
|
|
||||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||||
import com.tangem.data.earn.DefaultEarnErrorResolver
|
import com.tangem.data.earn.DefaultEarnErrorResolver
|
||||||
import com.tangem.data.earn.repository.DefaultEarnRepository
|
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|
||||||
import com.tangem.data.earn.datastore.EarnNetworksStore
|
import com.tangem.data.earn.datastore.EarnNetworksStore
|
||||||
import com.tangem.data.earn.datastore.EarnTopTokensStore
|
import com.tangem.data.earn.datastore.EarnTopTokensStore
|
||||||
|
import com.tangem.data.earn.repository.DefaultEarnRepository
|
||||||
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.earn.EarnErrorResolver
|
import com.tangem.domain.earn.EarnErrorResolver
|
||||||
import com.tangem.domain.earn.repository.EarnRepository
|
import com.tangem.domain.earn.repository.EarnRepository
|
||||||
|
|
@ -29,19 +29,19 @@ internal object EarnDataModule {
|
||||||
tangemTechApi: TangemTechApi,
|
tangemTechApi: TangemTechApi,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
cryptoCurrencyFactory: CryptoCurrencyFactory,
|
|
||||||
earnNetworksStore: EarnNetworksStore,
|
earnNetworksStore: EarnNetworksStore,
|
||||||
earnTopTokensStore: EarnTopTokensStore,
|
earnTopTokensStore: EarnTopTokensStore,
|
||||||
earnErrorResolver: EarnErrorResolver,
|
earnErrorResolver: EarnErrorResolver,
|
||||||
|
excludedBlockchains: ExcludedBlockchains,
|
||||||
): EarnRepository {
|
): EarnRepository {
|
||||||
return DefaultEarnRepository(
|
return DefaultEarnRepository(
|
||||||
tangemTechApi = tangemTechApi,
|
tangemTechApi = tangemTechApi,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
cryptoCurrencyFactory = cryptoCurrencyFactory,
|
|
||||||
earnNetworksStore = earnNetworksStore,
|
earnNetworksStore = earnNetworksStore,
|
||||||
earnTopTokensStore = earnTopTokensStore,
|
earnTopTokensStore = earnTopTokensStore,
|
||||||
earnErrorResolver = earnErrorResolver,
|
earnErrorResolver = earnErrorResolver,
|
||||||
|
excludedBlockchains = excludedBlockchains,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.earn.repository
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||||
import com.tangem.data.earn.converter.EarnTokenConverter
|
import com.tangem.data.earn.converter.EarnTokenConverter
|
||||||
|
|
@ -35,12 +36,16 @@ internal class DefaultEarnRepository(
|
||||||
private val tangemTechApi: TangemTechApi,
|
private val tangemTechApi: TangemTechApi,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val cryptoCurrencyFactory: CryptoCurrencyFactory,
|
|
||||||
private val earnNetworksStore: EarnNetworksStore,
|
private val earnNetworksStore: EarnNetworksStore,
|
||||||
private val earnTopTokensStore: EarnTopTokensStore,
|
private val earnTopTokensStore: EarnTopTokensStore,
|
||||||
private val earnErrorResolver: EarnErrorResolver,
|
private val earnErrorResolver: EarnErrorResolver,
|
||||||
|
private val excludedBlockchains: ExcludedBlockchains,
|
||||||
) : EarnRepository {
|
) : EarnRepository {
|
||||||
|
|
||||||
|
private val cryptoCurrencyFactory: CryptoCurrencyFactory by lazy {
|
||||||
|
CryptoCurrencyFactory(excludedBlockchains = excludedBlockchains)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow {
|
override fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow {
|
||||||
val batchFetcher = EarnTokensBatchFetcher(
|
val batchFetcher = EarnTokensBatchFetcher(
|
||||||
tangemTechApi = tangemTechApi,
|
tangemTechApi = tangemTechApi,
|
||||||
|
|
@ -73,7 +78,7 @@ internal class DefaultEarnRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeEarnNetworks(): Flow<EarnNetworks> {
|
override fun observeEarnNetworks(): Flow<EarnNetworks?> {
|
||||||
return earnNetworksStore.get()
|
return earnNetworksStore.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +105,7 @@ internal class DefaultEarnRepository(
|
||||||
EarnTokenWithCurrency(
|
EarnTokenWithCurrency(
|
||||||
earnToken = earnToken,
|
earnToken = earnToken,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +116,7 @@ internal class DefaultEarnRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeTopEarnTokens(): Flow<EarnTopToken> {
|
override fun observeTopEarnTokens(): Flow<EarnTopToken?> {
|
||||||
return earnTopTokensStore.get()
|
return earnTopTokensStore.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,7 +156,7 @@ internal fun createCryptoCurrencyForEarnToken(
|
||||||
rawId = CryptoCurrency.RawID(earnToken.token.id),
|
rawId = CryptoCurrency.RawID(earnToken.token.id),
|
||||||
name = earnToken.token.name,
|
name = earnToken.token.name,
|
||||||
symbol = earnToken.token.symbol,
|
symbol = earnToken.token.symbol,
|
||||||
decimals = blockchain.decimals(),
|
decimals = earnToken.token.decimalCount ?: blockchain.decimals(),
|
||||||
contractAddress = earnToken.token.address,
|
contractAddress = earnToken.token.address,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.tangem.data.earn.repository.batch
|
package com.tangem.data.earn.repository.batch
|
||||||
|
|
||||||
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||||
import com.tangem.data.earn.converter.EarnTokenConverter
|
import com.tangem.data.earn.converter.EarnTokenConverter
|
||||||
import com.tangem.data.earn.repository.DefaultEarnRepository.Companion.FIRST_PAGE
|
import com.tangem.data.earn.repository.DefaultEarnRepository.Companion.FIRST_PAGE
|
||||||
|
|
@ -88,6 +90,7 @@ internal class EarnTokensBatchFetcher(
|
||||||
EarnTokenWithCurrency(
|
EarnTokenWithCurrency(
|
||||||
earnToken = earnToken,
|
earnToken = earnToken,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ interface EarnRepository {
|
||||||
*/
|
*/
|
||||||
suspend fun fetchEarnNetworks(type: String)
|
suspend fun fetchEarnNetworks(type: String)
|
||||||
|
|
||||||
fun observeEarnNetworks(): Flow<EarnNetworks>
|
fun observeEarnNetworks(): Flow<EarnNetworks?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch top N earn tokens by isForEarn = true and hold it in runtime store.
|
* Fetch top N earn tokens by isForEarn = true and hold it in runtime store.
|
||||||
*/
|
*/
|
||||||
suspend fun fetchTopEarnTokens(limit: Int)
|
suspend fun fetchTopEarnTokens(limit: Int)
|
||||||
|
|
||||||
fun observeTopEarnTokens(): Flow<EarnTopToken>
|
fun observeTopEarnTokens(): Flow<EarnTopToken?>
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.domain.earn.usecase
|
package com.tangem.domain.earn.usecase
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.earn.repository.EarnRepository
|
import com.tangem.domain.earn.repository.EarnRepository
|
||||||
import com.tangem.domain.models.earn.EarnNetwork
|
import com.tangem.domain.models.earn.EarnNetwork
|
||||||
|
|
@ -27,11 +28,11 @@ class GetEarnNetworksUseCase(
|
||||||
earnRepository.observeEarnNetworks(),
|
earnRepository.observeEarnNetworks(),
|
||||||
observeMyNetworkIds(),
|
observeMyNetworkIds(),
|
||||||
) { earn, myNetworkIds ->
|
) { earn, myNetworkIds ->
|
||||||
earn.map { earnNetworks ->
|
earn?.map { earnNetworks ->
|
||||||
earnNetworks.map { network ->
|
earnNetworks.map { network ->
|
||||||
network.copy(isAdded = network.networkId in myNetworkIds)
|
network.copy(isAdded = network.networkId in myNetworkIds)
|
||||||
}
|
}
|
||||||
}
|
} ?: Either.Right(emptyList())
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class GetTopEarnTokensUseCase(
|
||||||
private val repository: EarnRepository,
|
private val repository: EarnRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
operator fun invoke(): Flow<EarnTopToken> {
|
operator fun invoke(): Flow<EarnTopToken?> {
|
||||||
return repository.observeTopEarnTokens().distinctUntilChanged()
|
return repository.observeTopEarnTokens().distinctUntilChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import kotlinx.serialization.Serializable
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EarnTokenWithCurrency(
|
data class EarnTokenWithCurrency(
|
||||||
|
val networkName: String,
|
||||||
val earnToken: EarnToken,
|
val earnToken: EarnToken,
|
||||||
val cryptoCurrency: CryptoCurrency,
|
val cryptoCurrency: CryptoCurrency,
|
||||||
)
|
)
|
||||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||||
import com.tangem.domain.markets.TokenMarketInfo
|
import com.tangem.domain.markets.TokenMarketInfo
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponent {
|
interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponent {
|
||||||
|
|
||||||
|
|
@ -22,6 +23,7 @@ interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponen
|
||||||
fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId)
|
fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class TokenToAdd(
|
data class TokenToAdd(
|
||||||
val network: TokenMarketInfo.Network,
|
val network: TokenMarketInfo.Network,
|
||||||
val id: CryptoCurrency.RawID,
|
val id: CryptoCurrency.RawID,
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,10 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
|
||||||
// suspend until all required data is selected
|
// suspend until all required data is selected
|
||||||
val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first()
|
val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first()
|
||||||
|
|
||||||
val isTokenAlreadyAdded = selectedPortfolioValue.account.addedMarketNetworks
|
val isTokenAlreadyAdded = getAccountCurrencyStatusUseCase.invokeSync(
|
||||||
.any { it.networkId == selectedNetworkValue.selectedNetwork.networkId }
|
userWalletId = selectedPortfolioValue.userWallet.walletId,
|
||||||
|
currency = selectedNetworkValue.cryptoCurrency,
|
||||||
|
).isSome()
|
||||||
|
|
||||||
if (isTokenAlreadyAdded) {
|
if (isTokenAlreadyAdded) {
|
||||||
finishSuccessFlow(
|
finishSuccessFlow(
|
||||||
|
|
@ -165,7 +167,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
|
||||||
): AvailableToAddData? {
|
): AvailableToAddData? {
|
||||||
val portfolioData = portfolioFetcher.data.firstOrNull() ?: return null
|
val portfolioData = portfolioFetcher.data.firstOrNull() ?: return null
|
||||||
|
|
||||||
val availableToOpenWallets = portfolioData.balances.mapNotNull { (walletId, balance) ->
|
val availableToAddInWallets = portfolioData.balances.mapNotNull { (walletId, balance) ->
|
||||||
val wallet = balance.userWallet
|
val wallet = balance.userWallet
|
||||||
val accounts = balance.accountsBalance.accountStatuses
|
val accounts = balance.accountsBalance.accountStatuses
|
||||||
|
|
||||||
|
|
@ -206,9 +208,9 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
if (availableToOpenWallets.isEmpty()) return null
|
if (availableToAddInWallets.isEmpty()) return null
|
||||||
|
|
||||||
return AvailableToAddData(availableToAddWallets = availableToOpenWallets)
|
return AvailableToAddData(availableToAddWallets = availableToAddInWallets)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createCryptoCurrency(
|
private suspend fun createCryptoCurrency(
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun EarnListPlaceholder(modifier: Modifier = Modifier) {
|
internal fun EarnListPlaceholder(modifier: Modifier = Modifier, placeholderCount: Int = PLACEHOLDER_ITEMS_COUNT) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
repeat(PLACEHOLDER_ITEMS_COUNT) {
|
repeat(placeholderCount) {
|
||||||
EarnItemPlaceholder()
|
EarnItemPlaceholder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,59 +4,38 @@ import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.ripple
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.onFirstVisible
|
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.withStyle
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastForEach
|
|
||||||
import com.tangem.common.ui.markets.MarketsListItem
|
|
||||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
|
||||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
|
||||||
import com.tangem.common.ui.news.ArticleCard
|
|
||||||
import com.tangem.common.ui.news.ArticleConfigUM
|
|
||||||
import com.tangem.common.ui.news.ShowMoreArticlesCard
|
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.SpacerH
|
import com.tangem.core.ui.components.SpacerH
|
||||||
import com.tangem.core.ui.components.SpacerW
|
import com.tangem.core.ui.components.SpacerW
|
||||||
import com.tangem.core.ui.components.UnableToLoadData
|
|
||||||
import com.tangem.core.ui.components.block.BlockCard
|
|
||||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
|
||||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
|
||||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
|
||||||
import com.tangem.core.ui.extensions.conditional
|
import com.tangem.core.ui.extensions.conditional
|
||||||
import com.tangem.core.ui.extensions.resolveReference
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
|
||||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR
|
import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR
|
||||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||||
|
import com.tangem.features.feed.ui.feed.components.*
|
||||||
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||||
import com.tangem.features.feed.ui.feed.state.*
|
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||||
|
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||||
|
import com.tangem.features.feed.ui.feed.state.GlobalFeedState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FeedListHeader(
|
internal fun FeedListHeader(
|
||||||
|
|
@ -167,6 +146,13 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
||||||
trendingArticle = state.trendingArticle,
|
trendingArticle = state.trendingArticle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
EarnBlock(
|
||||||
|
onSeeAllClick = {
|
||||||
|
// TODO handle in [REDACTED_TASK_KEY]
|
||||||
|
},
|
||||||
|
earnListUM = null, // TODO in [REDACTED_TASK_KEY]
|
||||||
|
)
|
||||||
|
|
||||||
MarketPulseBlock(
|
MarketPulseBlock(
|
||||||
marketChartConfig = state.marketChartConfig,
|
marketChartConfig = state.marketChartConfig,
|
||||||
feedListCallbacks = state.feedListCallbacks,
|
feedListCallbacks = state.feedListCallbacks,
|
||||||
|
|
@ -174,400 +160,6 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedListCallbacks) {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = marketChart,
|
|
||||||
label = "MarketBlockChartAnimation",
|
|
||||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
|
||||||
) { currentChart ->
|
|
||||||
when (currentChart) {
|
|
||||||
is MarketChartUM.Content,
|
|
||||||
MarketChartUM.Loading,
|
|
||||||
is MarketChartUM.LoadingError,
|
|
||||||
-> {
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Header(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResourceSafe(R.string.markets_common_title),
|
|
||||||
style = TangemTheme.typography.h3,
|
|
||||||
color = TangemTheme.colors.text.primary1,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
|
||||||
)
|
|
||||||
|
|
||||||
SpacerH(12.dp)
|
|
||||||
|
|
||||||
Charts(
|
|
||||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
marketChart = currentChart,
|
|
||||||
)
|
|
||||||
|
|
||||||
SpacerH(32.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
null -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
|
||||||
val onSeeAllClick by rememberUpdatedState {
|
|
||||||
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
|
|
||||||
}
|
|
||||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
|
||||||
Header(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
|
||||||
style = TangemTheme.typography.h3,
|
|
||||||
color = TangemTheme.colors.text.primary1,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onSeeAllClick = { onSeeAllClick() },
|
|
||||||
)
|
|
||||||
|
|
||||||
LazyRow(
|
|
||||||
modifier = Modifier.padding(vertical = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
state = rememberLazyListState(),
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = marketChartConfig.getFilterPreset(),
|
|
||||||
key = SortByTypeUM::name,
|
|
||||||
) { sortByTypeUM ->
|
|
||||||
FilterChip(
|
|
||||||
sortByTypeUM = sortByTypeUM,
|
|
||||||
isSelected = sortByTypeUM == marketChartConfig.currentSortByType,
|
|
||||||
onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpacerH(12.dp)
|
|
||||||
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = marketChartConfig.currentSortByType,
|
|
||||||
label = "MarketPulseChartAnimation",
|
|
||||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
|
||||||
) { currentSortType ->
|
|
||||||
marketChartConfig.marketCharts[currentSortType]?.let { chart ->
|
|
||||||
Charts(
|
|
||||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
marketChart = chart,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpacerH(32.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
|
||||||
AnimatedContent(news.newsUMState) { newsUMState ->
|
|
||||||
when (newsUMState) {
|
|
||||||
NewsUMState.LOADING -> NewsLoadingBlock()
|
|
||||||
NewsUMState.CONTENT -> {
|
|
||||||
if (news.content.isNotEmpty()) {
|
|
||||||
NewsContentBlock(
|
|
||||||
feedListCallbacks = feedListCallbacks,
|
|
||||||
news = news,
|
|
||||||
trendingArticle = trendingArticle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NewsUMState.ERROR -> NewsErrorBlock(onRetryClick = news.onRetryClicked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
@Composable
|
|
||||||
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val articlesReadStatus = remember(news.content) {
|
|
||||||
news.content.map { it.isViewed }
|
|
||||||
}
|
|
||||||
LaunchedEffect(articlesReadStatus) {
|
|
||||||
listState.requestScrollToItem(0)
|
|
||||||
}
|
|
||||||
Column {
|
|
||||||
Header(
|
|
||||||
title = {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
text = stringResourceSafe(R.string.common_news),
|
|
||||||
style = TangemTheme.typography.h3,
|
|
||||||
color = TangemTheme.colors.text.primary1,
|
|
||||||
)
|
|
||||||
|
|
||||||
SpacerW(4.dp)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20),
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
SpacerW(2.dp)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = buildAnnotatedString {
|
|
||||||
withStyle(
|
|
||||||
SpanStyle().copy(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
GRADIENT_START to LinearGradientFirstPart,
|
|
||||||
GRADIENT_END to LinearGradientSecondPart,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
append(stringResourceSafe(R.string.feed_tangem_ai))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = TangemTheme.typography.subtitle1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
|
||||||
)
|
|
||||||
SpacerH(12.dp)
|
|
||||||
|
|
||||||
if (trendingArticle != null) {
|
|
||||||
Column {
|
|
||||||
ArticleCard(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
articleConfigUM = trendingArticle,
|
|
||||||
onArticleClick = { feedListCallbacks.onArticleClick(trendingArticle.id) },
|
|
||||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
|
||||||
)
|
|
||||||
SpacerH(12.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyRow(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
state = listState,
|
|
||||||
) {
|
|
||||||
itemsIndexed(
|
|
||||||
items = news.content,
|
|
||||||
key = { _, article -> article.id },
|
|
||||||
contentType = { _, _ -> "article" },
|
|
||||||
) { index, article ->
|
|
||||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
|
||||||
Modifier.onFirstVisible(
|
|
||||||
minFractionVisible = 0.5f,
|
|
||||||
callback = feedListCallbacks.onSliderScroll,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
ArticleCard(
|
|
||||||
articleConfigUM = article,
|
|
||||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
|
||||||
modifier = articleModifier
|
|
||||||
.heightIn(min = 164.dp)
|
|
||||||
.width(216.dp),
|
|
||||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item(contentType = "show_more") {
|
|
||||||
ShowMoreArticlesCard(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(216.dp)
|
|
||||||
.heightIn(min = 164.dp)
|
|
||||||
.onFirstVisible(
|
|
||||||
minFractionVisible = 0.5f,
|
|
||||||
callback = feedListCallbacks.onSliderEndReached,
|
|
||||||
),
|
|
||||||
onClick = { feedListCallbacks.onOpenAllNews(true) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpacerH(32.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
title()
|
|
||||||
|
|
||||||
SecondarySmallButton(
|
|
||||||
config = SmallButtonConfig(
|
|
||||||
text = TextReference.Res(R.string.common_see_all),
|
|
||||||
onClick = onSeeAllClick,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Charts(
|
|
||||||
marketChart: MarketChartUM,
|
|
||||||
onItemClick: (MarketsListItemUM) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
BlockCard(
|
|
||||||
modifier = modifier,
|
|
||||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.primary),
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
when (marketChart) {
|
|
||||||
MarketChartUM.Loading -> {
|
|
||||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
|
||||||
MarketsListItemPlaceholder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is MarketChartUM.LoadingError -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 35.dp, horizontal = 10.dp),
|
|
||||||
) {
|
|
||||||
UnableToLoadData(
|
|
||||||
onRetryClick = marketChart.onRetryClicked,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is MarketChartUM.Content -> {
|
|
||||||
marketChart.items.fastForEach { chart ->
|
|
||||||
MarketsListItem(
|
|
||||||
model = chart,
|
|
||||||
onClick = { onItemClick(chart) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun FilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: () -> Unit) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(shape = RoundedCornerShape(12.dp))
|
|
||||||
.background(
|
|
||||||
color = if (isSelected) {
|
|
||||||
TangemTheme.colors.button.primary
|
|
||||||
} else {
|
|
||||||
TangemTheme.colors.button.secondary
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.clickable(
|
|
||||||
onClick = onClick,
|
|
||||||
indication = ripple(),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 24.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = sortByTypeUM.text.resolveReference(),
|
|
||||||
style = TangemTheme.typography.button,
|
|
||||||
color = if (isSelected) {
|
|
||||||
TangemTheme.colors.text.primary2
|
|
||||||
} else {
|
|
||||||
TangemTheme.colors.text.primary1
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun FeedListGlobalError(onRetryClick: () -> Unit, currentDate: String, modifier: Modifier = Modifier) {
|
|
||||||
val background = LocalMainBottomSheetColor.current.value
|
|
||||||
Column(modifier) {
|
|
||||||
DateBlock(currentDate)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.drawBehind { drawRect(background) }
|
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
UnableToLoadData(onRetryClick = onRetryClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
|
||||||
Column {
|
|
||||||
Header(
|
|
||||||
title = {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
text = stringResourceSafe(R.string.common_news),
|
|
||||||
style = TangemTheme.typography.h3,
|
|
||||||
color = TangemTheme.colors.text.primary1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSeeAllClick = {},
|
|
||||||
)
|
|
||||||
SpacerH(12.dp)
|
|
||||||
BlockCard(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
|
||||||
) {
|
|
||||||
UnableToLoadData(
|
|
||||||
onRetryClick = onRetryClick,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 35.dp, horizontal = 10.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SpacerH(32.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ColumnScope.DateBlock(currentDate: String) {
|
|
||||||
SpacerH(20.dp)
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp),
|
|
||||||
text = stringResourceSafe(R.string.feed_market_and_news),
|
|
||||||
style = TangemTheme.typography.h2,
|
|
||||||
color = TangemTheme.colors.text.primary1,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp),
|
|
||||||
text = currentDate,
|
|
||||||
style = TangemTheme.typography.h2,
|
|
||||||
color = TangemTheme.colors.text.tertiary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
|
||||||
private const val FOURTH_ITEM_INDEX = 3
|
|
||||||
private const val GRADIENT_START = 0f
|
|
||||||
private const val GRADIENT_END = 0.5f
|
|
||||||
private val LinearGradientFirstPart = Color(0xFF635EEC)
|
|
||||||
private val LinearGradientSecondPart = Color(0xFFE05AED)
|
|
||||||
|
|
||||||
@Preview(showBackground = true, heightDp = 1500)
|
@Preview(showBackground = true, heightDp = 1500)
|
||||||
@Composable
|
@Composable
|
||||||
private fun FeedListPreview() {
|
private fun FeedListPreview() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.tangem.features.feed.ui.feed.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.components.RectangleShimmer
|
||||||
|
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||||
|
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title: @Composable () -> Unit) {
|
||||||
|
AnimatedContent(isLoading) { animatedState ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (animatedState) {
|
||||||
|
RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp))
|
||||||
|
} else {
|
||||||
|
title()
|
||||||
|
SecondarySmallButton(
|
||||||
|
config = SmallButtonConfig(
|
||||||
|
text = TextReference.Res(R.string.common_see_all),
|
||||||
|
onClick = onSeeAllClick,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.tangem.features.feed.ui.feed.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ChartsFilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: () -> Unit) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(shape = RoundedCornerShape(12.dp))
|
||||||
|
.background(
|
||||||
|
color = if (isSelected) {
|
||||||
|
TangemTheme.colors.button.primary
|
||||||
|
} else {
|
||||||
|
TangemTheme.colors.button.secondary
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick,
|
||||||
|
indication = ripple(),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 24.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = sortByTypeUM.text.resolveReference(),
|
||||||
|
style = TangemTheme.typography.button,
|
||||||
|
color = if (isSelected) {
|
||||||
|
TangemTheme.colors.text.primary2
|
||||||
|
} else {
|
||||||
|
TangemTheme.colors.text.primary1
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.tangem.features.feed.ui.feed.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DateBlock(currentDate: String) {
|
||||||
|
SpacerH(20.dp)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
text = stringResourceSafe(R.string.feed_market_and_news),
|
||||||
|
style = TangemTheme.typography.h2,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
text = currentDate,
|
||||||
|
style = TangemTheme.typography.h2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.tangem.features.feed.ui.feed.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.components.UnableToLoadData
|
||||||
|
import com.tangem.core.ui.components.block.BlockCard
|
||||||
|
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||||
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.features.feed.ui.earn.components.EarnListItem
|
||||||
|
import com.tangem.features.feed.ui.earn.components.EarnListPlaceholder
|
||||||
|
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||||
|
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modifier: Modifier = Modifier) {
|
||||||
|
if (earnListUM == null) return
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Header(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.markets_earn_common_title),
|
||||||
|
style = TangemTheme.typography.h3,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSeeAllClick = onSeeAllClick,
|
||||||
|
isLoading = earnListUM is EarnListUM.Loading,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerH(12.dp)
|
||||||
|
|
||||||
|
BlockCard(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||||
|
) {
|
||||||
|
AnimatedContent(targetState = earnListUM) { earnListState ->
|
||||||
|
when (earnListState) {
|
||||||
|
is EarnListUM.Content -> EarnContentBlock(items = earnListState.items)
|
||||||
|
is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListState.onRetryClicked)
|
||||||
|
EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpacerH(32.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EarnErrorBlock(onRetryClick: () -> Unit) {
|
||||||
|
UnableToLoadData(
|
||||||
|
onRetryClick = onRetryClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EarnContentBlock(items: ImmutableList<EarnListItemUM>) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
items.fastForEach { item ->
|
||||||
|
EarnListItem(item = item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PLACEHOLDER_ITEM_COUNT = 5
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.tangem.features.feed.ui.feed.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.components.UnableToLoadData
|
||||||
|
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun FeedListGlobalError(onRetryClick: () -> Unit, currentDate: String, modifier: Modifier = Modifier) {
|
||||||
|
val background = LocalMainBottomSheetColor.current.value
|
||||||
|
Column(modifier) {
|
||||||
|
DateBlock(currentDate)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.drawBehind { drawRect(background) }
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
UnableToLoadData(onRetryClick = onRetryClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.tangem.features.feed.ui.feed
|
package com.tangem.features.feed.ui.feed.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
|
@ -14,6 +14,8 @@ import com.tangem.common.ui.news.TrendingLoadingArticle
|
||||||
import com.tangem.core.ui.components.RectangleShimmer
|
import com.tangem.core.ui.components.RectangleShimmer
|
||||||
import com.tangem.core.ui.components.SpacerH
|
import com.tangem.core.ui.components.SpacerH
|
||||||
import com.tangem.core.ui.components.block.BlockCard
|
import com.tangem.core.ui.components.block.BlockCard
|
||||||
|
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -94,7 +96,10 @@ internal fun NewsLoadingBlock() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChartsLoading(modifier: Modifier = Modifier) {
|
private fun ChartsLoading(modifier: Modifier = Modifier) {
|
||||||
BlockCard(modifier) {
|
BlockCard(
|
||||||
|
modifier = modifier,
|
||||||
|
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
package com.tangem.features.feed.ui.feed.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import com.tangem.common.ui.markets.MarketsListItem
|
||||||
|
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||||
|
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.components.UnableToLoadData
|
||||||
|
import com.tangem.core.ui.components.block.BlockCard
|
||||||
|
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||||
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||||
|
import com.tangem.features.feed.ui.feed.state.FeedListCallbacks
|
||||||
|
import com.tangem.features.feed.ui.feed.state.MarketChartConfig
|
||||||
|
import com.tangem.features.feed.ui.feed.state.MarketChartUM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedListCallbacks) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = marketChart,
|
||||||
|
label = "MarketBlockChartAnimation",
|
||||||
|
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||||
|
) { currentChart ->
|
||||||
|
when (currentChart) {
|
||||||
|
is MarketChartUM.Content,
|
||||||
|
MarketChartUM.Loading,
|
||||||
|
is MarketChartUM.LoadingError,
|
||||||
|
-> {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Header(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.markets_common_title),
|
||||||
|
style = TangemTheme.typography.h3,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerH(12.dp)
|
||||||
|
|
||||||
|
Charts(
|
||||||
|
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
marketChart = currentChart,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerH(32.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||||
|
val onSeeAllClick by rememberUpdatedState {
|
||||||
|
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
|
||||||
|
}
|
||||||
|
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||||
|
Header(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||||
|
style = TangemTheme.typography.h3,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSeeAllClick = { onSeeAllClick() },
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
state = rememberLazyListState(),
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = marketChartConfig.getFilterPreset(),
|
||||||
|
key = SortByTypeUM::name,
|
||||||
|
) { sortByTypeUM ->
|
||||||
|
ChartsFilterChip(
|
||||||
|
sortByTypeUM = sortByTypeUM,
|
||||||
|
isSelected = sortByTypeUM == marketChartConfig.currentSortByType,
|
||||||
|
onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpacerH(12.dp)
|
||||||
|
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = marketChartConfig.currentSortByType,
|
||||||
|
label = "MarketPulseChartAnimation",
|
||||||
|
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||||
|
) { currentSortType ->
|
||||||
|
marketChartConfig.marketCharts[currentSortType]?.let { chart ->
|
||||||
|
Charts(
|
||||||
|
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
marketChart = chart,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpacerH(32.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Charts(
|
||||||
|
marketChart: MarketChartUM,
|
||||||
|
onItemClick: (MarketsListItemUM) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
BlockCard(
|
||||||
|
modifier = modifier,
|
||||||
|
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
when (marketChart) {
|
||||||
|
MarketChartUM.Loading -> {
|
||||||
|
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||||
|
MarketsListItemPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MarketChartUM.LoadingError -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||||
|
) {
|
||||||
|
UnableToLoadData(
|
||||||
|
onRetryClick = marketChart.onRetryClicked,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MarketChartUM.Content -> {
|
||||||
|
marketChart.items.fastForEach { chart ->
|
||||||
|
MarketsListItem(
|
||||||
|
model = chart,
|
||||||
|
onClick = { onItemClick(chart) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
package com.tangem.features.feed.ui.feed.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.onFirstVisible
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.common.ui.news.ArticleCard
|
||||||
|
import com.tangem.common.ui.news.ArticleConfigUM
|
||||||
|
import com.tangem.common.ui.news.ShowMoreArticlesCard
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.components.SpacerW
|
||||||
|
import com.tangem.core.ui.components.UnableToLoadData
|
||||||
|
import com.tangem.core.ui.components.block.BlockCard
|
||||||
|
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||||
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.features.feed.ui.feed.state.FeedListCallbacks
|
||||||
|
import com.tangem.features.feed.ui.feed.state.NewsUM
|
||||||
|
import com.tangem.features.feed.ui.feed.state.NewsUMState
|
||||||
|
|
||||||
|
private const val FOURTH_ITEM_INDEX = 3
|
||||||
|
private const val GRADIENT_START = 0f
|
||||||
|
private const val GRADIENT_END = 0.5f
|
||||||
|
private val LinearGradientFirstPart = Color(0xFF635EEC)
|
||||||
|
private val LinearGradientSecondPart = Color(0xFFE05AED)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||||
|
AnimatedContent(news.newsUMState) { newsUMState ->
|
||||||
|
when (newsUMState) {
|
||||||
|
NewsUMState.LOADING -> NewsLoadingBlock()
|
||||||
|
NewsUMState.CONTENT -> {
|
||||||
|
if (news.content.isNotEmpty()) {
|
||||||
|
NewsContentBlock(
|
||||||
|
feedListCallbacks = feedListCallbacks,
|
||||||
|
news = news,
|
||||||
|
trendingArticle = trendingArticle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NewsUMState.ERROR -> NewsErrorBlock(onRetryClick = news.onRetryClicked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val articlesReadStatus = remember(news.content) {
|
||||||
|
news.content.map { it.isViewed }
|
||||||
|
}
|
||||||
|
LaunchedEffect(articlesReadStatus) {
|
||||||
|
listState.requestScrollToItem(0)
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Header(
|
||||||
|
title = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.common_news),
|
||||||
|
style = TangemTheme.typography.h3,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerW(4.dp)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerW(2.dp)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = buildAnnotatedString {
|
||||||
|
withStyle(
|
||||||
|
SpanStyle().copy(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
GRADIENT_START to LinearGradientFirstPart,
|
||||||
|
GRADIENT_END to LinearGradientSecondPart,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
append(stringResourceSafe(R.string.feed_tangem_ai))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = TangemTheme.typography.subtitle1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
||||||
|
)
|
||||||
|
SpacerH(12.dp)
|
||||||
|
|
||||||
|
if (trendingArticle != null) {
|
||||||
|
Column {
|
||||||
|
ArticleCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
articleConfigUM = trendingArticle,
|
||||||
|
onArticleClick = { feedListCallbacks.onArticleClick(trendingArticle.id) },
|
||||||
|
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||||
|
)
|
||||||
|
SpacerH(12.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
state = listState,
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
items = news.content,
|
||||||
|
key = { _, article -> article.id },
|
||||||
|
contentType = { _, _ -> "article" },
|
||||||
|
) { index, article ->
|
||||||
|
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||||
|
Modifier.onFirstVisible(
|
||||||
|
minFractionVisible = 0.5f,
|
||||||
|
callback = feedListCallbacks.onSliderScroll,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
ArticleCard(
|
||||||
|
articleConfigUM = article,
|
||||||
|
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||||
|
modifier = articleModifier
|
||||||
|
.heightIn(min = 164.dp)
|
||||||
|
.width(216.dp),
|
||||||
|
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(contentType = "show_more") {
|
||||||
|
ShowMoreArticlesCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(216.dp)
|
||||||
|
.heightIn(min = 164.dp)
|
||||||
|
.onFirstVisible(
|
||||||
|
minFractionVisible = 0.5f,
|
||||||
|
callback = feedListCallbacks.onSliderEndReached,
|
||||||
|
),
|
||||||
|
onClick = { feedListCallbacks.onOpenAllNews(true) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpacerH(32.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
||||||
|
Column {
|
||||||
|
Header(
|
||||||
|
title = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.common_news),
|
||||||
|
style = TangemTheme.typography.h3,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSeeAllClick = {},
|
||||||
|
)
|
||||||
|
SpacerH(12.dp)
|
||||||
|
BlockCard(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||||
|
) {
|
||||||
|
UnableToLoadData(
|
||||||
|
onRetryClick = onRetryClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SpacerH(32.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package com.tangem.features.feed.ui.feed.state
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
|
||||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
|
|
||||||
internal data class EarnListUM(
|
|
||||||
val items: ImmutableList<EarnListItemUM>,
|
|
||||||
val contentState: EarnListContentState,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
internal data class EarnListItemUM(
|
|
||||||
val network: TextReference.Str,
|
|
||||||
val symbol: String,
|
|
||||||
val tokenName: String,
|
|
||||||
val currencyIconState: CurrencyIconState,
|
|
||||||
val earnValue: EarnValueUM,
|
|
||||||
val earnType: EarnType,
|
|
||||||
val onItemClick: () -> Unit,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
internal data class EarnValueUM(
|
|
||||||
val percent: SerializedBigDecimal,
|
|
||||||
val earnValueType: EarnValueType,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal enum class EarnType {
|
|
||||||
Staking, Yield
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum class EarnValueType {
|
|
||||||
APR, APY
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
internal sealed interface EarnListContentState {
|
|
||||||
data object Loading : EarnListContentState
|
|
||||||
data object Content : EarnListContentState
|
|
||||||
data class Error(val onRetryClicked: () -> Unit) : EarnListContentState
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue