Updated on 2026-08-14
This commit is contained in:
commit
bfd903b43c
595 changed files with 12755 additions and 19837 deletions
|
|
@ -15,7 +15,7 @@ interface AccountCreateEditComponent : ComposableContentComponent {
|
|||
) : Params
|
||||
|
||||
data class Edit(
|
||||
val account: Account,
|
||||
val account: Account.CryptoPortfolio,
|
||||
) : Params
|
||||
}
|
||||
}
|
||||
|
|
@ -7,5 +7,5 @@ import com.tangem.domain.models.account.Account
|
|||
interface AccountDetailsComponent : ComposableContentComponent {
|
||||
interface Factory : ComponentFactory<Params, AccountDetailsComponent>
|
||||
|
||||
data class Params(val account: Account)
|
||||
data class Params(val account: Account.CryptoPortfolio)
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
|
|||
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
|
||||
|
|
@ -73,7 +72,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId)
|
||||
is AccountCreateEditComponent.Params.Edit -> {
|
||||
val derivationIndex = params.account.derivationIndex?.value
|
||||
val derivationIndex = params.account.derivationIndex.value
|
||||
val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
|
|
@ -170,7 +169,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon)
|
||||
val isNewName = name != params.account.accountName
|
||||
val isNewIcon = icon != params.account.portfolioIcon
|
||||
val derivationIndex = params.account.derivationIndex?.value
|
||||
val derivationIndex = params.account.derivationIndex.value
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex))
|
||||
|
||||
uiState.value = uiState.value.toggleProgress(showProgress = true)
|
||||
|
|
@ -182,7 +181,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
uiState.value = uiState.value.toggleProgress(showProgress = false)
|
||||
|
||||
result
|
||||
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) }
|
||||
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex.value) }
|
||||
.onRight {
|
||||
showMessage(R.string.account_edit_success_message)
|
||||
router.pop()
|
||||
|
|
|
|||
|
|
@ -14,12 +14,10 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.models.PortfolioId
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
|
|
@ -54,29 +52,29 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
|
||||
init {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened())
|
||||
singleAccountSupplier(SingleAccountProducer.Params(accountId))
|
||||
singleAccountSupplier.filterCryptoPortfolioAccount(accountId)
|
||||
.onEach { account -> uiState.update { buildUI(account) } }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onEditAccountClick(account: Account) {
|
||||
private fun onEditAccountClick(account: Account.CryptoPortfolio) {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit())
|
||||
router.push(AppRoute.EditAccount(account))
|
||||
}
|
||||
|
||||
private fun onManageTokensClick(account: Account) {
|
||||
private fun onManageTokensClick(account: Account.CryptoPortfolio) {
|
||||
val route = AppRoute.ManageTokens(
|
||||
source = AppRoute.ManageTokens.Source.ACCOUNT,
|
||||
portfolioId = PortfolioId(account.accountId),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value),
|
||||
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex.value),
|
||||
)
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
private fun onArchiveAccountClick() {
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val accountDerivation = params.account.derivationIndex.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
confirmArchiveDialog()
|
||||
|
|
@ -86,7 +84,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
val secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val accountDerivation = params.account.derivationIndex.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
},
|
||||
|
|
@ -107,7 +105,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun archiveCryptoPortfolio() = modelScope.launch {
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val accountDerivation = params.account.derivationIndex.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
uiState.update { it.toggleProgress(true) }
|
||||
|
|
@ -128,7 +126,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = AccountSettingsAnalyticEvents.Source.ARCHIVE,
|
||||
error = error.tag,
|
||||
accountDerivation = params.account.derivationIndex?.value,
|
||||
accountDerivation = params.account.derivationIndex.value,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
val titleRes: Int
|
||||
|
|
@ -155,16 +153,13 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
messageSender.send(dialogMessage)
|
||||
}
|
||||
|
||||
private fun buildUI(account: Account): AccountDetailsUM {
|
||||
val archiveMode = when (account) {
|
||||
is Account.CryptoPortfolio -> when (account.isMainAccount) {
|
||||
true -> ArchiveMode.None
|
||||
false -> ArchiveMode.Available(
|
||||
onArchiveAccountClick = ::onArchiveAccountClick,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||
private fun buildUI(account: Account.CryptoPortfolio): AccountDetailsUM {
|
||||
val archiveMode = when (account.isMainAccount) {
|
||||
true -> ArchiveMode.None
|
||||
false -> ArchiveMode.Available(
|
||||
onArchiveAccountClick = ::onArchiveAccountClick,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId).getOrNull()
|
||||
?.isMultiCurrency == true
|
||||
|
|
|
|||
28
features/approval/api/build.gradle.kts
Normal file
28
features/approval/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.approval.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.approval.api
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
interface GiveApprovalComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val amount: String,
|
||||
val spenderAddress: String,
|
||||
val subtitle: TextReference,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
interface Callback {
|
||||
fun onApproveDone()
|
||||
fun onApproveFailed()
|
||||
fun onCancelClick()
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params): GiveApprovalComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.approval.api
|
||||
|
||||
interface GiveApprovalFeatureToggles {
|
||||
|
||||
val isGaslessApprovalEnabled: Boolean
|
||||
}
|
||||
57
features/approval/impl/build.gradle.kts
Normal file
57
features/approval/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.approval.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Feature */
|
||||
implementation(projects.features.approval.api)
|
||||
implementation(projects.features.sendV2.api)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** SDK */
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.transaction)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.runtime)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.decompose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.features.approval.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.approval.impl.model.GiveApprovalModel
|
||||
import com.tangem.features.approval.impl.ui.GiveApprovalContent
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import com.tangem.common.ui.R as CommonUiR
|
||||
|
||||
internal class DefaultGiveApprovalComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: GiveApprovalComponent.Params,
|
||||
feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : GiveApprovalComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: GiveApprovalModel = getOrCreateModel(params = params)
|
||||
|
||||
private val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create(
|
||||
context = child("giveApprovalFeeSelector"),
|
||||
params = FeeSelectorParams.FeeSelectorBlockParams(
|
||||
state = FeeSelectorUM.Loading,
|
||||
onLoadFee = { model.loadFee() },
|
||||
onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) },
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.None,
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = CommonSendAnalyticEvents.APPROVE_CATEGORY,
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Approve,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
||||
private val currency: String = params.cryptoCurrencyStatus.currency.symbol
|
||||
|
||||
override fun dismiss() {
|
||||
params.callback.onCancelClick()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
val config = remember {
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
titleText = resourceReference(CommonUiR.string.give_permission_title),
|
||||
titleAction = TopAppBarButtonUM.Icon(
|
||||
iconRes = CommonUiR.drawable.ic_information_24,
|
||||
onClicked = model::showPermissionInfoDialog,
|
||||
),
|
||||
) {
|
||||
GiveApprovalContent(
|
||||
currency = currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = uiState.approveType,
|
||||
approveItems = uiState.approveItems,
|
||||
onChangeApproveType = model::onChangeApproveType,
|
||||
walletInteractionIcon = walletInterationIcon(params.userWallet),
|
||||
isApproveEnabled = uiState.isApproveButtonEnabled,
|
||||
isApproveLoading = uiState.isApproveLoading,
|
||||
onApproveClick = model::onApproveClick,
|
||||
onCancelClick = model::onCancelClick,
|
||||
onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : GiveApprovalComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: GiveApprovalComponent.Params,
|
||||
): DefaultGiveApprovalComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.approval.impl
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
|
||||
|
||||
internal class DefaultGiveApprovalFeatureToggles(
|
||||
private val featureToggles: FeatureTogglesManager,
|
||||
) : GiveApprovalFeatureToggles {
|
||||
|
||||
// Remove GiveTxPermissionBottomSheet and all dependencies with this toggle
|
||||
override val isGaslessApprovalEnabled: Boolean
|
||||
get() = featureToggles.isFeatureEnabled("GASLESS_APPROVAL_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.approval.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.approval.impl.DefaultGiveApprovalComponent
|
||||
import com.tangem.features.approval.impl.model.GiveApprovalModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
internal interface GiveApprovalFeatureModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface GiveApprovalModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(GiveApprovalModel::class)
|
||||
fun bindModel(model: GiveApprovalModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.features.approval.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
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.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class GiveApprovalModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase,
|
||||
private val getFeeForTokenUseCase: GetFeeForTokenUseCase,
|
||||
private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model(), FeeSelectorModelCallback {
|
||||
|
||||
private val params: GiveApprovalComponent.Params = paramsContainer.require()
|
||||
|
||||
val uiState: StateFlow<GiveApprovalUM>
|
||||
field = MutableStateFlow(
|
||||
GiveApprovalUM(
|
||||
approveType = ApproveType.LIMITED,
|
||||
isApproveButtonEnabled = false,
|
||||
isApproveLoading = false,
|
||||
),
|
||||
)
|
||||
|
||||
private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
this.feeSelectorUM = feeSelectorUM
|
||||
uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) }
|
||||
}
|
||||
|
||||
fun onApproveClick() {
|
||||
uiState.update { it.copy(isApproveLoading = true) }
|
||||
modelScope.launch(dispatchers.main) {
|
||||
val isSuccess = sendApprovalTransaction()
|
||||
uiState.update { it.copy(isApproveLoading = false) }
|
||||
if (isSuccess) {
|
||||
params.callback.onApproveDone()
|
||||
} else {
|
||||
params.callback.onApproveFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCancelClick() {
|
||||
params.callback.onCancelClick()
|
||||
}
|
||||
|
||||
fun onChangeApproveType(approveType: ApproveType) {
|
||||
uiState.update { it.copy(approveType = approveType) }
|
||||
}
|
||||
|
||||
fun onOpenLearnMoreAboutApproveClick() {
|
||||
urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP)
|
||||
}
|
||||
|
||||
fun showPermissionInfoDialog() {
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
message = resourceReference(com.tangem.common.ui.R.string.give_permission_staking_footer),
|
||||
title = resourceReference(com.tangem.common.ui.R.string.common_approve),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun prepareApprovalTransaction(): Either<Throwable, TransactionData> {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||
?: return Either.Left(IllegalStateException("Currency is not a token"))
|
||||
|
||||
return createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return getFeeUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = params.userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return if (maybeToken == null) {
|
||||
getFeeForGaslessUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = params.userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
} else {
|
||||
getFeeForTokenUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = params.userWallet,
|
||||
token = maybeToken.currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendApprovalTransaction(): Boolean {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false
|
||||
|
||||
val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false
|
||||
val selectedFee = feeContent.selectedFeeItem.fee
|
||||
val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended
|
||||
|
||||
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||
|
||||
val transactionData = createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
amount = getApprovalAmount(),
|
||||
fee = selectedFee,
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
Timber.e(error, "Failed to create approval transaction")
|
||||
return false
|
||||
}
|
||||
|
||||
return if (isFeeInTokenCurrency) {
|
||||
createAndSendGaslessTransactionUseCase(
|
||||
userWallet = params.userWallet,
|
||||
transactionData = transactionData,
|
||||
fee = feeExtended,
|
||||
)
|
||||
} else {
|
||||
sendTransactionUseCase(
|
||||
txData = transactionData,
|
||||
userWallet = params.userWallet,
|
||||
network = tokenCurrency.network,
|
||||
)
|
||||
}.fold(
|
||||
ifLeft = { error ->
|
||||
Timber.e("Failed to send approval transaction: $error")
|
||||
false
|
||||
},
|
||||
ifRight = { true },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getApprovalAmount(): BigDecimal? {
|
||||
return if (uiState.value.approveType == ApproveType.LIMITED) {
|
||||
params.amount.toBigDecimalOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.approval.impl.model
|
||||
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal data class GiveApprovalUM(
|
||||
val approveType: ApproveType,
|
||||
val approveItems: ImmutableList<ApproveType> = ApproveType.entries.toImmutableList(),
|
||||
val isApproveButtonEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
package com.tangem.features.approval.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.common.ui.R as CommonUiR
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
internal fun GiveApprovalContent(
|
||||
currency: String,
|
||||
subtitle: TextReference,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
walletInteractionIcon: Int?,
|
||||
isApproveEnabled: Boolean,
|
||||
isApproveLoading: Boolean,
|
||||
onApproveClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24),
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
|
||||
ApprovalInfo(
|
||||
currency = currency,
|
||||
approveType = approveType,
|
||||
approveItems = approveItems,
|
||||
onChangeApproveType = onChangeApproveType,
|
||||
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing20)
|
||||
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResourceSafe(id = CommonUiR.string.common_approve),
|
||||
iconResId = walletInteractionIcon,
|
||||
showProgress = isApproveLoading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = onApproveClick,
|
||||
enabled = isApproveEnabled,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResourceSafe(id = CommonUiR.string.common_cancel),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = onCancelClick,
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun ApprovalInfo(
|
||||
currency: String,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = annotatedReference {
|
||||
append(stringResourceSafe(CommonUiR.string.swap_approve_description))
|
||||
append(" ")
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "APPROVE_TAG",
|
||||
linkInteractionListener = { onOpenLearnMoreAboutApproveClick() },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = stringResourceSafe(CommonUiR.string.common_learn_more),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
AmountItem(
|
||||
currency = currency,
|
||||
approveType = approveType,
|
||||
onChangeApproveType = onChangeApproveType,
|
||||
approveItems = approveItems,
|
||||
)
|
||||
}
|
||||
SpacerH16()
|
||||
FooterContainer(
|
||||
footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer),
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
feeSelectorBlockComponent.Content(
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountItem(
|
||||
currency: String,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
) {
|
||||
var isExpandSelector by remember { mutableStateOf(false) }
|
||||
var amountSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = { isExpandSelector = true },
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onSizeChanged { amountSize = it }
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerWMax()
|
||||
Text(
|
||||
text = approveType.text.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
DropdownSelector(
|
||||
isExpanded = isExpandSelector,
|
||||
onDismiss = { isExpandSelector = false },
|
||||
onItemClick = { type ->
|
||||
isExpandSelector = false
|
||||
onChangeApproveType(type)
|
||||
},
|
||||
items = approveItems,
|
||||
selectedType = approveType,
|
||||
amountSize = amountSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun DropdownSelector(
|
||||
isExpanded: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onItemClick: (ApproveType) -> Unit,
|
||||
items: ImmutableList<ApproveType>,
|
||||
selectedType: ApproveType,
|
||||
amountSize: IntSize,
|
||||
) {
|
||||
var dropDownWidth by remember { mutableStateOf(IntSize.Zero) }
|
||||
val offsetY = amountSize.height.times(-1)
|
||||
val offsetX = amountSize.width - dropDownWidth.width
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action),
|
||||
shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)),
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss,
|
||||
properties = PopupProperties(clippingEnabled = false),
|
||||
offset = with(LocalDensity.current) {
|
||||
DpOffset(x = offsetX.toDp(), y = offsetY.toDp())
|
||||
},
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.onSizeChanged { dropDownWidth = it },
|
||||
) {
|
||||
items.forEach { item ->
|
||||
val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent
|
||||
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = {
|
||||
Row {
|
||||
Text(
|
||||
text = when (item) {
|
||||
ApproveType.LIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_current_transaction,
|
||||
)
|
||||
ApproveType.UNLIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_unlimited,
|
||||
)
|
||||
},
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerWMax()
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24),
|
||||
),
|
||||
tint = color,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.size20),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onItemClick.invoke(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun GiveApprovalContentPreview(
|
||||
@PreviewParameter(GiveApprovalContentPreviewProvider::class) params: GiveApprovalPreviewParams,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
GiveApprovalContent(
|
||||
currency = params.currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = params.approveType,
|
||||
approveItems = params.approveItems,
|
||||
onChangeApproveType = {},
|
||||
walletInteractionIcon = params.walletInteractionIcon,
|
||||
isApproveEnabled = params.isApproveEnabled,
|
||||
isApproveLoading = params.isApproveLoading,
|
||||
onApproveClick = {},
|
||||
onCancelClick = {},
|
||||
onOpenLearnMoreAboutApproveClick = {},
|
||||
feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class GiveApprovalPreviewParams(
|
||||
val currency: String,
|
||||
val subtitle: TextReference,
|
||||
val approveType: ApproveType,
|
||||
val approveItems: ImmutableList<ApproveType>,
|
||||
val walletInteractionIcon: Int?,
|
||||
val isApproveEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
)
|
||||
|
||||
private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<GiveApprovalPreviewParams> {
|
||||
override val values: Sequence<GiveApprovalPreviewParams>
|
||||
get() = sequenceOf(
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDT",
|
||||
subtitle = stringReference("Allow this app to access your USDT"),
|
||||
approveType = ApproveType.LIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
|
||||
isApproveEnabled = true,
|
||||
isApproveLoading = false,
|
||||
),
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDC",
|
||||
subtitle = stringReference("Allow this app to access your USDC"),
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
|
||||
isApproveEnabled = false,
|
||||
isApproveLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.approval.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
|
||||
internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent {
|
||||
override fun updateState(feeSelectorUM: FeeSelectorUM) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.features.feed.entry.featuretoggle
|
||||
|
||||
interface FeedFeatureToggle {
|
||||
val isFeedEnabled: Boolean
|
||||
val isEarnBlockEnabled: Boolean
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.tokens)
|
||||
|
|
@ -56,12 +57,6 @@ dependencies {
|
|||
implementation(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.earn)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
// Instead, create some kind of interface for such cases.
|
||||
/* Redux -_- */
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||
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.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
|
|
@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
|
|
@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
private val router: Router,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val getOfframpUrlUseCase: GetOfframpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
|
||||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
|
|
@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Sell(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
),
|
||||
)
|
||||
getOfframpUrlUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
).onRight { url ->
|
||||
urlOpener.openUrl(url)
|
||||
analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ internal class DefaultFeedFeatureToggle(
|
|||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : FeedFeatureToggle {
|
||||
|
||||
override val isFeedEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED")
|
||||
|
||||
override val isEarnBlockEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED")
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.converter
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
|
|||
|
|
@ -32,12 +32,10 @@ import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConv
|
|||
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter
|
||||
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter
|
||||
import com.tangem.features.feed.model.earn.state.EarnStateController
|
||||
import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.*
|
||||
import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager
|
||||
import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager
|
||||
import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
|
@ -69,8 +67,8 @@ internal class EarnModel @Inject constructor(
|
|||
private val earnNetworks = MutableStateFlow<EarnNetworks>(Either.Right(emptyList()))
|
||||
private val earnListConfigProvider = Provider {
|
||||
createEarnTokensListConfig(
|
||||
selectedTypeFilter = stateController.value.selectedTypeFilter,
|
||||
selectedNetworkFilter = stateController.value.selectedNetworkFilter,
|
||||
selectedTypeFilter = stateController.value.earnFilterUM.selectedTypeFilter,
|
||||
selectedNetworkFilter = stateController.value.earnFilterUM.selectedNetworkFilter,
|
||||
earnNetworks = earnNetworks.value,
|
||||
)
|
||||
}
|
||||
|
|
@ -104,7 +102,6 @@ internal class EarnModel @Inject constructor(
|
|||
init {
|
||||
updateInitialState()
|
||||
fetchEarnNetworks()
|
||||
fetchTopEarnTokens()
|
||||
subscribeOnStoredFilters()
|
||||
subscribeOnNetworks()
|
||||
subscribeOnBatchFlow()
|
||||
|
|
@ -117,19 +114,22 @@ internal class EarnModel @Inject constructor(
|
|||
batchFlowManager.initialLoadingError,
|
||||
batchFlowManager.paginationStatus,
|
||||
) { items, error, paginationStatus ->
|
||||
val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All ||
|
||||
state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
|
||||
error?.let(::handleBestOpportunitiesErrorAnalytics)
|
||||
val hasActiveFilters = state.value.earnFilterUM.selectedTypeFilter != EarnFilterTypeUM.All ||
|
||||
state.value.earnFilterUM.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
|
||||
EarnListStateManager.calculateState(
|
||||
items = items,
|
||||
error = error,
|
||||
paginationStatus = paginationStatus,
|
||||
hasActiveFilters = hasActiveFilters,
|
||||
onRetryClick = { batchFlowManager.reload() },
|
||||
onRetryClick = {
|
||||
batchFlowManager.reload()
|
||||
reloadEarnNetworks()
|
||||
},
|
||||
onLoadMore = { batchFlowManager.loadMore() },
|
||||
onClearFiltersClick = ::onClearFiltersClick,
|
||||
)
|
||||
}.onEach { bestOpportunitiesState ->
|
||||
) to error
|
||||
}.onEach { (bestOpportunitiesState, error) ->
|
||||
error?.let(::handleBestOpportunitiesErrorAnalytics)
|
||||
stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState))
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
|
@ -156,23 +156,27 @@ internal class EarnModel @Inject constructor(
|
|||
|
||||
private fun subscribeOnStoredFilters() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
getEarnFilterUseCase()
|
||||
.collect { filter ->
|
||||
val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
|
||||
val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
|
||||
stateController.update(
|
||||
EarnFilterSelectedStateTransformer(
|
||||
filterType = typeFilterUM,
|
||||
filterNetwork = networkFilterUM,
|
||||
),
|
||||
)
|
||||
batchFlowManager.reload()
|
||||
}
|
||||
combine(
|
||||
getEarnFilterUseCase(),
|
||||
earnNetworks,
|
||||
) { filter, networks ->
|
||||
val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
|
||||
val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
|
||||
stateController.update(
|
||||
EarnFilterSelectedStateTransformer(
|
||||
filterType = typeFilterUM,
|
||||
filterNetwork = networkFilterUM,
|
||||
earnNetworks = networks,
|
||||
),
|
||||
)
|
||||
batchFlowManager.reload()
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchTopEarnTokens() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
stateController.update(UpdateMostlyUsedStateLoadingTransformer())
|
||||
fetchTopEarnTokensUseCase()
|
||||
}
|
||||
}
|
||||
|
|
@ -183,13 +187,21 @@ internal class EarnModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun reloadEarnNetworks() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
if (earnNetworks.value.isLeft()) {
|
||||
fetchEarnNetworks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* start of clicks area */
|
||||
private fun onTypeFilterClick() {
|
||||
val currentState = state.value
|
||||
bottomSheetNavigation.activate(
|
||||
FeedBottomSheetRoute.TypeFilter(
|
||||
params = EarnTypeFilterComponent.Params(
|
||||
selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter),
|
||||
selectedFilter = EarnFilterTypeUMConverter().convert(currentState.earnFilterUM.selectedTypeFilter),
|
||||
onFilterSelected = ::onTypeFilterOptionSelected,
|
||||
onDismiss = { bottomSheetNavigation.dismiss() },
|
||||
),
|
||||
|
|
@ -210,7 +222,7 @@ internal class EarnModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createNetworkFilters(): List<EarnFilterNetwork> {
|
||||
val selectedFilter = state.value.selectedNetworkFilter
|
||||
val selectedFilter = state.value.earnFilterUM.selectedNetworkFilter
|
||||
return buildList {
|
||||
add(
|
||||
EarnFilterNetwork.AllNetworks(
|
||||
|
|
@ -277,11 +289,14 @@ internal class EarnModel @Inject constructor(
|
|||
modelScope.launch(dispatchers.default) {
|
||||
setEarnFilterUseCase(
|
||||
EarnFilter(
|
||||
earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter),
|
||||
earnFilterNetwork = EarnFilterNetworkUMConverter().convert(
|
||||
value = state.value.earnFilterUM.selectedNetworkFilter,
|
||||
),
|
||||
earnFilterType = type,
|
||||
),
|
||||
)
|
||||
bottomSheetNavigation.dismiss()
|
||||
reloadEarnNetworks()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +305,7 @@ internal class EarnModel @Inject constructor(
|
|||
setEarnFilterUseCase(
|
||||
EarnFilter(
|
||||
earnFilterNetwork = filter,
|
||||
earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter),
|
||||
earnFilterType = EarnFilterTypeUMConverter().convert(state.value.earnFilterUM.selectedTypeFilter),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -319,11 +334,13 @@ internal class EarnModel @Inject constructor(
|
|||
is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty()
|
||||
else -> null to ""
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
EarnAnalyticsEvent.BestOpportunitiesLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
if (state.value.bestOpportunities !is EarnBestOpportunitiesUM.Error) {
|
||||
analyticsEventHandler.send(
|
||||
EarnAnalyticsEvent.BestOpportunitiesLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,9 @@ internal fun createEarnTokensListConfig(
|
|||
earnNetworks.fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { networks ->
|
||||
networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId)
|
||||
networks.filter(EarnNetwork::isAdded)
|
||||
.map(EarnNetwork::networkId)
|
||||
.ifEmpty { listOf(NO_ONE_NETWORK) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -34,4 +36,9 @@ internal fun createEarnTokensListConfig(
|
|||
networks = networks,
|
||||
isForEarn = isForEarn,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This id means that backend has to return empty result
|
||||
*/
|
||||
private const val NO_ONE_NETWORK = "-1"
|
||||
|
|
@ -26,7 +26,7 @@ internal sealed class EarnAnalyticsEvent(
|
|||
event = "Best Opportunities Filter Network Applied",
|
||||
params = mapOf(
|
||||
"Network Filter Type" to filterType.value,
|
||||
"NetworkId" to networkId.orEmpty(),
|
||||
"Network Id" to networkId.orEmpty(),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ internal class EarnStateController @Inject constructor() {
|
|||
return EarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
earnFilterUM = EarnFilterUM(
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.domain.models.earn.EarnNetworks
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class EarnFilterSelectedStateTransformer(
|
||||
private val earnNetworks: EarnNetworks,
|
||||
private val filterType: EarnFilterTypeUM,
|
||||
private val filterNetwork: EarnFilterNetworkUM,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(
|
||||
selectedTypeFilter = filterType,
|
||||
selectedNetworkFilter = filterNetwork,
|
||||
earnFilterUM = prevState.earnFilterUM.copy(
|
||||
selectedTypeFilter = filterType,
|
||||
selectedNetworkFilter = filterNetwork,
|
||||
isNetworkFilterEnabled = earnNetworks.isRight(),
|
||||
isTypeFilterEnabled = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class EarnNetworkFilterSelectedStateTransformer(
|
||||
private val filter: EarnFilterNetworkUM,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(selectedNetworkFilter = filter)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class UpdateMostlyUsedStateLoadingTransformer : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(mostlyUsed = EarnListUM.Loading)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.news.list.statemanager
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.news.list.statemanager
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.features.feed.model.news.list.analytics.NewsListAnalyticsEvent
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.feed.ui.earn
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -14,21 +13,21 @@ import androidx.compose.runtime.mutableStateOf
|
|||
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.graphics.Color
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.SmallButtonShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
|
|
@ -39,6 +38,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder
|
||||
import com.tangem.features.feed.ui.earn.components.EarnListItem
|
||||
import com.tangem.features.feed.ui.earn.components.MostlyUsedCard
|
||||
import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder
|
||||
import com.tangem.features.feed.ui.earn.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -91,12 +91,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
|||
SpacerH(12.dp)
|
||||
BestOpportunitiesFilters(
|
||||
state = state.bestOpportunities,
|
||||
selectedNetworkFilterText = when (state.selectedNetworkFilter) {
|
||||
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
|
||||
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
|
||||
is EarnFilterNetworkUM.Network -> TextReference.Str(state.selectedNetworkFilter.text)
|
||||
},
|
||||
selectedTypeFilterText = state.selectedTypeFilterText,
|
||||
earnFilterUM = state.earnFilterUM,
|
||||
onNetworkFilterClick = state.onNetworkFilterClick,
|
||||
onTypeFilterClick = state.onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -151,10 +146,12 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(vertical = 32.dp, horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = st.onRetryClicked)
|
||||
|
|
@ -164,71 +161,17 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BestOpportunitiesFilters(
|
||||
state: EarnBestOpportunitiesUM,
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
earnFilterUM: EarnFilterUM,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer()
|
||||
else -> FilterButtons(
|
||||
selectedNetworkFilterText = selectedNetworkFilterText,
|
||||
selectedTypeFilterText = selectedTypeFilterText,
|
||||
isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered,
|
||||
earnFilterUM = earnFilterUM,
|
||||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -307,9 +250,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM)
|
|||
|
||||
@Composable
|
||||
private fun FilterButtons(
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
isEnabled: Boolean,
|
||||
earnFilterUM: EarnFilterUM,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -319,10 +260,14 @@ private fun FilterButtons(
|
|||
) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedNetworkFilterText,
|
||||
text = when (earnFilterUM.selectedNetworkFilter) {
|
||||
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
|
||||
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
|
||||
is EarnFilterNetworkUM.Network -> TextReference.Str(earnFilterUM.selectedNetworkFilter.text)
|
||||
},
|
||||
onClick = onNetworkFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = earnFilterUM.isNetworkFilterEnabled,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -330,10 +275,10 @@ private fun FilterButtons(
|
|||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedTypeFilterText,
|
||||
text = earnFilterUM.selectedTypeFilter.text,
|
||||
onClick = onTypeFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = earnFilterUM.isTypeFilterEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -496,7 +441,7 @@ private fun EarnContentLoadingPreview() {
|
|||
) {
|
||||
EarnContent(
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
),
|
||||
)
|
||||
|
|
@ -588,8 +533,12 @@ private fun previewEarnUM(
|
|||
): EarnUM = EarnUM(
|
||||
mostlyUsed = mostlyUsed,
|
||||
bestOpportunities = bestOpportunities,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
earnFilterUM = EarnFilterUM(
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
isTypeFilterEnabled = true,
|
||||
isNetworkFilterEnabled = true,
|
||||
),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.features.feed.ui.earn.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.ds.opportunities.OpportunitiesBG
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
|
||||
@Composable
|
||||
internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
|
||||
if (isRedesignEnabled) {
|
||||
MostlyUsedCardV2(
|
||||
modifier = modifier,
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
MostlyUsedCardV1(
|
||||
modifier = modifier,
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
OpportunitiesBG(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(onClick = onClick),
|
||||
icon = TangemIconUM.Currency(item.currencyIconState),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(22.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors2.text.status.positive,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCardV1(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun EarnListItemPreviewV1() {
|
||||
TangemThemePreview {
|
||||
MostlyUsedCardV1(
|
||||
EarnListItemUM(
|
||||
network = stringReference("Ethereum"),
|
||||
symbol = stringReference("USDT"),
|
||||
tokenName = stringReference("Tether"),
|
||||
currencyIconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
onItemClick = {},
|
||||
),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun EarnListItemPreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
MostlyUsedCardV2(
|
||||
EarnListItemUM(
|
||||
network = stringReference("Ethereum"),
|
||||
symbol = stringReference("USDT"),
|
||||
tokenName = stringReference("Tether"),
|
||||
currencyIconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
onItemClick = {},
|
||||
),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal data class EarnFilterUM(
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val isTypeFilterEnabled: Boolean = true,
|
||||
val isNetworkFilterEnabled: Boolean = true,
|
||||
)
|
||||
|
|
@ -1,20 +1,14 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class EarnUM(
|
||||
val mostlyUsed: EarnListUM,
|
||||
val bestOpportunities: EarnBestOpportunitiesUM,
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val earnFilterUM: EarnFilterUM,
|
||||
val onBackClick: () -> Unit,
|
||||
val onNetworkFilterClick: () -> Unit,
|
||||
val onTypeFilterClick: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
) {
|
||||
|
||||
val selectedTypeFilterText: TextReference
|
||||
get() = selectedTypeFilter.text
|
||||
}
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -8,12 +9,18 @@ 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.SpacerW
|
||||
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) {
|
||||
internal fun Header(
|
||||
onSeeAllClick: () -> Unit,
|
||||
isLoading: Boolean,
|
||||
shouldShowSeeAll: Boolean,
|
||||
title: @Composable () -> Unit,
|
||||
) {
|
||||
AnimatedContent(isLoading) { animatedState ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -25,13 +32,18 @@ internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title
|
|||
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,
|
||||
),
|
||||
)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
title()
|
||||
}
|
||||
SpacerW(8.dp)
|
||||
AnimatedVisibility(shouldShowSeeAll) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -33,10 +34,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif
|
|||
text = stringResourceSafe(R.string.markets_earn_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = onSeeAllClick,
|
||||
isLoading = earnListUM is EarnListUM.Loading,
|
||||
shouldShowSeeAll = earnListUM is EarnListUM.Content,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.news.DefaultLoadingArticle
|
||||
import com.tangem.common.ui.news.TrendingLoadingArticle
|
||||
import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle
|
||||
import com.tangem.features.feed.ui.feed.components.articles.TrendingLoadingArticle
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ 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.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -19,6 +14,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
|
|
@ -55,9 +51,13 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
|
|||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
shouldShowSeeAll = currentChart is MarketChartUM.Content,
|
||||
isLoading = currentChart is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
|
@ -88,9 +88,13 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall
|
|||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
shouldShowSeeAll = true,
|
||||
isLoading = marketChartConfig.marketCharts[marketChartConfig.currentSortByType] is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
|
|
|
|||
|
|
@ -2,28 +2,26 @@ 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.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.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.style.TextOverflow
|
||||
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.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
|
|
@ -31,16 +29,18 @@ 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.LocalRedesignEnabled
|
||||
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
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
internal 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)
|
||||
private const val LINEAR_GRADIENT_FIRST_PART_V2 = 0xFFA3A0FF
|
||||
private const val LINEAR_GRADIENT_SECOND_PART_V2 = 0xFFF79DFF
|
||||
|
||||
private const val LINEAR_GRADIENT_FIRST_PART_V1 = 0xFF635EEC
|
||||
private const val LINEAR_GRADIENT_SECOND_PART_V1 = 0xFFE05AED
|
||||
|
||||
@Composable
|
||||
internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
|
|
@ -64,13 +64,23 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend
|
|||
@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 }
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val gradientStart = remember(isRedesignEnabled) {
|
||||
if (isRedesignEnabled) {
|
||||
Color(LINEAR_GRADIENT_FIRST_PART_V2)
|
||||
} else {
|
||||
Color(LINEAR_GRADIENT_FIRST_PART_V1)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
|
||||
val gradientEnd = remember(isRedesignEnabled) {
|
||||
if (isRedesignEnabled) {
|
||||
Color(LINEAR_GRADIENT_SECOND_PART_V2)
|
||||
} else {
|
||||
Color(LINEAR_GRADIENT_SECOND_PART_V1)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
|
|
@ -95,8 +105,8 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
withStyle(
|
||||
SpanStyle().copy(
|
||||
brush = Brush.linearGradient(
|
||||
GRADIENT_START to LinearGradientFirstPart,
|
||||
GRADIENT_END to LinearGradientSecondPart,
|
||||
GRADIENT_START to gradientStart,
|
||||
GRADIENT_END to gradientEnd,
|
||||
),
|
||||
),
|
||||
) {
|
||||
|
|
@ -104,10 +114,14 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
||||
isLoading = news.newsUMState == NewsUMState.LOADING,
|
||||
shouldShowSeeAll = news.newsUMState == NewsUMState.CONTENT,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
|
||||
|
|
@ -125,48 +139,19 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
}
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
NewsSlider(
|
||||
NewsSliderConfig(
|
||||
callbacks = NewsSliderCallbacks(
|
||||
onOpenAllNews = { feedListCallbacks.onOpenAllNews(true) },
|
||||
onSliderScroll = feedListCallbacks.onSliderScroll,
|
||||
onSliderEndReached = feedListCallbacks.onSliderEndReached,
|
||||
onArticleClick = feedListCallbacks.onArticleClick,
|
||||
),
|
||||
content = news.content,
|
||||
shouldShowSeeAllNewsItem = true,
|
||||
),
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -181,10 +166,14 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
|||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = {},
|
||||
shouldShowSeeAll = false,
|
||||
isLoading = false,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
BlockCard(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
|
||||
@Composable
|
||||
internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
if (isRedesignEnabled) {
|
||||
NewsSliderV2(newsSliderConfig)
|
||||
} else {
|
||||
NewsSliderV1(newsSliderConfig)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
|
||||
@Composable
|
||||
internal fun NewsSliderV1(newsSliderConfig: NewsSliderConfig) {
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = newsSliderConfig.content,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
|
||||
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = newsSliderConfig.callbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
|
||||
private val dividerSpacerWidth = 20.dp
|
||||
|
||||
@Suppress("MagicNumber", "LongMethod")
|
||||
@Composable
|
||||
internal fun NewsSliderV2(newsSliderConfig: NewsSliderConfig) {
|
||||
val density = LocalDensity.current
|
||||
val dividerWidthPx = with(density) { 1.dp.roundToPx() }
|
||||
val spacerWidthPx = with(density) { dividerSpacerWidth.roundToPx() }
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = newsSliderConfig.content,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = Modifier.conditional(
|
||||
condition = index == FOURTH_ITEM_INDEX,
|
||||
modifier = {
|
||||
onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderScroll,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val shouldShowDivider = newsSliderConfig.shouldShowSeeAllNewsItem ||
|
||||
index < newsSliderConfig.content.size - 1
|
||||
|
||||
// have to use layout cause LazyRow has not fixed height and divider can not be measured
|
||||
Layout(
|
||||
modifier = Modifier,
|
||||
content = {
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.fillMaxHeight()
|
||||
.width(220.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(dividerSpacerWidth))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(1.dp)
|
||||
.background(TangemTheme.colors2.border.neutral.secondary),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(dividerSpacerWidth))
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
val cardPlaceable = measurables[0].measure(constraints)
|
||||
val height = cardPlaceable.height
|
||||
|
||||
if (shouldShowDivider) {
|
||||
val leftSpacer = measurables[1].measure(Constraints.fixed(spacerWidthPx, height))
|
||||
val divider = measurables[2].measure(Constraints.fixed(dividerWidthPx, height))
|
||||
val rightSpacer = measurables[3].measure(Constraints.fixed(spacerWidthPx, height))
|
||||
val totalWidth = cardPlaceable.width + leftSpacer.width + divider.width + rightSpacer.width
|
||||
|
||||
layout(totalWidth, height) {
|
||||
cardPlaceable.place(0, 0)
|
||||
leftSpacer.place(cardPlaceable.width, 0)
|
||||
divider.place(cardPlaceable.width + leftSpacer.width, 0)
|
||||
rightSpacer.place(cardPlaceable.width + leftSpacer.width + divider.width, 0)
|
||||
}
|
||||
} else {
|
||||
layout(cardPlaceable.width, height) {
|
||||
cardPlaceable.place(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(216.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = newsSliderConfig.callbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
|
||||
@Composable
|
||||
fun ArticleCard(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: CardColors = TangemBlockCardColors,
|
||||
) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
|
||||
if (isRedesignEnabled) {
|
||||
ArticleCardV2(
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
ArticleCardV1(
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
modifier = modifier,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Composable
|
||||
internal fun ArticleCardV1(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: CardColors = TangemBlockCardColors,
|
||||
) {
|
||||
if (articleConfigUM.isTrending) {
|
||||
TrendingArticle(
|
||||
articleConfigUM = articleConfigUM,
|
||||
modifier = modifier,
|
||||
onArticleClick = onArticleClick,
|
||||
colors = colors,
|
||||
)
|
||||
} else {
|
||||
DefaultArticle(
|
||||
articleConfigUM = articleConfigUM,
|
||||
modifier = modifier,
|
||||
onArticleClick = onArticleClick,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
colors: CardColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(colors.containerColor)
|
||||
.clickable { onArticleClick() },
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
painter = painterResource(R.drawable.bg_trending_article),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
text = stringResourceSafe(R.string.feed_trending_now),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
|
||||
Tags(
|
||||
modifier = Modifier.padding(horizontal = 46.dp),
|
||||
tags = articleConfigUM.tags.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ShowMoreArticlesCardV1(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 31.dp, horizontal = 12.dp),
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
|
||||
contentDescription = stringResourceSafe(R.string.common_show_more),
|
||||
)
|
||||
|
||||
SpacerH(16.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_all_news),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_stay_in_the_loop),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
colors: CardColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(colors.containerColor)
|
||||
.clickable { onArticleClick() }
|
||||
.padding(12.dp),
|
||||
) {
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
SpacerH(16.dp)
|
||||
|
||||
SpacerHMax()
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TagsPreview() {
|
||||
TangemThemePreview {
|
||||
Tags(
|
||||
tags = persistentListOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Best rate")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, backgroundColor = 0xFF000000)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ArticleCardsPreview() {
|
||||
val tags = listOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
).toImmutableSet()
|
||||
|
||||
val config = ArticleConfigUM(
|
||||
id = 1,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
score = 9.5f,
|
||||
createdAt = TextReference.Str("1h ago"),
|
||||
isTrending = true,
|
||||
tags = tags,
|
||||
isViewed = false,
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
ArticleCard(
|
||||
articleConfigUM = config,
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isTrending = false),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isTrending = false, isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
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.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Composable
|
||||
internal fun ArticleCardV2(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (articleConfigUM.isTrending) {
|
||||
TrendingArticle(
|
||||
modifier = modifier,
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
)
|
||||
} else {
|
||||
DefaultArticle(
|
||||
modifier = modifier,
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TrendingArticleBackground(
|
||||
modifier = modifier,
|
||||
onClick = onArticleClick,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
DayAndRatingInfo(rating = stringReference("${articleConfigUM.score}"))
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors2.text.neutral.tertiary
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
},
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
|
||||
SpacerH(18.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.createdAt.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
SpacerH(18.dp)
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ShowMoreArticlesCardV2(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(color = TangemTheme.colors2.surface.level3)
|
||||
.clickable(onClick = onClick)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
)
|
||||
.padding(vertical = 41.dp, horizontal = 16.dp),
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
|
||||
contentDescription = stringResourceSafe(R.string.common_show_more),
|
||||
)
|
||||
|
||||
SpacerH(10.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_all_news),
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_stay_in_the_loop),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.clickable { onArticleClick() }
|
||||
.padding(vertical = 16.dp, horizontal = 10.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RatingInfo(rating = stringReference("${articleConfigUM.score}"))
|
||||
}
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors2.text.neutral.tertiary
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
},
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
minLines = 3,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
text = articleConfigUM.createdAt.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun TrendingArticleBackground(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val isDarkTheme = LocalIsInDarkTheme.current
|
||||
|
||||
val bgColor = remember(isDarkTheme) {
|
||||
if (isDarkTheme) {
|
||||
Color(TRENDING_NIGHT_BG)
|
||||
} else {
|
||||
Color(TRENDING_LIGHT_BG)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.drawBehind {
|
||||
drawRect(bgColor)
|
||||
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val radiusScale = (w + h) / 2f
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7C16F1).copy(alpha = .8f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(w / 2f, 2.4f * h),
|
||||
radius = radiusScale * 1.57f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF3360FF).copy(alpha = .7f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(w / 2f, 2.95f * h),
|
||||
radius = radiusScale * 1.9f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFF9408).copy(alpha = .45f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(-0.38f * w, 2.1f * h),
|
||||
radius = radiusScale * 1.41f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFC2424).copy(alpha = .5f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(1.188f * w, 2.37f * h),
|
||||
radius = radiusScale * 1.41f,
|
||||
),
|
||||
)
|
||||
}
|
||||
.border(width = 1.dp, color = bgColor.copy(.1f))
|
||||
.clickable(onClick = onClick),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayAndRatingInfo(rating: TextReference, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RatingInfo(rating)
|
||||
|
||||
SpacerW(8.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.feed_trending_now),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RatingInfo(rating: TextReference) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16),
|
||||
tint = TangemTheme.colors2.fill.status.attention,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(2.dp)
|
||||
|
||||
Text(
|
||||
text = rating.resolveReference(),
|
||||
color = TangemTheme.colors2.text.status.attention,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
)
|
||||
}
|
||||
|
||||
private const val TRENDING_NIGHT_BG = 0xFF1F1F1F
|
||||
private const val TRENDING_LIGHT_BG = 0xFFFFFFFF
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TagsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Tags(
|
||||
tags = persistentListOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Best rate")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_YES)
|
||||
@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
private fun ArticleCardsPreview() {
|
||||
val tags = listOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
).toImmutableSet()
|
||||
|
||||
val config = ArticleConfigUM(
|
||||
id = 1,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
score = 9.5f,
|
||||
createdAt = TextReference.Str("1h ago"),
|
||||
isTrending = true,
|
||||
tags = tags,
|
||||
isViewed = false,
|
||||
)
|
||||
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config,
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isTrending = false),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isTrending = false, isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ShowMoreArticlesCardV2(onClick = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@Immutable
|
||||
data class ArticleConfigUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val score: Float,
|
||||
val createdAt: TextReference,
|
||||
val isTrending: Boolean,
|
||||
val tags: ImmutableSet<LabelUM>,
|
||||
val isViewed: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ArticleHeader(
|
||||
title: String,
|
||||
createdAt: String,
|
||||
score: Float,
|
||||
tags: ImmutableList<LabelUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ArticleInfo(
|
||||
score = score,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
if (tags.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
tags.forEach { tag ->
|
||||
Label(
|
||||
state = tag,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
|
||||
val dotColor = TangemTheme.colors.text.secondary
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = score.toString(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.drawWithCache {
|
||||
val radius = size.minDimension / 2f
|
||||
onDrawBehind {
|
||||
drawCircle(
|
||||
color = dotColor,
|
||||
radius = radius,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = createdAt,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
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.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(14.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(32.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultLoadingArticle(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(16.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
|
||||
@Composable
|
||||
fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
val isRedesignEnabled: Boolean = LocalRedesignEnabled.current
|
||||
if (isRedesignEnabled) {
|
||||
ShowMoreArticlesCardV2(modifier = modifier, onClick = onClick)
|
||||
} else {
|
||||
ShowMoreArticlesCardV1(modifier = modifier, onClick = onClick)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.layout.SubcomposeMeasureScope
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private val TAG_SPACING = 4.dp
|
||||
|
||||
/**
|
||||
* A layout that displays a list of tags, automatically calculating how many can fit
|
||||
* in a single line. If not all tags fit, it shows an overflow indicator (e.g., "+N").
|
||||
*
|
||||
* @param tags The immutable list of [LabelUM] to display.
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
*/
|
||||
@Composable
|
||||
internal fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
|
||||
if (tags.isEmpty()) return
|
||||
|
||||
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||
val spacingPx = TAG_SPACING.roundToPx()
|
||||
|
||||
val tagPlaceables = subcompose(ContentSlot.Tags) {
|
||||
tags.forEach { tag ->
|
||||
Label(state = tag)
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val layoutInfo = calculateLayoutInfo(
|
||||
maxWidth = constraints.maxWidth,
|
||||
spacingPx = spacingPx,
|
||||
tagPlaceables = tagPlaceables,
|
||||
)
|
||||
|
||||
val overflowPlaceable = layoutInfo.overflowCount.takeIf { it > 0 }?.let {
|
||||
subcompose(ContentSlot.Overflow) {
|
||||
OverflowLabel(count = it)
|
||||
}.first().measure(constraints)
|
||||
}
|
||||
|
||||
val visiblePlaceables = tagPlaceables.take(layoutInfo.visibleCount)
|
||||
val allVisibleItems = visiblePlaceables + listOfNotNull(overflowPlaceable)
|
||||
|
||||
val width = calculateRowWidth(
|
||||
placeables = allVisibleItems,
|
||||
spacingPx = spacingPx,
|
||||
).coerceIn(constraints.minWidth, constraints.maxWidth)
|
||||
|
||||
val height = allVisibleItems.maxOfOrNull { it.height } ?: 0
|
||||
|
||||
layout(width = width, height = height) {
|
||||
var xPosition = 0
|
||||
allVisibleItems.forEachIndexed { index, placeable ->
|
||||
if (index > 0) {
|
||||
xPosition += spacingPx
|
||||
}
|
||||
val yPosition = height - placeable.height
|
||||
placeable.placeRelative(xPosition, yPosition)
|
||||
xPosition += placeable.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many tags can be displayed and how many are overflowing.
|
||||
*/
|
||||
private fun SubcomposeMeasureScope.calculateLayoutInfo(
|
||||
maxWidth: Int,
|
||||
spacingPx: Int,
|
||||
tagPlaceables: List<Placeable>,
|
||||
): TagsLayoutInfo {
|
||||
val totalWidthWithoutOverflow = calculateRowWidth(tagPlaceables, spacingPx)
|
||||
|
||||
if (totalWidthWithoutOverflow <= maxWidth) {
|
||||
return TagsLayoutInfo(visibleCount = tagPlaceables.size, overflowCount = 0)
|
||||
}
|
||||
|
||||
for (count in tagPlaceables.indices.reversed()) {
|
||||
val visibleTags = tagPlaceables.take(count)
|
||||
val overflowCount = tagPlaceables.size - count
|
||||
|
||||
val tempOverflowPlaceable = subcompose("overflow_temp_$overflowCount") {
|
||||
OverflowLabel(count = overflowCount)
|
||||
}.first().measure(Constraints())
|
||||
|
||||
val widthWithOverflow = calculateRowWidth(
|
||||
placeables = visibleTags + tempOverflowPlaceable,
|
||||
spacingPx = spacingPx,
|
||||
)
|
||||
|
||||
if (widthWithOverflow <= maxWidth) {
|
||||
return TagsLayoutInfo(visibleCount = count, overflowCount = overflowCount)
|
||||
}
|
||||
}
|
||||
|
||||
return TagsLayoutInfo(visibleCount = 0, overflowCount = tagPlaceables.size)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverflowLabel(count: Int) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("${StringsSigns.PLUS}$count"),
|
||||
maxLines = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun calculateRowWidth(placeables: List<Placeable>, spacingPx: Int): Int {
|
||||
if (placeables.isEmpty()) return 0
|
||||
val tagsWidth = placeables.sumOf { it.width }
|
||||
val spacingWidth = spacingPx * (placeables.size - 1).coerceAtLeast(0)
|
||||
return tagsWidth + spacingWidth
|
||||
}
|
||||
|
||||
private data class TagsLayoutInfo(val visibleCount: Int, val overflowCount: Int)
|
||||
|
||||
private enum class ContentSlot { Tags, Overflow }
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.preview
|
|||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class NewsSliderConfig(
|
||||
val shouldShowSeeAllNewsItem: Boolean,
|
||||
val content: ImmutableList<ArticleConfigUM>,
|
||||
val callbacks: NewsSliderCallbacks,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal data class NewsSliderCallbacks(
|
||||
val onOpenAllNews: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
val onSliderEndReached: () -> Unit,
|
||||
val onArticleClick: (id: Int) -> Unit,
|
||||
)
|
||||
|
|
@ -2,30 +2,24 @@ package com.tangem.features.feed.ui.market.detailed.components
|
|||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
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.layout.onFirstVisible
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.items.DescriptionItem
|
||||
import com.tangem.core.ui.components.items.DescriptionPlaceholder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.feed.components.NewsSlider
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews
|
||||
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
|
||||
@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA]
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(
|
||||
state: MarketsTokenDetailsUM.Body,
|
||||
|
|
@ -207,13 +201,6 @@ private fun LazyListScope.loadingInfoBlocks() {
|
|||
|
||||
private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
||||
item("related-news") {
|
||||
val listState = rememberLazyListState()
|
||||
val articlesReadStatus = remember(relatedNews.articles) {
|
||||
relatedNews.articles.map { it.isViewed }
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -231,35 +218,18 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
|||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = relatedNews.articles,
|
||||
key = { index, article -> article.id },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = relatedNews.onScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { relatedNews.onArticledClicked(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
NewsSlider(
|
||||
NewsSliderConfig(
|
||||
callbacks = NewsSliderCallbacks(
|
||||
onOpenAllNews = {}, // not applicable here
|
||||
onSliderScroll = relatedNews.onScroll,
|
||||
onSliderEndReached = {}, // not applicable here
|
||||
onArticleClick = relatedNews.onArticledClicked,
|
||||
),
|
||||
content = relatedNews.articles,
|
||||
shouldShowSeeAllNewsItem = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.market.detailed.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp
|
|||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.common.ui.news.ArticleHeader
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.chip.Chip
|
||||
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.DefaultLoadingArticle
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.feed.ui.news.list.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject
|
|||
|
||||
/**
|
||||
* Mocking it for release/external builds to exclude SumSub dependency
|
||||
* This will never be called if the FT [isTangemPayEnabled] is off
|
||||
*/
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
internal class MockKycComponent @AssistedInject constructor(
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
package com.tangem.features.markets.details
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenDetailsComponent : ComposableContentComponent {
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val token: TokenMarketParams,
|
||||
val appCurrency: AppCurrency,
|
||||
val shouldShowPortfolio: Boolean,
|
||||
val analyticsParams: AnalyticsParams?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsParams(
|
||||
val blockchain: String?,
|
||||
val source: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, MarketsTokenDetailsComponent>
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.features.markets.entry
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface MarketsEntryComponent {
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext): MarketsEntryComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ dependencies {
|
|||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.staking)
|
||||
|
|
@ -49,12 +50,6 @@ dependencies {
|
|||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.yieldSupply)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
// Instead, create some kind of interface for such cases.
|
||||
/* Redux -_- */
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params
|
||||
import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
|
||||
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
|
||||
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Params,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
|
||||
|
||||
// applying l2 compatibility
|
||||
private val updatedParams = params.copy(
|
||||
token = params.token.copy(
|
||||
id = CryptoCurrency.RawID(getTokenIdIfL2Network(params.token.id.value)),
|
||||
),
|
||||
)
|
||||
private val analyticsParams = params.analyticsParams
|
||||
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
|
||||
|
||||
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) {
|
||||
portfolioComponentFactory.create(
|
||||
context = child("my_portfolio"),
|
||||
params = MarketsPortfolioComponent.Params(
|
||||
updatedParams.token,
|
||||
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
init {
|
||||
componentScope.launch {
|
||||
model.networksState.collectLatest { networksState ->
|
||||
when (networksState) {
|
||||
is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks(
|
||||
networksState.networks,
|
||||
)
|
||||
TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Analytics ===
|
||||
if (analyticsParams != null) {
|
||||
analyticsEventHandler.send(
|
||||
MarketDetailsAnalyticsEvent.EventBuilder(
|
||||
token = params.token,
|
||||
).screenOpened(
|
||||
blockchain = analyticsParams.blockchain,
|
||||
source = analyticsParams.source,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
model.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
model.isVisibleOnScreen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bsState by bottomSheetState
|
||||
|
||||
LaunchedEffect(bsState) {
|
||||
model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED
|
||||
}
|
||||
|
||||
BackHandler(enabled = bsState == BottomSheetState.EXPANDED) {
|
||||
navigateBack()
|
||||
}
|
||||
|
||||
MarketsTokenDetailsContent(
|
||||
modifier = modifier,
|
||||
backgroundColor = LocalMainBottomSheetColor.current.value,
|
||||
addTopBarStatusBarPadding = false,
|
||||
state = state,
|
||||
onBackClick = ::navigateBack,
|
||||
backButtonEnabled = bsState == BottomSheetState.EXPANDED,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
isAccountEnabled = accountsFeatureToggles.isFeatureEnabled,
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
model.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
model.isVisibleOnScreen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
MarketsTokenDetailsContent(
|
||||
modifier = modifier,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
addTopBarStatusBarPadding = true,
|
||||
state = state,
|
||||
onBackClick = ::navigateBack,
|
||||
backButtonEnabled = true,
|
||||
onHeaderSizeChange = {},
|
||||
isAccountEnabled = accountsFeatureToggles.isFeatureEnabled,
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateBack() = router.pop()
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsTokenDetailsComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Params): DefaultMarketsTokenDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
|
||||
internal class MarketDetailsAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
|
||||
|
||||
data class EventBuilder(
|
||||
val token: TokenMarketParams,
|
||||
) {
|
||||
fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent(
|
||||
event = "Token Chart Screen Opened",
|
||||
params = buildMap {
|
||||
put("Token", token.symbol)
|
||||
blockchain?.let { put("blockchain", it) }
|
||||
put("Source", source)
|
||||
},
|
||||
)
|
||||
|
||||
fun intervalChanged(intervalType: IntervalType, interval: PriceChangeInterval) = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Period",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
"Period" to interval.toAnalyticsString(),
|
||||
"Source" to intervalType.source,
|
||||
),
|
||||
)
|
||||
|
||||
fun readMoreClicked() = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Read More",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
),
|
||||
)
|
||||
|
||||
fun linkClicked(linkTitle: String) = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Links",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
"Link" to linkTitle,
|
||||
),
|
||||
)
|
||||
|
||||
fun exchangesScreenOpened() = MarketDetailsAnalyticsEvent(
|
||||
event = "Exchanges Screen Opened",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
),
|
||||
)
|
||||
|
||||
fun securityScoreOpened() = MarketDetailsAnalyticsEvent(
|
||||
event = "Security Score Info",
|
||||
params = mapOf("Token" to token.symbol),
|
||||
)
|
||||
|
||||
fun securityScoreProviderClicked(provider: String) = MarketDetailsAnalyticsEvent(
|
||||
event = "Security Score Provider Clicked",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
"Provider" to provider,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
enum class IntervalType(val source: String) {
|
||||
Chart("Chart"),
|
||||
PricePerformance("Price"),
|
||||
Insights("Insights"),
|
||||
}
|
||||
}
|
||||
|
||||
private fun PriceChangeInterval.toAnalyticsString() = when (this) {
|
||||
PriceChangeInterval.H24 -> "24h"
|
||||
PriceChangeInterval.WEEK -> "7d"
|
||||
PriceChangeInterval.MONTH -> "1m"
|
||||
PriceChangeInterval.MONTH3 -> "3m"
|
||||
PriceChangeInterval.MONTH6 -> "6m"
|
||||
PriceChangeInterval.YEAR -> "1y"
|
||||
PriceChangeInterval.ALL_TIME -> "All"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.di
|
||||
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.DefaultMarketsTokenDetailsComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMarketsTokenDetailsComponent(
|
||||
factory: DefaultMarketsTokenDetailsComponent.Factory,
|
||||
): MarketsTokenDetailsComponent.Factory
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(MarketsTokenDetailsModel::class)
|
||||
fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model
|
||||
}
|
||||
|
|
@ -1,645 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
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.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
|
||||
import com.tangem.features.markets.details.impl.model.converters.ExchangeItemStateConverter
|
||||
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
|
||||
import com.tangem.features.markets.details.impl.model.formatter.*
|
||||
import com.tangem.features.markets.details.impl.model.state.QuotesStateUpdater
|
||||
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
|
||||
import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class MarketsTokenDetailsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase,
|
||||
private val getTokenExchangesUseCase: GetTokenExchangesUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase,
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val quotesJob = JobHolder()
|
||||
private var userCountry: UserCountry? = null
|
||||
private val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
|
||||
private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token)
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = params.appCurrency,
|
||||
)
|
||||
|
||||
private val infoConverter = TokenMarketInfoConverter(
|
||||
appCurrency = Provider { currentAppCurrency.value },
|
||||
onInfoClick = { showBottomSheet(it) },
|
||||
onListedOnClick = ::onListedOnClick,
|
||||
onLinkClick = { link ->
|
||||
urlOpener.openUrl(link.url)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title))
|
||||
},
|
||||
onSecurityScoreInfoClick = { content ->
|
||||
showBottomSheet(content)
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.securityScoreOpened())
|
||||
},
|
||||
onSecurityScoreProviderLinkClick = { provider ->
|
||||
provider.urlData?.fullUrl?.let { url ->
|
||||
urlOpener.openUrl(url)
|
||||
}
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.securityScoreProviderClicked(provider.name))
|
||||
},
|
||||
// === Analytics ===
|
||||
onPricePerformanceIntervalChanged = { interval ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
},
|
||||
onInsightsIntervalChanged = { interval ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Insights,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
},
|
||||
needApplyFCARestrictions = Provider {
|
||||
userCountry.needApplyFCARestrictions()
|
||||
},
|
||||
// ==================
|
||||
)
|
||||
|
||||
private val descriptionConverter = DescriptionConverter(
|
||||
onReadModeClicked = { content ->
|
||||
showBottomSheet(content)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked())
|
||||
},
|
||||
needApplyFCARestrictions = Provider {
|
||||
userCountry.needApplyFCARestrictions()
|
||||
},
|
||||
onGeneratedAINotificationClick = {
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase(
|
||||
type = FeedbackEmailType.CurrencyDescriptionError(
|
||||
currencyId = params.token.id.value,
|
||||
currencyName = params.token.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
|
||||
updateLook { currentLook ->
|
||||
val percentChangeType = params.token.tokenQuotes.h24Percent.percentChangeType()
|
||||
|
||||
currentLook.copy(
|
||||
type = percentChangeType.toChartType(),
|
||||
xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24),
|
||||
yAxisFormatter = { value ->
|
||||
value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
).price()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val currentQuotes = MutableStateFlow(
|
||||
TokenQuotes(
|
||||
currentPrice = params.token.tokenQuotes.currentPrice,
|
||||
h24ChangePercent = params.token.tokenQuotes.h24Percent,
|
||||
weekChangePercent = params.token.tokenQuotes.weekPercent,
|
||||
monthChangePercent = params.token.tokenQuotes.monthPercent,
|
||||
m3ChangePercent = null,
|
||||
m6ChangePercent = null,
|
||||
yearChangePercent = null,
|
||||
allTimeChangePercent = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val currentTokenInfo = MutableStateFlow<TokenMarketInfo?>(null)
|
||||
private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis)
|
||||
|
||||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
|
||||
|
||||
val state = MutableStateFlow(
|
||||
MarketsTokenDetailsUM(
|
||||
tokenName = params.token.name,
|
||||
priceText = params.token.tokenQuotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
).price()
|
||||
},
|
||||
dateTimeText = resourceReference(R.string.common_today),
|
||||
priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() },
|
||||
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
|
||||
iconUrl = params.token.imageUrl,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = chartDataProducer,
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = ::onMarkerPointSelected,
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = ::onSelectedIntervalChange,
|
||||
isMarkerSet = false,
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
triggerPriceChange = consumedEvent(),
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val quotesStateUpdater = QuotesStateUpdater(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
state = state,
|
||||
currentQuotes = currentQuotes,
|
||||
lastUpdatedTimestamp = lastUpdatedTimestamp,
|
||||
currentTokenInfo = currentTokenInfo,
|
||||
onPricePerformanceIntervalChanged = { interval ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private val loadChartJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
|
||||
?: UserCountry.Other(Locale.getDefault().country)
|
||||
// reload screen if currency changed
|
||||
modelScope.launch {
|
||||
currentAppCurrency
|
||||
.filter { it != params.appCurrency }
|
||||
.collectLatest { _ ->
|
||||
initialLoad()
|
||||
}
|
||||
}
|
||||
|
||||
initialLoad()
|
||||
}
|
||||
|
||||
private fun initialLoad() {
|
||||
loadInfo()
|
||||
loadChart(state.value.selectedInterval)
|
||||
modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS)
|
||||
}
|
||||
|
||||
private fun loadQuotes() {
|
||||
modelScope.launch {
|
||||
val result = getTokenFullQuotesUseCase(
|
||||
tokenId = params.token.id,
|
||||
appCurrency = currentAppCurrency.value,
|
||||
tokenSymbol = params.token.symbol,
|
||||
)
|
||||
|
||||
result.onRight { res ->
|
||||
updateQuotes(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChart(interval: PriceChangeInterval) {
|
||||
modelScope.launch {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
chartState = currentState.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
}
|
||||
|
||||
val chart = getTokenPriceChartUseCase.invoke(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
interval = interval,
|
||||
tokenId = params.token.id,
|
||||
tokenSymbol = params.token.symbol,
|
||||
preview = false,
|
||||
)
|
||||
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
selectedInterval = interval,
|
||||
chartState = currentState.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
chart
|
||||
.onRight { updateTokenChart(it) }
|
||||
.onLeft {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
chartState = currentState.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.ERROR,
|
||||
),
|
||||
body = if (currentState.body is MarketsTokenDetailsUM.Body.Error) {
|
||||
MarketsTokenDetailsUM.Body.Nothing
|
||||
} else {
|
||||
currentState.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.saveIn(loadChartJobHolder)
|
||||
}
|
||||
|
||||
private suspend fun updateTokenChart(tokenChart: TokenChart) {
|
||||
val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval)
|
||||
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data(
|
||||
y = tokenChart.priceY.toImmutableList(),
|
||||
x = tokenChart.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted()
|
||||
|
||||
updateLook { currentLook ->
|
||||
currentLook.copy(
|
||||
xAxisFormatter = xAxisFormatter,
|
||||
type = state.value.priceChangeType.toChartType(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
chartState = currentState.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.DATA,
|
||||
),
|
||||
body = if (currentState.body is MarketsTokenDetailsUM.Body.Nothing) {
|
||||
MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked)
|
||||
} else {
|
||||
currentState.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadInfo() {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
val tokenMarketInfo = getTokenMarketInfoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
tokenId = params.token.id,
|
||||
tokenSymbol = params.token.symbol,
|
||||
)
|
||||
|
||||
tokenMarketInfo.fold(
|
||||
ifRight = { result -> updateInfo(result) },
|
||||
ifLeft = {
|
||||
state.update { currentState ->
|
||||
if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
currentState.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Error(
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
currentState.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Nothing,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateInfo(newInfo: TokenMarketInfo) {
|
||||
lastUpdatedTimestamp.value = DateTime.now().millis
|
||||
|
||||
currentTokenInfo.value = newInfo
|
||||
currentQuotes.value = newInfo.quotes
|
||||
|
||||
val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
priceText = newInfo.quotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
).price()
|
||||
},
|
||||
priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval(
|
||||
interval = currentState.selectedInterval,
|
||||
),
|
||||
priceChangeType = percent.percentChangeType(),
|
||||
body = MarketsTokenDetailsUM.Body.Content(
|
||||
description = descriptionConverter.convert(newInfo),
|
||||
infoBlocks = infoConverter.convert(newInfo),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val areAllWalletsHot = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
|
||||
|
||||
val networks = newInfo.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
blockchainId = network.networkId,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = areAllWalletsHot,
|
||||
)
|
||||
}
|
||||
|
||||
networksState.value = if (networks.isNullOrEmpty()) {
|
||||
TokenNetworksState.NoNetworksAvailable
|
||||
} else {
|
||||
TokenNetworksState.NetworksAvailable(networks)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook { currentLook ->
|
||||
currentLook.copy(type = percent.percentChangeType().toChartType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
|
||||
val populatedNewQuotes = currentQuotes.value.populateWith(newQuotes)
|
||||
|
||||
quotesStateUpdater.updateQuotes(newQuotes = populatedNewQuotes)
|
||||
|
||||
val percent = populatedNewQuotes
|
||||
.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(type = percent.percentChangeType().toChartType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
|
||||
if (state.value.selectedInterval == interval) return
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
// ==================
|
||||
|
||||
val quotes = currentQuotes.value
|
||||
val priceChangePercent = quotes.getFormattedPercentByInterval(interval)
|
||||
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
priceChangePercentText = priceChangePercent,
|
||||
selectedInterval = interval,
|
||||
priceChangeType = quotes.getPercentByInterval(interval)?.percentChangeType()
|
||||
?: PriceChangeType.NEUTRAL,
|
||||
dateTimeText = getDefaultDateTimeString(interval),
|
||||
)
|
||||
}
|
||||
|
||||
loadChart(interval)
|
||||
|
||||
if (priceChangePercent.isEmpty()) {
|
||||
loadQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun onMarkerPointSelected(markerTimestamp: BigDecimal?, price: BigDecimal?) {
|
||||
val currentState = state.value
|
||||
|
||||
val dateTimeText = markerTimestamp?.let { timestamp ->
|
||||
MarketsDateTimeFormatters.formatDateByIntervalWithMarker(
|
||||
interval = currentState.selectedInterval,
|
||||
markerTimestamp = timestamp,
|
||||
)
|
||||
} ?: getDefaultDateTimeString(currentState.selectedInterval)
|
||||
|
||||
val priceText = (price ?: currentQuotes.value.currentPrice).format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
).price()
|
||||
}
|
||||
|
||||
val percent = price?.let { selectedPrice ->
|
||||
getChangePercentBetween(
|
||||
previousPrice = selectedPrice,
|
||||
currentPrice = currentQuotes.value.currentPrice,
|
||||
)
|
||||
} ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval)
|
||||
|
||||
val percentText = percent?.format { percent() }.orEmpty()
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
isMarkerSet = markerTimestamp != null,
|
||||
dateTimeText = dateTimeText,
|
||||
priceText = priceText,
|
||||
priceChangePercentText = percentText,
|
||||
priceChangeType = percent.percentChangeType(),
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook { currentLook ->
|
||||
currentLook.copy(
|
||||
type = percent.percentChangeType().toChartType(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBottomSheet(content: TangemBottomSheetConfigContent) {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(
|
||||
isShown = true,
|
||||
onDismissRequest = ::hideBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideBottomSheet() {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLoadRetryClicked() {
|
||||
val currentState = state.value
|
||||
|
||||
if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) {
|
||||
loadChart(currentState.selectedInterval)
|
||||
}
|
||||
|
||||
if (currentState.body is MarketsTokenDetailsUM.Body.Error ||
|
||||
currentState.body is MarketsTokenDetailsUM.Body.Nothing
|
||||
) {
|
||||
loadInfo()
|
||||
modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onListedOnClick(exchangesCount: Int) {
|
||||
modelScope.launch {
|
||||
analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened())
|
||||
|
||||
showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount))
|
||||
|
||||
val maybeExchanges = getTokenExchangesUseCase(tokenId = params.token.id)
|
||||
|
||||
// Delay to show the bottom sheet
|
||||
delay(timeMillis = 400L)
|
||||
|
||||
updateExchangeBSContent(maybeExchanges = maybeExchanges, exchangesCount = exchangesCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExchangeBSContent(
|
||||
maybeExchanges: Either<Throwable, List<TokenMarketExchange>>,
|
||||
exchangesCount: Int,
|
||||
) {
|
||||
val content = maybeExchanges
|
||||
.fold(
|
||||
ifLeft = {
|
||||
ExchangesBottomSheetContent.Error(onRetryClick = { onListedOnClick(exchangesCount) })
|
||||
},
|
||||
ifRight = { exchanges ->
|
||||
ExchangesBottomSheetContent.Content(
|
||||
exchangeItems = ExchangeItemStateConverter.convertList(exchanges).toImmutableList(),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(content = content),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
|
||||
launch {
|
||||
while (true) {
|
||||
delay(timeMillis)
|
||||
// Update quotes only when content is visible on the screen
|
||||
isVisibleOnScreen.first { it }
|
||||
|
||||
loadQuotes()
|
||||
}
|
||||
}.saveIn(quotesJob)
|
||||
}
|
||||
|
||||
private fun getDefaultDateTimeString(interval: PriceChangeInterval): TextReference {
|
||||
return MarketsDateTimeFormatters.formatDateByInterval(
|
||||
interval = interval,
|
||||
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
|
||||
interval = interval,
|
||||
currentTimestamp = lastUpdatedTimestamp.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
internal class DescriptionConverter(
|
||||
private val onReadModeClicked: (InfoBottomSheetContent) -> Unit,
|
||||
private val onGeneratedAINotificationClick: () -> Unit,
|
||||
private val needApplyFCARestrictions: Provider<Boolean>,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.Description?> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? {
|
||||
if (needApplyFCARestrictions()) return null
|
||||
val shortDesc = value.shortDescription ?: return null
|
||||
return MarketsTokenDetailsUM.Description(
|
||||
shortDescription = stringReference(shortDesc),
|
||||
fullDescription = value.fullDescription?.let(::stringReference),
|
||||
onReadMoreClick = {
|
||||
onReadModeClicked(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_about_token_title,
|
||||
wrappedList(
|
||||
value.name,
|
||||
),
|
||||
),
|
||||
body = stringReference(value.fullDescription.orEmpty()),
|
||||
generatedAINotificationUM = InfoBottomSheetContent.GeneratedAINotificationUM(
|
||||
onClick = onGeneratedAINotificationClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.markets.TokenMarketExchange
|
||||
import com.tangem.domain.markets.TokenMarketExchange.TrustScore
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [TokenMarketExchange] to [TokenItemState]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object ExchangeItemStateConverter : Converter<TokenMarketExchange, TokenItemState> {
|
||||
|
||||
override fun convert(value: TokenMarketExchange): TokenItemState {
|
||||
return TokenItemState.Content(
|
||||
id = value.id,
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = value.imageUrl,
|
||||
fallbackResId = R.drawable.ic_alert_24,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value.name)),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(
|
||||
text = value.volumeInUsd.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = "USD",
|
||||
fiatCurrencySymbol = "$",
|
||||
).price()
|
||||
},
|
||||
),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference(value = if (value.isCentralized) "CEX" else "DEX"),
|
||||
),
|
||||
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
|
||||
auditLabelUM = value.trustScore.toAuditLabelUM(),
|
||||
),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TrustScore.toAuditLabelUM(): AuditLabelUM {
|
||||
return when (this) {
|
||||
TrustScore.Risky -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_risky),
|
||||
type = AuditLabelUM.Type.Prohibition,
|
||||
)
|
||||
TrustScore.Caution -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_caution),
|
||||
type = AuditLabelUM.Type.Warning,
|
||||
)
|
||||
TrustScore.Trusted -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_trusted),
|
||||
type = AuditLabelUM.Type.Permit,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.rawCompact
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.InsightsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
internal class InsightsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Insights, InsightsUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Insights): InsightsUM {
|
||||
return with(value) {
|
||||
InsightsUM(
|
||||
h24Info = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.day,
|
||||
holdersChange = holdersChange?.day,
|
||||
liquidityChange = liquidityChange?.day,
|
||||
buyPressureChange = buyPressureChange?.day,
|
||||
),
|
||||
weekInfo = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.week,
|
||||
holdersChange = holdersChange?.week,
|
||||
liquidityChange = liquidityChange?.week,
|
||||
buyPressureChange = buyPressureChange?.week,
|
||||
),
|
||||
monthInfo = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.month,
|
||||
holdersChange = holdersChange?.month,
|
||||
liquidityChange = liquidityChange?.month,
|
||||
buyPressureChange = buyPressureChange?.month,
|
||||
),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_insights),
|
||||
body = resourceReference(
|
||||
R.string.markets_insights_info_description_message,
|
||||
wrappedList(value.sourceNetworks.joinToString { it.name }),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onIntervalChanged = onIntervalChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInfoPointList(
|
||||
experiencedBuyerChange: BigDecimal?,
|
||||
holdersChange: BigDecimal?,
|
||||
liquidityChange: BigDecimal?,
|
||||
buyPressureChange: BigDecimal?,
|
||||
): ImmutableList<InfoPointUM> {
|
||||
return listOfNotNull(
|
||||
experiencedBuyerChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = experiencedBuyerChange.convertChange(),
|
||||
change = experiencedBuyerChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers_full),
|
||||
body = resourceReference(R.string.markets_token_details_experienced_buyers_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
buyPressureChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = buyPressureChange.convertChange(isFiatValue = true),
|
||||
change = buyPressureChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure_full),
|
||||
body = resourceReference(R.string.markets_token_details_buy_pressure_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
holdersChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = holdersChange.convertChange(),
|
||||
change = holdersChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_holders_full),
|
||||
body = resourceReference(R.string.markets_token_details_holders_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
liquidityChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = liquidityChange.convertChange(),
|
||||
change = liquidityChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity_full),
|
||||
body = resourceReference(R.string.markets_token_details_liquidity_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
private fun BigDecimal.changeType(): InfoPointUM.ChangeType? {
|
||||
return when {
|
||||
this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP
|
||||
this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String {
|
||||
val value = if (isFiatValue) {
|
||||
this.abs().format {
|
||||
val currency = appCurrency()
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).compact()
|
||||
}
|
||||
} else {
|
||||
this.abs().format {
|
||||
rawCompact()
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
this > BigDecimal.ZERO -> StringsSigns.PLUS + value
|
||||
this < BigDecimal.ZERO -> StringsSigns.MINUS + value
|
||||
this == BigDecimal.ZERO -> value
|
||||
else -> StringsSigns.DASH_SIGN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM.Link
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Stable
|
||||
internal class LinksConverter(
|
||||
private val onLinkClick: (LinksUM.Link) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Links, LinksUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Links): LinksUM {
|
||||
return LinksUM(
|
||||
officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
social = value.social?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Link.convert(): LinksUM.Link {
|
||||
return LinksUM.Link(
|
||||
title = title,
|
||||
iconRes = getIconById(id),
|
||||
url = link,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconById(id: String?): Int {
|
||||
return when (id) {
|
||||
"linkedin" -> R.drawable.ic_linkedin_24
|
||||
"discord" -> R.drawable.ic_discord_24
|
||||
"youtube" -> R.drawable.ic_youtube_24
|
||||
"telegram" -> R.drawable.ic_telegram_24
|
||||
"github" -> R.drawable.ic_github_24
|
||||
"twitter" -> R.drawable.ic_twitter_24
|
||||
"facebook" -> R.drawable.ic_facebook_24
|
||||
"reddit" -> R.drawable.ic_reddit_24
|
||||
"instagram" -> R.drawable.ic_instagram_24
|
||||
"whitepaper" -> R.drawable.ic_doc_24
|
||||
else -> R.drawable.ic_arrow_top_right_24
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MetricsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
internal class MetricsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val tokenSymbol: String,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Metrics, MetricsUM> {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun convert(value: TokenMarketInfo.Metrics): MetricsUM {
|
||||
return with(value) {
|
||||
MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
value = marketCap.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_market_capitalization_full,
|
||||
),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_market_capitalization_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
value = marketRating?.toString() ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating_full),
|
||||
body = resourceReference(R.string.markets_token_details_market_rating_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
value = volume24h.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume_full),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_trading_volume_24h_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
value = fullyDilutedValuation.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_fully_diluted_valuation_full,
|
||||
),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_fully_diluted_valuation_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
value = circulatingSupply.formatAmount(crypto = true),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply_full),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_circulating_supply_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_max_supply),
|
||||
value = maxSupply.formatMaxSupply(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_max_supply_full),
|
||||
body = resourceReference(R.string.markets_token_details_total_supply_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatMaxSupply(): String {
|
||||
when (this) {
|
||||
null -> return StringsSigns.DASH_SIGN
|
||||
BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN
|
||||
}
|
||||
|
||||
return this.formatAmount(crypto = true)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
|
||||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
return if (crypto) {
|
||||
format {
|
||||
crypto(
|
||||
symbol = tokenSymbol,
|
||||
decimals = 2,
|
||||
).compact()
|
||||
}
|
||||
} else {
|
||||
val currency = appCurrency()
|
||||
|
||||
format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).compact()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
@Stable
|
||||
internal class PricePerformanceConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) {
|
||||
|
||||
fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM {
|
||||
return PricePerformanceUM(
|
||||
h24 = value.day.convert(currentPrice),
|
||||
month = value.month.convert(currentPrice),
|
||||
all = value.allTime.convert(currentPrice),
|
||||
onIntervalChanged = onIntervalChanged,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value {
|
||||
if (this == null || this.low == null || this.high == null) {
|
||||
return PricePerformanceUM.Value(
|
||||
low = StringsSigns.DASH_SIGN,
|
||||
high = StringsSigns.DASH_SIGN,
|
||||
indicatorFraction = 0f,
|
||||
)
|
||||
}
|
||||
|
||||
return PricePerformanceUM.Value(
|
||||
low = low.convert(),
|
||||
high = high.convert(),
|
||||
indicatorFraction = calculateFraction(currentPrice),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.convert(): String {
|
||||
val currency = appCurrency()
|
||||
|
||||
return format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).price()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float {
|
||||
val lowValue = low
|
||||
val highValue = high
|
||||
return when {
|
||||
lowValue == null || highValue == null || highValue == BigDecimal.ZERO || currentPrice < lowValue -> 0f
|
||||
currentPrice > highValue || lowValue == highValue -> 1f
|
||||
else -> {
|
||||
(currentPrice - lowValue).divide(highValue - lowValue, RoundingMode.HALF_UP)
|
||||
.setScale(2, RoundingMode.HALF_UP)
|
||||
.toFloat().coerceAtMost(1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.model.formatter.MarketsDateTimeFormatters
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
internal class SecurityScoreConverter(
|
||||
private val onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit,
|
||||
private val onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit,
|
||||
) : Converter<TokenMarketInfo.SecurityData, SecurityScoreUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.SecurityData): SecurityScoreUM {
|
||||
val ratingsCount = value.securityScoreProviderData.size
|
||||
return SecurityScoreUM(
|
||||
score = value.totalSecurityScore,
|
||||
description = pluralReference(
|
||||
id = R.plurals.markets_token_details_based_on_ratings,
|
||||
count = ratingsCount,
|
||||
formatArgs = wrappedList(ratingsCount),
|
||||
),
|
||||
onInfoClick = {
|
||||
onSecurityScoreInfoClick(
|
||||
SecurityScoreBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_security_score),
|
||||
description = resourceReference(R.string.markets_token_details_security_score_description),
|
||||
providers = value.securityScoreProviderData.map { provider ->
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = provider.providerName,
|
||||
lastAuditDate = provider.lastAuditDate?.let { date ->
|
||||
MarketsDateTimeFormatters.formatAsDate(date.millis)
|
||||
},
|
||||
score = provider.securityScore,
|
||||
urlData = provider.urlData?.let { urlData ->
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = urlData.fullUrl,
|
||||
rootHost = urlData.rootHost,
|
||||
)
|
||||
},
|
||||
iconUrl = provider.iconUrl,
|
||||
)
|
||||
},
|
||||
onProviderLinkClick = onSecurityScoreProviderLinkClick,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.ListedOnUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
@Suppress("LongParameterList")
|
||||
internal class TokenMarketInfoConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val needApplyFCARestrictions: Provider<Boolean>,
|
||||
private val onInfoClick: (TangemBottomSheetConfigContent) -> Unit,
|
||||
private val onListedOnClick: (Int) -> Unit,
|
||||
onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit,
|
||||
onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
onInsightsIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
|
||||
|
||||
private val insightsConverter = InsightsConverter(
|
||||
appCurrency = appCurrency,
|
||||
onInfoClick = onInfoClick,
|
||||
onIntervalChanged = onInsightsIntervalChanged,
|
||||
)
|
||||
|
||||
private val securityScoreConverter = SecurityScoreConverter(
|
||||
onSecurityScoreInfoClick = onSecurityScoreInfoClick,
|
||||
onSecurityScoreProviderLinkClick = onSecurityScoreProviderLinkClick,
|
||||
)
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(
|
||||
appCurrency = appCurrency,
|
||||
onIntervalChanged = onPricePerformanceIntervalChanged,
|
||||
)
|
||||
private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {
|
||||
val metricsConverter = MetricsConverter(
|
||||
tokenSymbol = value.symbol,
|
||||
appCurrency = appCurrency,
|
||||
onInfoClick = onInfoClick,
|
||||
)
|
||||
|
||||
val exchangesAmount = value.exchangesAmount
|
||||
val insights = if (needApplyFCARestrictions()) {
|
||||
null
|
||||
} else {
|
||||
value.insights?.let { insightsConverter.convert(it) }
|
||||
}
|
||||
val securityScore = if (needApplyFCARestrictions()) {
|
||||
null
|
||||
} else {
|
||||
value.securityData?.let { securityScoreConverter.convert(it) }
|
||||
}
|
||||
return MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = insights,
|
||||
securityScore = securityScore,
|
||||
metrics = value.metrics?.let { metricsConverter.convert(it) },
|
||||
pricePerformance = value.pricePerformance?.let { performance ->
|
||||
pricePerformanceConverter.convert(
|
||||
value = performance,
|
||||
currentPrice = value.quotes.currentPrice,
|
||||
)
|
||||
},
|
||||
listedOn = if (exchangesAmount != null && exchangesAmount > 0) {
|
||||
ListedOnUM.Content(onClick = { onListedOnClick(exchangesAmount) }, amount = exchangesAmount)
|
||||
} else {
|
||||
ListedOnUM.Empty
|
||||
},
|
||||
links = value.links?.let { linksConverter.convert(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.formatter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.getFiatPriceAmountWithScale
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String {
|
||||
val percent = when (interval) {
|
||||
PriceChangeInterval.H24 -> h24ChangePercent
|
||||
PriceChangeInterval.WEEK -> weekChangePercent
|
||||
PriceChangeInterval.MONTH -> monthChangePercent
|
||||
PriceChangeInterval.MONTH3 -> m3ChangePercent
|
||||
PriceChangeInterval.MONTH6 -> m6ChangePercent
|
||||
PriceChangeInterval.YEAR -> yearChangePercent
|
||||
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
|
||||
}
|
||||
|
||||
return percent?.format { percent() }.orEmpty()
|
||||
}
|
||||
|
||||
internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> h24ChangePercent
|
||||
PriceChangeInterval.WEEK -> weekChangePercent
|
||||
PriceChangeInterval.MONTH -> monthChangePercent
|
||||
PriceChangeInterval.MONTH3 -> m3ChangePercent
|
||||
PriceChangeInterval.MONTH6 -> m6ChangePercent
|
||||
PriceChangeInterval.YEAR -> yearChangePercent
|
||||
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal fun BigDecimal?.percentChangeType(): PriceChangeType {
|
||||
val scaled = this?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: BigDecimal): BigDecimal {
|
||||
return if (previousPrice == BigDecimal.ZERO) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
currentPrice.subtract(previousPrice).divide(previousPrice, 4, RoundingMode.HALF_UP)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType {
|
||||
val current = getFiatPriceAmountWithScale(value = currentPrice).first
|
||||
val updated = getFiatPriceAmountWithScale(value = updatedPrice).first
|
||||
|
||||
return when {
|
||||
updated > current -> PriceChangeType.UP
|
||||
updated < current -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
|
||||
return when (this) {
|
||||
PriceChangeType.UP -> MarketChartLook.Type.Growing
|
||||
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
|
||||
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.formatter
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.formatAsDateTime
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.H24_MILLIS
|
||||
import com.tangem.utils.WEEK_MILLIS
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal object MarketsDateTimeFormatters {
|
||||
|
||||
private val dateTimeMMMFormatter by lazy {
|
||||
DateTimeFormatters.getBestFormatterBySkeleton("dd MMM Hm")
|
||||
}
|
||||
|
||||
private val dateFormatter = DateTimeFormatters.dateDDMMYYYY
|
||||
|
||||
fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> { value: BigDecimal ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter)
|
||||
}
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
PriceChangeInterval.MONTH6,
|
||||
-> { value ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd)
|
||||
}
|
||||
PriceChangeInterval.YEAR -> { value ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd)
|
||||
}
|
||||
PriceChangeInterval.ALL_TIME -> { value ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.dateYYYY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> resourceReference(R.string.common_today)
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
-> {
|
||||
resourceReference(
|
||||
R.string.common_range_with_space,
|
||||
wrappedList(
|
||||
stringReference(
|
||||
startTimestamp.formatAsDateTime(dateTimeMMMFormatter),
|
||||
),
|
||||
resourceReference(R.string.common_now),
|
||||
),
|
||||
)
|
||||
}
|
||||
PriceChangeInterval.MONTH6,
|
||||
PriceChangeInterval.YEAR,
|
||||
-> {
|
||||
resourceReference(
|
||||
R.string.common_range_with_space,
|
||||
wrappedList(
|
||||
stringReference(
|
||||
startTimestamp.formatAsDateTime(dateFormatter),
|
||||
),
|
||||
resourceReference(R.string.common_now),
|
||||
),
|
||||
)
|
||||
}
|
||||
PriceChangeInterval.ALL_TIME -> resourceReference(R.string.common_all)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
-> {
|
||||
resourceReference(
|
||||
R.string.common_range_with_space,
|
||||
wrappedList(
|
||||
stringReference(
|
||||
markerTimestamp.toLong().formatAsDateTime(dateTimeMMMFormatter),
|
||||
),
|
||||
resourceReference(R.string.common_now),
|
||||
),
|
||||
)
|
||||
}
|
||||
PriceChangeInterval.MONTH6,
|
||||
PriceChangeInterval.YEAR,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
-> {
|
||||
resourceReference(
|
||||
R.string.common_range_with_space,
|
||||
wrappedList(
|
||||
stringReference(
|
||||
markerTimestamp.toLong().formatAsDateTime(dateFormatter),
|
||||
),
|
||||
resourceReference(R.string.common_now),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getStartTimestampByInterval(interval: PriceChangeInterval, currentTimestamp: Long): Long {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> currentTimestamp - H24_MILLIS
|
||||
PriceChangeInterval.WEEK -> currentTimestamp - WEEK_MILLIS
|
||||
PriceChangeInterval.MONTH -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(1).millis
|
||||
PriceChangeInterval.MONTH3 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(3).millis
|
||||
PriceChangeInterval.MONTH6 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(6).millis
|
||||
PriceChangeInterval.YEAR -> DateTime(currentTimestamp, DateTimeZone.UTC).minusYears(1).millis
|
||||
PriceChangeInterval.ALL_TIME -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference {
|
||||
return formatDateByInterval(
|
||||
interval = interval,
|
||||
startTimestamp = getStartTimestampByInterval(
|
||||
interval = interval,
|
||||
currentTimestamp = currentTimestamp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun formatAsDate(timestamp: Long): String {
|
||||
return timestamp.formatAsDateTime(dateFormatter)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.state
|
||||
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter
|
||||
import com.tangem.features.markets.details.impl.model.formatter.*
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class QuotesStateUpdater(
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val state: MutableStateFlow<MarketsTokenDetailsUM>,
|
||||
private val currentQuotes: MutableStateFlow<TokenQuotes>,
|
||||
private val lastUpdatedTimestamp: MutableStateFlow<Long>,
|
||||
private val currentTokenInfo: MutableStateFlow<TokenMarketInfo?>,
|
||||
private val onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) {
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(
|
||||
currentAppCurrency,
|
||||
onIntervalChanged = onPricePerformanceIntervalChanged,
|
||||
)
|
||||
|
||||
suspend fun updateQuotes(newQuotes: TokenQuotes) {
|
||||
val triggerPriceChangeType = getFormattedPriceChange(
|
||||
currentPrice = currentQuotes.value.currentPrice,
|
||||
updatedPrice = newQuotes.currentPrice,
|
||||
)
|
||||
val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) {
|
||||
triggeredEvent(
|
||||
data = triggerPriceChangeType,
|
||||
onConsume = {
|
||||
state.update { it.copy(triggerPriceChange = consumedEvent()) }
|
||||
},
|
||||
)
|
||||
} else {
|
||||
consumedEvent()
|
||||
}
|
||||
|
||||
val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
val priceChangeType = percent.percentChangeType()
|
||||
|
||||
// wait until marker is removed
|
||||
state.first { it.isMarkerSet.not() }
|
||||
|
||||
currentQuotes.value = newQuotes
|
||||
lastUpdatedTimestamp.value = DateTime.now().millis
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
priceText = newQuotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency().symbol,
|
||||
fiatCurrencyCode = currentAppCurrency().code,
|
||||
).price()
|
||||
},
|
||||
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
|
||||
interval = stateToUpdate.selectedInterval,
|
||||
),
|
||||
priceChangeType = priceChangeType,
|
||||
triggerPriceChange = trigger,
|
||||
dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString(
|
||||
stateToUpdate.selectedInterval,
|
||||
currentTimestamp = lastUpdatedTimestamp.value,
|
||||
),
|
||||
body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body {
|
||||
val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this
|
||||
|
||||
return if (this is MarketsTokenDetailsUM.Body.Content) {
|
||||
copy(
|
||||
infoBlocks = infoBlocks.copy(
|
||||
pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.model.state
|
||||
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
|
||||
internal sealed class TokenNetworksState {
|
||||
|
||||
data object Loading : TokenNetworksState()
|
||||
|
||||
data object NoNetworksAvailable : TokenNetworksState()
|
||||
|
||||
data class NetworksAvailable(val networks: List<TokenMarketInfo.Network>) : TokenNetworksState()
|
||||
}
|
||||
|
|
@ -1,351 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.Animatable
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.components.*
|
||||
import com.tangem.features.markets.details.impl.ui.preview.MarketsTokenDetailsPreview
|
||||
import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun MarketsTokenDetailsContent(
|
||||
state: MarketsTokenDetailsUM,
|
||||
backgroundColor: Color,
|
||||
addTopBarStatusBarPadding: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
backButtonEnabled: Boolean,
|
||||
isAccountEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
Content(
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
backButtonEnabled = backButtonEnabled,
|
||||
portfolioBlock = portfolioBlock,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
addTopBarStatusBarInsets = addTopBarStatusBarPadding,
|
||||
)
|
||||
|
||||
when (state.bottomSheetConfig.content) {
|
||||
is InfoBottomSheetContent -> InfoBottomSheet(config = state.bottomSheetConfig)
|
||||
is SecurityScoreBottomSheetContent -> SecurityScoreBottomSheet(config = state.bottomSheetConfig)
|
||||
is ExchangesBottomSheetContent -> ExchangesBottomSheet(config = state.bottomSheetConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun Content(
|
||||
state: MarketsTokenDetailsUM,
|
||||
backgroundColor: Color,
|
||||
addTopBarStatusBarInsets: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
backButtonEnabled: Boolean,
|
||||
isAccountEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.let { if (addTopBarStatusBarInsets) it.statusBarsPadding() else it }
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
TopBar(
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
lazyListState = lazyListState,
|
||||
tokenName = state.tokenName,
|
||||
tokenPrice = state.priceText,
|
||||
isBackButtonEnabled = backButtonEnabled,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
|
||||
SpacerH4()
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
item("intervalSelector") {
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH32() }
|
||||
item("chart") {
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = backgroundColor,
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
portfolioBlock = portfolioBlock,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
lazyListState: LazyListState,
|
||||
tokenName: String,
|
||||
tokenPrice: String,
|
||||
isBackButtonEnabled: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val shouldShowPriceSubtitle by remember {
|
||||
derivedStateOf {
|
||||
lazyListState.firstVisibleItemIndex > 1
|
||||
}
|
||||
}
|
||||
|
||||
TangemTopAppBar(
|
||||
modifier = modifier,
|
||||
title = tokenName,
|
||||
subtitle = if (shouldShowPriceSubtitle) tokenPrice else null,
|
||||
startButton = TopAppBarButtonUM.Back(
|
||||
onBackClicked = onBackClick,
|
||||
enabled = isBackButtonEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
TokenPriceText(
|
||||
price = state.priceText,
|
||||
triggerPriceChange = state.triggerPriceChange,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
Text(
|
||||
text = state.dateTimeText.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
if (state.priceChangePercentText != null) {
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = state.priceChangePercentText,
|
||||
type = state.priceChangeType,
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SpacerW4()
|
||||
CoinIcon(
|
||||
modifier = Modifier.requiredSize(TangemTheme.dimens.size48),
|
||||
url = state.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceText(
|
||||
price: String,
|
||||
triggerPriceChange: StateEvent<PriceChangeType>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val growColor = TangemTheme.colors.text.accent
|
||||
val fallColor = TangemTheme.colors.text.warning
|
||||
val generalColor = TangemTheme.colors.text.primary1
|
||||
|
||||
val color = remember(generalColor) { Animatable(generalColor) }
|
||||
|
||||
EventEffect(triggerPriceChange) { changeType ->
|
||||
val nextColor = when (changeType) {
|
||||
PriceChangeType.UP,
|
||||
-> growColor
|
||||
PriceChangeType.DOWN -> fallColor
|
||||
PriceChangeType.NEUTRAL -> return@EventEffect
|
||||
}
|
||||
|
||||
color.animateTo(nextColor, snap())
|
||||
color.animateTo(generalColor, tween(durationMillis = 500))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = price,
|
||||
modifier = modifier,
|
||||
color = color.value,
|
||||
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.head.fontSize),
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.head,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IntervalSelector(
|
||||
trendInterval: PriceChangeInterval,
|
||||
isEnabled: Boolean,
|
||||
onIntervalClick: (PriceChangeInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
PriceChangeInterval.MONTH6,
|
||||
PriceChangeInterval.YEAR,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
),
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
initialSelectedItem = trendInterval,
|
||||
onClick = onIntervalClick,
|
||||
isEnabled = isEnabled,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = if (isEnabled) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.disabled
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PriceChangeInterval.getText(): TextReference {
|
||||
return when (this) {
|
||||
PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title)
|
||||
PriceChangeInterval.WEEK -> resourceReference(R.string.markets_selector_interval_7d_title)
|
||||
PriceChangeInterval.MONTH -> resourceReference(R.string.markets_selector_interval_1m_title)
|
||||
PriceChangeInterval.MONTH3 -> resourceReference(R.string.markets_selector_interval_3m_title)
|
||||
PriceChangeInterval.MONTH6 -> resourceReference(R.string.markets_selector_interval_6m_title)
|
||||
PriceChangeInterval.YEAR -> resourceReference(R.string.markets_selector_interval_1y_title)
|
||||
PriceChangeInterval.ALL_TIME -> resourceReference(R.string.markets_selector_interval_all_title)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun MarketsTokenDetailsContent_Preview(
|
||||
@PreviewParameter(MarketsTokenDetailsContentPreviewProvider::class) params: MarketsTokenDetailsUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
MarketsTokenDetailsContent(
|
||||
state = params,
|
||||
onHeaderSizeChange = {},
|
||||
onBackClick = {},
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
portfolioBlock = {},
|
||||
backButtonEnabled = true,
|
||||
isAccountEnabled = true,
|
||||
addTopBarStatusBarPadding = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class MarketsTokenDetailsContentPreviewProvider : PreviewParameterProvider<MarketsTokenDetailsUM> {
|
||||
override val values: Sequence<MarketsTokenDetailsUM>
|
||||
get() = sequenceOf(
|
||||
MarketsTokenDetailsPreview.loadingState,
|
||||
MarketsTokenDetailsPreview.contentState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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 com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Exchanges bottom sheet
|
||||
*
|
||||
* @param config bottom sheet config
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<ExchangesBottomSheetContent>(
|
||||
config = config,
|
||||
addBottomInsets = false,
|
||||
title = { Title(textResId = it.titleResId, onBackClick = config.onDismissRequest) },
|
||||
content = { content ->
|
||||
Box {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
) {
|
||||
item(key = "subtitle") {
|
||||
Subtitle(
|
||||
subtitleRes = content.subtitleResId,
|
||||
volumeReference = content.volumeReference,
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = content.exchangeItems,
|
||||
key = TokenItemState::id,
|
||||
itemContent = { TokenItem(state = it, isBalanceHidden = false) },
|
||||
)
|
||||
}
|
||||
|
||||
if (content is ExchangesBottomSheetContent.Error) {
|
||||
Error(
|
||||
content = content,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(@StringRes textResId: Int, onBackClick: () -> Unit) {
|
||||
TangemTopAppBar(
|
||||
title = stringResourceSafe(id = textResId),
|
||||
startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Subtitle(@StringRes subtitleRes: Int, volumeReference: TextReference, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
SubtitleText(textReference = resourceReference(id = subtitleRes))
|
||||
|
||||
SubtitleText(textReference = volumeReference)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubtitleText(textReference: TextReference) {
|
||||
Text(
|
||||
text = textReference.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = content.message),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(id = R.string.alert_button_try_again),
|
||||
onClick = content.onRetryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ExchangesBottomSheet(
|
||||
@PreviewParameter(ExchangesBottomSheetContentProvider::class) content: ExchangesBottomSheetContent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ExchangesBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
onDismissRequest = {},
|
||||
content = content,
|
||||
isShown = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterProvider<ExchangesBottomSheetContent>(
|
||||
listOf(
|
||||
ExchangesBottomSheetContent.Loading(exchangesCount = 13),
|
||||
ExchangesBottomSheetContent.Error(onRetryClick = {}),
|
||||
ExchangesBottomSheetContent.Content(
|
||||
exchangeItems = List(size = 13) { index ->
|
||||
TokenItemState.Content(
|
||||
id = index.toString(),
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.ic_facebook_24,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "OKX")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference(value = "CEX")),
|
||||
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
|
||||
auditLabelUM = AuditLabelUM(
|
||||
text = stringReference("Caution"),
|
||||
type = AuditLabelUM.Type.Warning,
|
||||
),
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.impl.R
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
|
||||
@Composable
|
||||
internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<InfoBottomSheetContent>(
|
||||
config = config,
|
||||
addBottomInsets = false,
|
||||
title = { TangemBottomSheetTitle(title = it.title) },
|
||||
content = { content ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
MarkdownText(
|
||||
markdown = content.body.resolveReference(),
|
||||
disableLinkMovementMethod = true,
|
||||
linkifyMask = 0,
|
||||
syntaxHighlightColor = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2.copy(
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
),
|
||||
)
|
||||
|
||||
if (content.generatedAINotificationUM != null) {
|
||||
AdditionalInfoNotification(
|
||||
onClick = content.generatedAINotificationUM.onClick,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Notification(
|
||||
config = NotificationConfig(
|
||||
subtitle = TextReference.Res(id = R.string.information_generated_with_ai),
|
||||
iconResId = R.drawable.ic_magic_28,
|
||||
onClick = onClick,
|
||||
shouldShowArrowIcon = false,
|
||||
),
|
||||
modifier = modifier,
|
||||
subtitleColor = TangemTheme.colors.text.primary1,
|
||||
containerColor = TangemTheme.colors.button.disabled,
|
||||
iconTint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.text.TooltipText
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
|
||||
@Composable
|
||||
internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
if (infoPointUM.onInfoClick != null) {
|
||||
TooltipText(
|
||||
text = infoPointUM.title,
|
||||
onInfoClick = infoPointUM.onInfoClick,
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = infoPointUM.title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Text(
|
||||
text = infoPointUM.value,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (infoPointUM.change != null) {
|
||||
SpacerW4()
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size8)
|
||||
.align(Alignment.CenterVertically),
|
||||
imageVector = ImageVector.vectorResource(
|
||||
id = when (infoPointUM.change) {
|
||||
InfoPointUM.ChangeType.UP -> R.drawable.ic_arrow_up_8
|
||||
InfoPointUM.ChangeType.DOWN -> R.drawable.ic_arrow_down_8
|
||||
},
|
||||
),
|
||||
tint = when (infoPointUM.change) {
|
||||
InfoPointUM.ChangeType.UP -> TangemTheme.colors.icon.accent
|
||||
InfoPointUM.ChangeType.DOWN -> TangemTheme.colors.icon.warning
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) {
|
||||
Column(
|
||||
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
if (withTooltip) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredHeight(TangemTheme.dimens.size16)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
textSizeHeight = false,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
style = TangemTheme.typography.body1,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(150.dp)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000,000",
|
||||
),
|
||||
)
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000,000",
|
||||
onInfoClick = { },
|
||||
),
|
||||
)
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000",
|
||||
change = InfoPointUM.ChangeType.UP,
|
||||
onInfoClick = { },
|
||||
),
|
||||
)
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000",
|
||||
change = InfoPointUM.ChangeType.DOWN,
|
||||
onInfoClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewShimmer() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(150.dp)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
InfoPointShimmer(modifier = Modifier.fillMaxWidth())
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.block.information.GridItems
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.text.TooltipText
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.InsightsUM
|
||||
import com.tangem.features.markets.details.impl.ui.getText
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
|
||||
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
TooltipText(
|
||||
text = resourceReference(R.string.markets_token_details_insights),
|
||||
textStyle = TangemTheme.typography.subtitle2,
|
||||
onInfoClick = state.onInfoClick,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
),
|
||||
initialSelectedItem = PriceChangeInterval.H24,
|
||||
onClick = { interval ->
|
||||
currentInterval = interval
|
||||
state.onIntervalChanged(interval)
|
||||
},
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
) { interval ->
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = 14.dp,
|
||||
vertical = 4.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = interval.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val infoPoints = when (currentInterval) {
|
||||
PriceChangeInterval.H24 -> state.h24Info
|
||||
PriceChangeInterval.WEEK -> state.weekInfo
|
||||
PriceChangeInterval.MONTH -> state.monthInfo
|
||||
else -> state.h24Info
|
||||
}
|
||||
|
||||
GridItems(
|
||||
items = infoPoints,
|
||||
itemContent = { infoPoint ->
|
||||
InfoPoint(
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
infoPointUM = infoPoint,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
|
||||
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
|
||||
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(headerHeight)
|
||||
.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
GridItems(
|
||||
items = List(size = 4) { it }.toImmutableList(),
|
||||
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
itemContent = {
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
InsightsBlock(
|
||||
state = InsightsUM(
|
||||
h24Info = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
),
|
||||
weekInfo = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000 000",
|
||||
),
|
||||
),
|
||||
monthInfo = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000",
|
||||
),
|
||||
),
|
||||
onInfoClick = {},
|
||||
onIntervalChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
actualContent = { ContentPreview() },
|
||||
shimmerContent = { InsightsBlockPlaceholder() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.ChipShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.chip.Chip
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
contentHorizontalPadding = 0.dp,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.markets_token_details_links),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column {
|
||||
SubBlock(
|
||||
title = stringResourceSafe(id = R.string.markets_token_details_official_links),
|
||||
links = state.officialLinks,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResourceSafe(id = R.string.markets_token_details_social),
|
||||
links = state.social,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResourceSafe(id = R.string.markets_token_details_repository),
|
||||
links = state.repository,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResourceSafe(id = R.string.markets_token_details_blockchain_site),
|
||||
links = state.blockchainSite,
|
||||
onLinkClick = state.onLinkClick,
|
||||
lastBlock = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun SubBlock(
|
||||
links: ImmutableList<LinksUM.Link>,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
lastBlock: Boolean = false,
|
||||
title: String = "Official links",
|
||||
) {
|
||||
if (links.isEmpty()) return
|
||||
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = !lastBlock,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
links.fastForEach { link ->
|
||||
Chip(
|
||||
text = stringReference(link.title),
|
||||
iconResId = link.iconRes,
|
||||
onClick = { onLinkClick(link) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LinksBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
contentHorizontalPadding = 0.dp,
|
||||
title = {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column {
|
||||
SubBlockPlaceholder()
|
||||
SubBlockPlaceholder()
|
||||
SubBlockPlaceholder(lastBlock = true)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = !lastBlock,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(78.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
repeat(times = 3) {
|
||||
ChipShimmer(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
LinksBlock(
|
||||
state = LinksUM(
|
||||
officialLinks = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = "Website",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = "Website",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = "Website",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
social = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = "Twitter",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = "Facebook",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
repository = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = "Github",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
blockchainSite = persistentListOf(),
|
||||
onLinkClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PlaceholderPreview() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = { LinksBlockPlaceholder() },
|
||||
actualContent = { ContentPreview() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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 com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.details.impl.ui.state.ListedOnUM
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* "Listed on" block
|
||||
*
|
||||
* @param state block state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun ListedOnBlock(state: ListedOnUM, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
InformationBlock(
|
||||
title = {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(enabled = state is ListedOnUM.Content) {
|
||||
(state as? ListedOnUM.Content)?.onClick?.invoke()
|
||||
},
|
||||
) {
|
||||
Description(
|
||||
state = state,
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
|
||||
if (state is ListedOnUM.Content) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ListedOnBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
title = {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction = 0.3f)
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Description(state: ListedOnUM, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
modifier = modifier,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 328, heightDp = 68)
|
||||
@Preview(name = "Dark Theme", widthDp = 328, heightDp = 68, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ListedOnBlock(@PreviewParameter(ListenOnUMProvider::class) state: ListedOnUM?) {
|
||||
TangemThemePreview {
|
||||
if (state == null) {
|
||||
ListedOnBlockPlaceholder()
|
||||
} else {
|
||||
ListedOnBlock(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ListedOnBlock_StateChanging() {
|
||||
var state by remember { mutableStateOf<ListedOnUM?>(value = null) }
|
||||
|
||||
Preview_ListedOnBlock(state = state)
|
||||
|
||||
LaunchedEffect(key1 = null) {
|
||||
delay(timeMillis = 3000)
|
||||
|
||||
state = ListedOnUM.Empty
|
||||
}
|
||||
}
|
||||
|
||||
private class ListenOnUMProvider : CollectionPreviewParameterProvider<ListedOnUM?>(
|
||||
collection = listOf(
|
||||
ListedOnUM.Empty,
|
||||
ListedOnUM.Content(onClick = {}, amount = 5),
|
||||
null,
|
||||
),
|
||||
)
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
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.graphics.Color
|
||||
import com.tangem.common.ui.charts.MarketChart
|
||||
import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.rememberMarketChartState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
|
||||
@Composable
|
||||
internal fun MarketTokenDetailsChart(
|
||||
state: MarketsTokenDetailsUM.ChartState,
|
||||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val growingColor = TangemTheme.colors.icon.accent
|
||||
val fallingColor = TangemTheme.colors.icon.warning
|
||||
val neutralColor = TangemTheme.colors.icon.informative
|
||||
|
||||
val chartState = rememberMarketChartState(
|
||||
dataProducer = state.dataProducer,
|
||||
colorMapper = { chartType ->
|
||||
when (chartType) {
|
||||
MarketChartLook.Type.Growing -> growingColor
|
||||
MarketChartLook.Type.Falling -> fallingColor
|
||||
MarketChartLook.Type.Neutral -> neutralColor
|
||||
}
|
||||
},
|
||||
onMarkerShown = state.onMarkerPointSelected,
|
||||
)
|
||||
|
||||
val bottomChartAxisHeight = getMarketChartBottomAxisHeight()
|
||||
|
||||
Box(modifier) {
|
||||
MarketChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = chartState,
|
||||
)
|
||||
|
||||
if (state.status != MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
Box(
|
||||
Modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.matchParentSize()
|
||||
.padding(bottom = bottomChartAxisHeight),
|
||||
) {
|
||||
when (state.status) {
|
||||
MarketsTokenDetailsUM.ChartState.Status.LOADING -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.align(Alignment.Center),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
MarketsTokenDetailsUM.ChartState.Status.ERROR -> {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.align(Alignment.Center),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.TextButton
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.GridItems
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MetricsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
const val MAX_METRICS_COUNT = 6
|
||||
|
||||
@Composable
|
||||
internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.markets_token_details_metrics),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
if (state.metrics.size > MAX_METRICS_COUNT) {
|
||||
ShowLessMoreButton(expanded = isExpanded, onClick = { isExpanded = !isExpanded })
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val metrics = if (isExpanded) {
|
||||
state.metrics
|
||||
} else {
|
||||
state.metrics.take(MAX_METRICS_COUNT).toImmutableList()
|
||||
}
|
||||
|
||||
GridItems(
|
||||
items = metrics,
|
||||
itemContent = {
|
||||
InfoPoint(infoPointUM = it)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock
|
||||
@Composable
|
||||
private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) {
|
||||
// FIXME add string resources
|
||||
val text = if (expanded) {
|
||||
"See less"
|
||||
} else {
|
||||
"See more"
|
||||
}
|
||||
|
||||
TextButton(
|
||||
text = text,
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.positiveButtonColors,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
Box(Modifier)
|
||||
},
|
||||
content = {
|
||||
GridItems(
|
||||
items = List(size = 6) { it }.toImmutableList(),
|
||||
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
itemContent = {
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun BlockPreview() {
|
||||
TangemThemePreview {
|
||||
MetricsBlock(
|
||||
state = MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
value = "A",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
actualContent = { BlockPreview() },
|
||||
shimmerContent = { MetricsBlockPlaceholder() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW8
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemAnimations
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.getText
|
||||
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) {
|
||||
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.markets_token_details_price_performance),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
),
|
||||
initialSelectedItem = PriceChangeInterval.H24,
|
||||
onClick = { interval ->
|
||||
currentInterval = interval
|
||||
state.onIntervalChanged(interval)
|
||||
},
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
) { interval ->
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = 14.dp,
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = interval.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val value = when (currentInterval) {
|
||||
PriceChangeInterval.H24 -> state.h24
|
||||
PriceChangeInterval.MONTH -> state.month
|
||||
PriceChangeInterval.ALL_TIME -> state.all
|
||||
else -> error("")
|
||||
}
|
||||
|
||||
Content(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = value,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) {
|
||||
val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(
|
||||
targetFraction = state.indicatorFraction,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_token_details_low),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SpacerW8()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_token_details_high),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
TangemLinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size6)
|
||||
.fillMaxWidth(),
|
||||
progress = { animatedIndicatorFraction },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = state.low,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = state.high,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
|
||||
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
|
||||
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(headerHeight)
|
||||
.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(35.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
SpacerW8()
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(35.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size6)
|
||||
.fillMaxWidth(),
|
||||
radius = 27.dp,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size56),
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
SpacerW8()
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size56),
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
PricePerformanceBlock(
|
||||
modifier = Modifier,
|
||||
state = PricePerformanceUM(
|
||||
h24 = PricePerformanceUM.Value(
|
||||
low = "\$38,5K",
|
||||
high = "\$58,5K",
|
||||
indicatorFraction = 0.5f,
|
||||
),
|
||||
month = PricePerformanceUM.Value(
|
||||
low = "\$500,5K",
|
||||
high = "\$5800,5K",
|
||||
indicatorFraction = 0.8f,
|
||||
),
|
||||
all = PricePerformanceUM.Value(
|
||||
low = "\$58,52",
|
||||
high = "\$580,5M",
|
||||
indicatorFraction = 0.2f,
|
||||
),
|
||||
onIntervalChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PlaceholderPreview() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
PricePerformanceBlockPlaceholder()
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlin.math.round
|
||||
|
||||
private const val STARS_COUNT = 5
|
||||
|
||||
@Composable
|
||||
internal fun ScoreStarsBlock(
|
||||
score: Float,
|
||||
horizontalSpacing: Dp,
|
||||
scoreTextStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val rounded = score.roundTo1decimal()
|
||||
val percentage = rounded / STARS_COUNT
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(horizontalSpacing),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = rounded.toString(),
|
||||
style = scoreTextStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Stars(fraction = percentage)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
|
||||
val grayColor = TangemTheme.colors.icon.inactive
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
repeat(times = 5) { i ->
|
||||
Box(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.requiredSize(16.dp)
|
||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||
.drawWithCache {
|
||||
onDrawWithContent {
|
||||
val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0)
|
||||
val starFractionFloat = starFraction
|
||||
.toFloat()
|
||||
.roundTo1decimal()
|
||||
|
||||
drawContent()
|
||||
drawRect(
|
||||
color = grayColor,
|
||||
topLeft = Offset(x = size.width * starFractionFloat, y = 0f),
|
||||
size = Size(size.width * (1 - starFractionFloat), size.height),
|
||||
blendMode = BlendMode.SrcIn,
|
||||
)
|
||||
}
|
||||
},
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_star_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun Float.roundTo1decimal(): Float {
|
||||
return round(this * 10) / 10
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.text.TooltipText
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = TangemTheme.dimens.size72)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TooltipText(
|
||||
text = resourceReference(R.string.markets_token_details_security_score),
|
||||
onInfoClick = state.onInfoClick,
|
||||
textStyle = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
ScoreStarsBlock(
|
||||
score = state.score,
|
||||
scoreTextStyle = TangemTheme.typography.body1,
|
||||
horizontalSpacing = TangemTheme.dimens.spacing8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = TangemTheme.dimens.size72)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction = 0.4f)
|
||||
.padding(vertical = TangemTheme.dimens.spacing2)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 328, showBackground = true)
|
||||
@Preview(widthDp = 328, showBackground = true, locale = "ru")
|
||||
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
SecurityScoreBlock(
|
||||
state = SecurityScoreUM(
|
||||
score = 3.5f,
|
||||
description = stringReference("Based on 3 ratings"),
|
||||
onInfoClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 328, showBackground = true)
|
||||
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
SecurityScoreBlockPlaceholder(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.details.impl.ui.preview.SecurityScorePreviewData
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent
|
||||
|
||||
@Composable
|
||||
internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<SecurityScoreBottomSheetContent>(
|
||||
config = config,
|
||||
addBottomInsets = false,
|
||||
title = { TangemBottomSheetTitle(title = it.title) },
|
||||
content = { content ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
Text(
|
||||
text = content.description.resolveReference(),
|
||||
style = TangemTheme.typography.body2.copy(
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
),
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
content.providers.fastForEachIndexed { index, provider ->
|
||||
DividerContainer(
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = content.providers.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
showDivider = index != content.providers.lastIndex,
|
||||
) {
|
||||
SecurityScoreProviderRow(
|
||||
providerUM = provider,
|
||||
onLinkClick = { content.onProviderLinkClick(provider) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH16()
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecurityScoreProviderRow(
|
||||
providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM,
|
||||
onLinkClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.heightIn(min = TangemTheme.dimens.size68),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size40)
|
||||
.clip(TangemTheme.shapes.roundedCorners8),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(providerUM.iconUrl)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.build(),
|
||||
loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
|
||||
error = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
Text(
|
||||
text = providerUM.name,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (providerUM.lastAuditDate != null) {
|
||||
Text(
|
||||
text = providerUM.lastAuditDate,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerWMax()
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
modifier = Modifier.clickable(
|
||||
enabled = providerUM.urlData != null,
|
||||
indication = ripple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onLinkClick,
|
||||
),
|
||||
) {
|
||||
ScoreStarsBlock(
|
||||
score = providerUM.score,
|
||||
scoreTextStyle = TangemTheme.typography.body2,
|
||||
horizontalSpacing = TangemTheme.dimens.spacing3,
|
||||
)
|
||||
|
||||
UrlBlock(providerUM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM) {
|
||||
val urlData = providerUM.urlData
|
||||
val rootHost = urlData?.rootHost
|
||||
if (urlData != null && rootHost != null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
Text(
|
||||
text = urlData.rootHost,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SecurityScoreBottomSheetPreview() {
|
||||
TangemThemePreview {
|
||||
SecurityScoreBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = SecurityScorePreviewData.bottomSheetContent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.items.DescriptionItem
|
||||
import com.tangem.core.ui.components.items.DescriptionPlaceholder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
||||
@Suppress("CanBeNonNullable")
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(
|
||||
state: MarketsTokenDetailsUM.Body,
|
||||
isAccountEnabled: Boolean,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
when (state) {
|
||||
MarketsTokenDetailsUM.Body.Loading -> {
|
||||
item("description-loading") {
|
||||
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
if (portfolioBlock != null) {
|
||||
item(key = "portfolio") {
|
||||
portfolioBlock(Modifier.blockPaddings())
|
||||
}
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
|
||||
loadingInfoBlocks()
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Content -> {
|
||||
if (state.description != null) {
|
||||
description(state.description)
|
||||
}
|
||||
|
||||
if (portfolioBlock != null) {
|
||||
item(key = "portfolio") {
|
||||
portfolioBlock(Modifier.blockPaddings())
|
||||
}
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
|
||||
infoBlocksList(state.infoBlocks)
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Error -> {
|
||||
error(state)
|
||||
}
|
||||
MarketsTokenDetailsUM.Body.Nothing -> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
|
||||
item("body-error") {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing40,
|
||||
),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.aboutCoinHeader() {
|
||||
item("aboutCoinHeader") {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
),
|
||||
text = stringResourceSafe(R.string.markets_about_coin_header),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
|
||||
item("description") {
|
||||
DescriptionItem(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
description = description.shortDescription,
|
||||
hasFullDescription = description.fullDescription != null,
|
||||
onReadMoreClick = description.onReadMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) {
|
||||
if (state.insights != null) {
|
||||
item("insights") {
|
||||
InsightsBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.insights,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.securityScore != null) {
|
||||
item("securityScore") {
|
||||
SecurityScoreBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.securityScore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.metrics != null) {
|
||||
item("metrics") {
|
||||
MetricsBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.metrics,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.pricePerformance != null) {
|
||||
item("pricePerformance") {
|
||||
PricePerformanceBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.pricePerformance,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "listedOn") {
|
||||
ListedOnBlock(
|
||||
state = state.listedOn,
|
||||
modifier = Modifier.blockPaddings(),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.links != null) {
|
||||
item("links") {
|
||||
LinksBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.links,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.loadingInfoBlocks() {
|
||||
item("insights-loading") {
|
||||
InsightsBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("securityScore-loading") {
|
||||
SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("metrics-loading") {
|
||||
MetricsBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("pricePerformance-loading") {
|
||||
PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item(key = "listedOn-loading") {
|
||||
ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("links-loading") {
|
||||
LinksBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.blockPaddings(): Modifier {
|
||||
return this.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.preview
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object MarketsTokenDetailsPreview {
|
||||
private val infoPoint = InfoPointUM(
|
||||
title = stringReference("1"),
|
||||
value = "2",
|
||||
change = InfoPointUM.ChangeType.DOWN,
|
||||
onInfoClick = {},
|
||||
)
|
||||
|
||||
val loadingState = MarketsTokenDetailsUM(
|
||||
tokenName = "Token Name",
|
||||
priceText = "$0.00000000324",
|
||||
dateTimeText = stringReference("Today"),
|
||||
priceChangePercentText = "52.00%",
|
||||
iconUrl = "",
|
||||
priceChangeType = PriceChangeType.UP,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = MarketChartDataProducer.build { },
|
||||
onLoadRetryClick = {},
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = { _, _ -> },
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = { },
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
isMarkerSet = false,
|
||||
triggerPriceChange = consumedEvent(),
|
||||
)
|
||||
|
||||
val contentState = MarketsTokenDetailsUM(
|
||||
tokenName = "Token Name",
|
||||
priceText = "$0.00000000324",
|
||||
dateTimeText = stringReference("Today"),
|
||||
priceChangePercentText = "52.00%",
|
||||
iconUrl = "",
|
||||
priceChangeType = PriceChangeType.UP,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = MarketChartDataProducer.build { },
|
||||
onLoadRetryClick = {},
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = { _, _ -> },
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = { },
|
||||
body = MarketsTokenDetailsUM.Body.Content(
|
||||
description = MarketsTokenDetailsUM.Description(
|
||||
shortDescription = stringReference("markets_token_details_description_short"),
|
||||
fullDescription = stringReference("markets_token_details_description_full"),
|
||||
onReadMoreClick = {},
|
||||
),
|
||||
infoBlocks = MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = InsightsUM(
|
||||
h24Info = persistentListOf(
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
),
|
||||
weekInfo = persistentListOf(
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
),
|
||||
monthInfo = persistentListOf(
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
),
|
||||
onInfoClick = {},
|
||||
onIntervalChanged = {},
|
||||
),
|
||||
securityScore = SecurityScoreUM(
|
||||
score = 2.3f,
|
||||
description = stringReference("markets_token_details_security_score_description"),
|
||||
onInfoClick = {},
|
||||
),
|
||||
metrics = MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
),
|
||||
),
|
||||
pricePerformance = PricePerformanceUM(
|
||||
h24 = PricePerformanceUM.Value(
|
||||
low = "1",
|
||||
high = "2",
|
||||
indicatorFraction = 0.3f,
|
||||
),
|
||||
month = PricePerformanceUM.Value(
|
||||
low = "1",
|
||||
high = "2",
|
||||
indicatorFraction = 0.3f,
|
||||
),
|
||||
all = PricePerformanceUM.Value(
|
||||
low = "1",
|
||||
high = "2",
|
||||
indicatorFraction = 0.3f,
|
||||
),
|
||||
onIntervalChanged = {},
|
||||
),
|
||||
listedOn = ListedOnUM.Empty,
|
||||
links = null,
|
||||
),
|
||||
),
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
isMarkerSet = true,
|
||||
triggerPriceChange = consumedEvent(),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.preview
|
||||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent
|
||||
|
||||
internal object SecurityScorePreviewData {
|
||||
|
||||
val bottomSheetContent = SecurityScoreBottomSheetContent(
|
||||
title = stringReference("Security score"),
|
||||
description = stringReference(
|
||||
"Security score of a token is a metric that assesses the " +
|
||||
"security level of a blockchain or token based on various factors and is compiled from " +
|
||||
"the sources listed below.",
|
||||
),
|
||||
providers = listOf(
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = "Moralis",
|
||||
lastAuditDate = "21.10.2024",
|
||||
score = 4.9F,
|
||||
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = "https://moralis.com/",
|
||||
rootHost = "moralis.com",
|
||||
),
|
||||
iconUrl = "",
|
||||
),
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = "Certik",
|
||||
lastAuditDate = "10.07.2024",
|
||||
score = 4.6F,
|
||||
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = "https://certik.com/",
|
||||
rootHost = "certik.com",
|
||||
),
|
||||
iconUrl = "",
|
||||
),
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = "Cyberscope",
|
||||
lastAuditDate = "25.06.2023",
|
||||
score = 4.5F,
|
||||
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = "https://cyberscope.com/",
|
||||
rootHost = "cyberscope.com",
|
||||
),
|
||||
iconUrl = "",
|
||||
),
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = "TokenInsight",
|
||||
lastAuditDate = "17.01.2022",
|
||||
score = 4.0F,
|
||||
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = "https://tokeninsight.com/",
|
||||
rootHost = "tokeninsight.com",
|
||||
),
|
||||
iconUrl = "",
|
||||
),
|
||||
|
||||
),
|
||||
onProviderLinkClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.plus
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.StringsSigns.DOT
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Exchanges bottom sheet content
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigContent {
|
||||
|
||||
/** Title of bottom sheet. Like, app bar. */
|
||||
@get:StringRes
|
||||
val titleResId: Int
|
||||
get() = R.string.markets_token_details_exchanges_title
|
||||
|
||||
/** Subtitle */
|
||||
@get:StringRes
|
||||
val subtitleResId: Int
|
||||
get() = R.string.markets_token_details_exchange
|
||||
|
||||
/** Volume info */
|
||||
@get:StringRes
|
||||
val volumeReference: TextReference
|
||||
get() = resourceReference(id = R.string.markets_token_details_volume) +
|
||||
stringReference(value = " $DOT ") +
|
||||
resourceReference(id = R.string.markets_selector_interval_24h_title)
|
||||
|
||||
/** Exchange items */
|
||||
val exchangeItems: ImmutableList<TokenItemState>
|
||||
|
||||
/**
|
||||
* Loading state
|
||||
*
|
||||
* @property exchangesCount count of exchanges
|
||||
*/
|
||||
data class Loading(val exchangesCount: Int) : ExchangesBottomSheetContent {
|
||||
|
||||
override val exchangeItems: ImmutableList<TokenItemState>
|
||||
get() = List(size = exchangesCount) { index -> TokenItemState.Loading(id = "loading#$index") }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Content state
|
||||
*
|
||||
* @property exchangeItems exchanges
|
||||
*/
|
||||
data class Content(
|
||||
override val exchangeItems: ImmutableList<TokenItemState>,
|
||||
) : ExchangesBottomSheetContent
|
||||
|
||||
/** Error state */
|
||||
data class Error(
|
||||
val onRetryClick: () -> Unit,
|
||||
) : ExchangesBottomSheetContent {
|
||||
override val exchangeItems: ImmutableList<TokenItemState> = persistentListOf()
|
||||
|
||||
@StringRes
|
||||
val message: Int = R.string.markets_loading_error_title
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class InfoBottomSheetContent(
|
||||
val title: TextReference,
|
||||
val body: TextReference,
|
||||
val generatedAINotificationUM: GeneratedAINotificationUM? = null,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
data class GeneratedAINotificationUM(val onClick: () -> Unit)
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class InfoPointUM(
|
||||
val title: TextReference,
|
||||
val value: String,
|
||||
val change: ChangeType? = null,
|
||||
val onInfoClick: (() -> Unit)? = null,
|
||||
) {
|
||||
enum class ChangeType {
|
||||
UP, DOWN
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class InsightsUM(
|
||||
val h24Info: ImmutableList<InfoPointUM>,
|
||||
val weekInfo: ImmutableList<InfoPointUM>,
|
||||
val monthInfo: ImmutableList<InfoPointUM>,
|
||||
val onInfoClick: () -> Unit,
|
||||
val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue