Updated on 2026-08-14
This commit is contained in:
commit
e52f9de341
987 changed files with 20247 additions and 8171 deletions
|
|
@ -66,7 +66,6 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
|
||||
/** DI */
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ import com.tangem.features.account.createedit.error.AccountFeatureError
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -147,7 +147,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
private fun logError(error: AccountFeatureError, params: Map<String, String> = emptyMap()) {
|
||||
val exception = IllegalStateException(error.toString())
|
||||
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
|
||||
analyticsExceptionHandler.sendException(
|
||||
event = ExceptionAnalyticsEvent(exception = exception, params = params),
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.account.usecase.ArchivedAccountList
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AccountArchivedUMBuilder @Inject constructor() {
|
||||
|
|
@ -49,7 +49,7 @@ internal class AccountArchivedUMBuilder @Inject constructor() {
|
|||
onCloseClick: () -> Unit,
|
||||
getArchivedAccounts: () -> Unit,
|
||||
): AccountArchivedUM.Error {
|
||||
Timber.e(throwable)
|
||||
TangemLogger.e("Error", throwable)
|
||||
return AccountArchivedUM.Error(
|
||||
onCloseClick = onCloseClick,
|
||||
onRetryClick = { getArchivedAccounts() },
|
||||
|
|
|
|||
|
|
@ -42,11 +42,11 @@ import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.
|
|||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName
|
||||
import com.tangem.features.account.createedit.error.AccountFeatureError
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
|
|
@ -242,7 +242,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
private fun onNameChange(name: AccountNameUM) {
|
||||
val isNotEmptyCustomName = (name as? AccountNameUM.Custom)?.raw?.isNotEmpty() == true
|
||||
if (!name.isValidName() && isNotEmptyCustomName) {
|
||||
Timber.d("Invalid account name: $name")
|
||||
TangemLogger.d("Invalid account name: $name")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -310,7 +310,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
private fun logError(error: AccountFeatureError, params: Map<String, String> = emptyMap()) {
|
||||
val exception = IllegalStateException(error.toString())
|
||||
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
|
||||
analyticsExceptionHandler.sendException(
|
||||
event = ExceptionAnalyticsEvent(exception = exception, params = params),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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.platform.testTag
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -31,6 +32,7 @@ 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.test.AccountDetailsScreenTestTags
|
||||
import com.tangem.features.account.details.entity.AccountDetailsUM
|
||||
|
||||
@Composable
|
||||
|
|
@ -130,7 +132,8 @@ private fun ManageTokensRow(state: AccountDetailsUM) {
|
|||
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.clickable(onClick = state.onManageTokensClick)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
.padding(all = TangemTheme.dimens.spacing12)
|
||||
.testTag(AccountDetailsScreenTestTags.MANAGE_TOKENS_BUTTON),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
|
|
@ -179,13 +180,10 @@ internal class PortfolioSelectorModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
portfolio.accountsBalance.accountStatuses.forEach { accountStatus ->
|
||||
portfolio.accountsBalance.accountStatuses.filterCryptoPortfolio().forEach { accountStatus ->
|
||||
val isEnabledByFeature = isEnabled(wallet, accountStatus)
|
||||
val account = accountStatus.account
|
||||
val accountBalance = when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
|
||||
is AccountStatus.Payment -> accountStatus.totalFiatBalance
|
||||
}
|
||||
val accountBalance = accountStatus.tokenList.totalFiatBalance
|
||||
val accountItemUM = AccountPortfolioItemUMConverter(
|
||||
onClick = { selectorController.selectAccount(account.accountId) },
|
||||
appCurrency = appCurrency,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent {
|
|||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val amount: String,
|
||||
val spenderAddress: String,
|
||||
val subtitle: TextReference,
|
||||
val amountFooter: TextReference,
|
||||
val feeFooter: TextReference,
|
||||
val isHoldToConfirm: Boolean = false,
|
||||
val isResetApproval: Boolean = false,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ dependencies {
|
|||
/** Other */
|
||||
implementation(deps.decompose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
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.R
|
||||
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
|
||||
|
|
@ -23,7 +24,6 @@ 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,
|
||||
|
|
@ -38,6 +38,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor(
|
|||
params = FeeSelectorParams.FeeSelectorBlockParams(
|
||||
state = FeeSelectorUM.Loading,
|
||||
onLoadFee = { model.loadFee() },
|
||||
onDisableCustomFee = { model.shouldDisableCustomFee() },
|
||||
onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) },
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
|
|
@ -70,24 +71,26 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor(
|
|||
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
titleText = resourceReference(CommonUiR.string.give_permission_title),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
titleText = resourceReference(
|
||||
if (uiState.isResetApproval) {
|
||||
R.string.update_approval_permission_title
|
||||
} else {
|
||||
R.string.give_permission_title
|
||||
},
|
||||
),
|
||||
titleAction = TopAppBarButtonUM.Icon(
|
||||
iconRes = CommonUiR.drawable.ic_information_24,
|
||||
onClicked = model::showPermissionInfoDialog,
|
||||
iconRes = R.drawable.ic_close_new_20,
|
||||
onClicked = model::onCancelClick,
|
||||
),
|
||||
) {
|
||||
GiveApprovalContent(
|
||||
currency = currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = uiState.approveType,
|
||||
approveItems = uiState.approveItems,
|
||||
amountFooter = params.amountFooter,
|
||||
feeFooter = params.feeFooter,
|
||||
uiState = uiState,
|
||||
onChangeApproveType = model::onChangeApproveType,
|
||||
walletInteractionIcon = uiState.walletInteractionIcon,
|
||||
isApproveEnabled = uiState.isApproveButtonEnabled,
|
||||
isApproveLoading = uiState.isApproveLoading,
|
||||
onApproveClick = model::onApproveClick,
|
||||
onCancelClick = model::onCancelClick,
|
||||
onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import arrow.core.Either
|
|||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionSender.MultipleTransactionSendMode
|
||||
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.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
|
|
@ -15,41 +17,45 @@ 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.navigation.url.UrlOpener
|
||||
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.AllowanceInfo
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase
|
||||
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.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
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 java.math.RoundingMode
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class GiveApprovalModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||
private val getAllowanceInfoUseCase: GetAllowanceInfoUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase,
|
||||
|
|
@ -76,11 +82,15 @@ internal class GiveApprovalModel @Inject constructor(
|
|||
walletInteractionIcon = walletInterationIcon(userWallet),
|
||||
isApproveButtonEnabled = false,
|
||||
isApproveLoading = false,
|
||||
isHoldToConfirm = params.isHoldToConfirm,
|
||||
isResetApproval = params.isResetApproval,
|
||||
),
|
||||
)
|
||||
|
||||
private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading
|
||||
|
||||
private var approvalTxList: Map<TransactionData.Uncompiled, TransactionFee> = emptyMap()
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
this.feeSelectorUM = feeSelectorUM
|
||||
uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) }
|
||||
|
|
@ -121,47 +131,94 @@ internal class GiveApprovalModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
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.userWalletId,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
return onApprovalTx(
|
||||
onApprove = { approve ->
|
||||
getFeeUseCase(
|
||||
transactionData = approve,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
).onRight { fee ->
|
||||
approvalTxList = mapOf(approve to fee)
|
||||
}
|
||||
},
|
||||
onResetApprove = { (revokeApproval, approve) ->
|
||||
getFeeUseCase(
|
||||
transactionData = revokeApproval,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
).map { revokeFee ->
|
||||
estimateFeeForResetApproval(
|
||||
revokeTransactionFee = revokeFee,
|
||||
revokeApprovalTransaction = revokeApproval,
|
||||
approvalTransaction = approve,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return getFeeUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
fun shouldDisableCustomFee(): Boolean {
|
||||
return approvalTxList.size > 1
|
||||
}
|
||||
|
||||
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
val approve = createApprovalTransactionUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = (params.cryptoCurrencyStatus.currency as CryptoCurrency.Token).contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to create approveTransaction", error)
|
||||
return GetFeeError.DataError(error).left()
|
||||
}
|
||||
|
||||
return if (maybeToken == null) {
|
||||
getFeeForGaslessUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
transactionData = approve,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
} else {
|
||||
getFeeForTokenUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
transactionData = approve,
|
||||
userWallet = userWallet,
|
||||
token = maybeToken.currency,
|
||||
)
|
||||
}.onRight { feeExtended ->
|
||||
approvalTxList = mapOf(approve to feeExtended.transactionFee)
|
||||
}
|
||||
}
|
||||
|
||||
private fun estimateFeeForResetApproval(
|
||||
revokeTransactionFee: TransactionFee,
|
||||
revokeApprovalTransaction: TransactionData.Uncompiled,
|
||||
approvalTransaction: TransactionData.Uncompiled,
|
||||
) = when (revokeTransactionFee) {
|
||||
is TransactionFee.Choosable -> {
|
||||
val approveFee = revokeTransactionFee.copy(
|
||||
minimum = revokeTransactionFee.minimum.increaseEthereumGasLimitBy(2.toBigDecimal()),
|
||||
normal = revokeTransactionFee.normal.increaseEthereumGasLimitBy(2.toBigDecimal()),
|
||||
priority = revokeTransactionFee.priority.increaseEthereumGasLimitBy(2.toBigDecimal()),
|
||||
)
|
||||
approvalTxList = mapOf(
|
||||
revokeApprovalTransaction to revokeTransactionFee,
|
||||
approvalTransaction to approveFee,
|
||||
)
|
||||
revokeTransactionFee.copy(
|
||||
minimum = approveFee.minimum + revokeTransactionFee.minimum,
|
||||
normal = approveFee.normal + revokeTransactionFee.normal,
|
||||
priority = approveFee.priority + revokeTransactionFee.priority,
|
||||
)
|
||||
}
|
||||
is TransactionFee.Single -> {
|
||||
val approveFee = revokeTransactionFee.copy(normal = revokeTransactionFee.normal)
|
||||
approvalTxList = mapOf(
|
||||
revokeApprovalTransaction to revokeTransactionFee,
|
||||
approvalTransaction to approveFee,
|
||||
)
|
||||
revokeTransactionFee.copy(normal = approveFee.normal + revokeTransactionFee.normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,38 +227,37 @@ internal class GiveApprovalModel @Inject constructor(
|
|||
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.userWalletId,
|
||||
amount = getApprovalAmount(),
|
||||
fee = selectedFee,
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
Timber.e(error, "Failed to create approval transaction")
|
||||
return false
|
||||
val transactions = approvalTxList.map { (tx, fee) ->
|
||||
tx.copy(
|
||||
fee = when (feeContent.selectedFeeItem) {
|
||||
is FeeItem.Fast -> (fee as? TransactionFee.Choosable)?.priority ?: fee.normal
|
||||
is FeeItem.Market -> fee.normal
|
||||
is FeeItem.Slow -> (fee as? TransactionFee.Choosable)?.minimum ?: fee.normal
|
||||
else -> feeContent.selectedFeeItem.fee
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return if (isFeeInTokenCurrency) {
|
||||
createAndSendGaslessTransactionUseCase(
|
||||
userWallet = userWallet,
|
||||
transactionData = transactionData,
|
||||
transactionData = transactions.first(),
|
||||
fee = feeExtended,
|
||||
)
|
||||
} else {
|
||||
sendTransactionUseCase(
|
||||
txData = transactionData,
|
||||
txsData = transactions,
|
||||
userWallet = userWallet,
|
||||
network = tokenCurrency.network,
|
||||
sendMode = MultipleTransactionSendMode.DEFAULT,
|
||||
)
|
||||
}.fold(
|
||||
ifLeft = { error ->
|
||||
Timber.e("Failed to send approval transaction: $error")
|
||||
TangemLogger.e("Failed to send approval transaction: $error")
|
||||
false
|
||||
},
|
||||
ifRight = {
|
||||
|
|
@ -240,4 +296,88 @@ internal class GiveApprovalModel @Inject constructor(
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> onApprovalTx(
|
||||
onApprove: suspend (TransactionData.Uncompiled) -> Either<GetFeeError, T>,
|
||||
onResetApprove:
|
||||
suspend (Pair<TransactionData.Uncompiled, TransactionData.Uncompiled>) -> Either<GetFeeError, T>,
|
||||
): Either<GetFeeError, T> {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||
?: return GetFeeError.DataError(IllegalStateException("Currency is not a token")).left()
|
||||
|
||||
val amount = params.amount.toBigDecimalOrNull()
|
||||
?: return GetFeeError.DataError(IllegalArgumentException("Invalid amount format")).left()
|
||||
|
||||
val allowance = getAllowanceInfoUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
spenderAddress = params.spenderAddress,
|
||||
requiredAmount = amount,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to get allowance info", error)
|
||||
return GetFeeError.DataError(error).left()
|
||||
}
|
||||
|
||||
val approve = createApprovalTransactionUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to create approveTransaction", error)
|
||||
return GetFeeError.DataError(error).left()
|
||||
}
|
||||
|
||||
return if (allowance is AllowanceInfo.ResetNeeded) {
|
||||
val revokeApproval = createApprovalTransactionUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amount = BigDecimal.ZERO,
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to create revoke approveTransaction", error)
|
||||
return GetFeeError.DataError(error).left()
|
||||
}
|
||||
onResetApprove(revokeApproval to approve)
|
||||
} else {
|
||||
onApprove(approve)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Fee.increaseEthereumGasLimitBy(multiplier: BigDecimal): Fee {
|
||||
if (this !is Fee.Ethereum) return this
|
||||
val increasedGasPrice = amount.value?.movePointRight(amount.decimals)
|
||||
?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP)
|
||||
val increasedGasLimit = gasLimit
|
||||
.multiply(multiplier.toBigInteger())
|
||||
val increasedAmount = amount.copy(
|
||||
value = increasedGasPrice?.multiply(
|
||||
increasedGasLimit.toBigDecimal().movePointLeft(amount.decimals),
|
||||
),
|
||||
)
|
||||
return when (this) {
|
||||
is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
|
||||
is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
|
||||
is Fee.Ethereum.TokenCurrency -> error("handle in [REDACTED_TASK_KEY]")
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Fee.plus(otherFee: Fee): Fee {
|
||||
if (this !is Fee.Ethereum || otherFee !is Fee.Ethereum) return this
|
||||
val gasLimit = this.gasLimit
|
||||
val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals)
|
||||
?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP)
|
||||
val increasedGasLimit = gasLimit + otherFee.gasLimit
|
||||
val increasedAmount = this.amount.copy(
|
||||
value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals),
|
||||
)
|
||||
return when (this) {
|
||||
is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
|
||||
is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
|
||||
is Fee.Ethereum.TokenCurrency -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,4 +11,6 @@ internal data class GiveApprovalUM(
|
|||
@DrawableRes val walletInteractionIcon: Int?,
|
||||
val isApproveButtonEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
val isHoldToConfirm: Boolean = false,
|
||||
val isResetApproval: Boolean = false,
|
||||
)
|
||||
|
|
@ -27,41 +27,37 @@ 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.R
|
||||
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.approval.impl.model.GiveApprovalUM
|
||||
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>,
|
||||
uiState: GiveApprovalUM,
|
||||
amountFooter: TextReference,
|
||||
feeFooter: TextReference,
|
||||
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(),
|
||||
text = amountFooter.resolveReference(),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -72,35 +68,39 @@ internal fun GiveApprovalContent(
|
|||
|
||||
ApprovalInfo(
|
||||
currency = currency,
|
||||
approveType = approveType,
|
||||
approveItems = approveItems,
|
||||
approveType = uiState.approveType,
|
||||
approveItems = uiState.approveItems,
|
||||
onChangeApproveType = onChangeApproveType,
|
||||
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
feeFooter = feeFooter,
|
||||
isResetApproval = uiState.isResetApproval,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
if (uiState.isHoldToConfirm) {
|
||||
HoldToConfirmButton(
|
||||
text = stringResourceSafe(id = R.string.common_approve),
|
||||
enabled = uiState.isApproveButtonEnabled,
|
||||
isLoading = uiState.isApproveLoading,
|
||||
onConfirm = onApproveClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
} else {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResourceSafe(id = R.string.common_approve),
|
||||
iconResId = uiState.walletInteractionIcon,
|
||||
showProgress = uiState.isApproveLoading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = onApproveClick,
|
||||
enabled = uiState.isApproveButtonEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH16()
|
||||
}
|
||||
|
|
@ -111,14 +111,16 @@ internal fun GiveApprovalContent(
|
|||
private fun ApprovalInfo(
|
||||
currency: String,
|
||||
approveType: ApproveType,
|
||||
isResetApproval: Boolean,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
feeFooter: TextReference,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = annotatedReference {
|
||||
append(stringResourceSafe(CommonUiR.string.swap_approve_description))
|
||||
append(stringResourceSafe(R.string.swap_approve_description))
|
||||
append(" ")
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
|
|
@ -127,7 +129,7 @@ private fun ApprovalInfo(
|
|||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = stringResourceSafe(CommonUiR.string.common_learn_more),
|
||||
text = stringResourceSafe(R.string.common_learn_more),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
|
|
@ -144,7 +146,11 @@ private fun ApprovalInfo(
|
|||
}
|
||||
SpacerH16()
|
||||
FooterContainer(
|
||||
footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer),
|
||||
footer = if (isResetApproval) {
|
||||
resourceReference(R.string.update_approval_permission_fee_note)
|
||||
} else {
|
||||
feeFooter
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
feeSelectorBlockComponent.Content(
|
||||
|
|
@ -189,7 +195,7 @@ private fun AmountItem(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency),
|
||||
text = stringResourceSafe(id = R.string.give_permission_rows_amount, currency),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 1,
|
||||
|
|
@ -202,7 +208,7 @@ private fun AmountItem(
|
|||
maxLines = 1,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)),
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
|
||||
|
|
@ -262,10 +268,10 @@ private fun DropdownSelector(
|
|||
Text(
|
||||
text = when (item) {
|
||||
ApproveType.LIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_current_transaction,
|
||||
id = R.string.give_permission_current_transaction,
|
||||
)
|
||||
ApproveType.UNLIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_unlimited,
|
||||
id = R.string.give_permission_unlimited,
|
||||
)
|
||||
},
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -275,7 +281,7 @@ private fun DropdownSelector(
|
|||
SpacerWMax()
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24),
|
||||
image = ImageVector.vectorResource(id = R.drawable.ic_check_24),
|
||||
),
|
||||
tint = color,
|
||||
contentDescription = null,
|
||||
|
|
@ -302,15 +308,11 @@ private fun GiveApprovalContentPreview(
|
|||
TangemThemePreview {
|
||||
GiveApprovalContent(
|
||||
currency = params.currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = params.approveType,
|
||||
approveItems = params.approveItems,
|
||||
amountFooter = params.amountFooter,
|
||||
feeFooter = params.feeFooter,
|
||||
uiState = params.uiState,
|
||||
onChangeApproveType = {},
|
||||
walletInteractionIcon = params.walletInteractionIcon,
|
||||
isApproveEnabled = params.isApproveEnabled,
|
||||
isApproveLoading = params.isApproveLoading,
|
||||
onApproveClick = {},
|
||||
onCancelClick = {},
|
||||
onOpenLearnMoreAboutApproveClick = {},
|
||||
feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(),
|
||||
)
|
||||
|
|
@ -319,12 +321,9 @@ private fun GiveApprovalContentPreview(
|
|||
|
||||
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,
|
||||
val amountFooter: TextReference,
|
||||
val feeFooter: TextReference,
|
||||
val uiState: GiveApprovalUM,
|
||||
)
|
||||
|
||||
private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<GiveApprovalPreviewParams> {
|
||||
|
|
@ -332,21 +331,40 @@ private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<Give
|
|||
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,
|
||||
amountFooter = stringReference("Allow this app to access your USDT"),
|
||||
feeFooter = stringReference("The network will charge a token approval fee"),
|
||||
uiState = GiveApprovalUM(
|
||||
approveType = ApproveType.LIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = R.drawable.ic_tangem_24,
|
||||
isApproveButtonEnabled = 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,
|
||||
amountFooter = stringReference("Allow this app to access your USDC"),
|
||||
feeFooter = stringReference("The network will charge a token approval fee"),
|
||||
uiState = GiveApprovalUM(
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = R.drawable.ic_tangem_24,
|
||||
isApproveButtonEnabled = false,
|
||||
isApproveLoading = true,
|
||||
),
|
||||
),
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDC",
|
||||
amountFooter = stringReference("Allow this app to access your USDC"),
|
||||
feeFooter = stringReference("The network will charge a token approval fee"),
|
||||
uiState = GiveApprovalUM(
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = R.drawable.ic_tangem_24,
|
||||
isApproveButtonEnabled = true,
|
||||
isApproveLoading = false,
|
||||
isResetApproval = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.wallets)
|
||||
|
|
@ -45,7 +46,6 @@ dependencies {
|
|||
}
|
||||
|
||||
/** Other */
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@ import com.tangem.features.biometry.AskBiometryComponent
|
|||
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -88,7 +88,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
* because it will be automatically saved on UserWalletsListManager switch
|
||||
*/
|
||||
val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run {
|
||||
Timber.e("Unable to save user wallet")
|
||||
TangemLogger.e("Unable to save user wallet")
|
||||
uiMessageSender.send(
|
||||
SnackbarMessage(stringReference("No selected user wallet")),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
|
||||
/** DI */
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
|
||||
/** DI */
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.dialog.Dialogs
|
||||
import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
|
|
@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
|
@ -211,7 +211,7 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("User wallet not created")
|
||||
TangemLogger.e("User wallet not created")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
when (error) {
|
||||
is SaveWalletError.DataError -> Timber.e(error.toString(), "Unable to save user wallet")
|
||||
is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error")
|
||||
is SaveWalletError.WalletAlreadySaved -> {
|
||||
userWalletsListRepository.unlock(
|
||||
userWalletId = userWallet.walletId,
|
||||
|
|
@ -246,17 +246,12 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
private fun handleScanError(error: TangemError) {
|
||||
when (error) {
|
||||
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
|
||||
is TangemSdkError -> Timber.e(error, "Scan error occurred")
|
||||
else -> Timber.e(error, "Error happened")
|
||||
is TangemSdkError -> TangemLogger.e("Scan error occurred", error)
|
||||
else -> TangemLogger.e("Error happened", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNfcFeatureUnavailable() {
|
||||
uiMessageSender.send(
|
||||
message = DialogMessage(
|
||||
message = resourceReference(R.string.nfc_error_unavailable),
|
||||
title = resourceReference(id = R.string.common_error),
|
||||
),
|
||||
)
|
||||
uiMessageSender.send(Dialogs.nfcFeatureUnavailable())
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ dependencies {
|
|||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common.ui)
|
||||
|
|
@ -78,7 +79,6 @@ dependencies {
|
|||
/* Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.reKotlin)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.arrow.fx)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.details.model
|
|||
|
||||
import android.content.res.Resources
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -84,8 +84,8 @@ internal class DetailsModel @Inject constructor(
|
|||
|
||||
val isWalletConnectAvailable = runBlocking {
|
||||
// danger region, this works immediately, but will be refactored later with WC
|
||||
checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse {
|
||||
Timber.w("Unable to check WalletConnect availability: $it")
|
||||
checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { throwable ->
|
||||
TangemLogger.w("Unable to check WalletConnect availability: $throwable")
|
||||
|
||||
false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -115,7 +115,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
unlockWalletUseCase(userWalletId)
|
||||
.onRight { router.push(AppRoute.WalletSettings(userWalletId)) }
|
||||
.onLeft { error ->
|
||||
Timber.e("Failed to unlock wallet $userWalletId: $error")
|
||||
TangemLogger.e("Failed to unlock wallet $userWalletId: $error")
|
||||
error.handle(
|
||||
onUserCancelled = {},
|
||||
isFromUnlockAll = false,
|
||||
|
|
@ -142,7 +142,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
applyUserWalletListSortingUseCase(userWalletIds).onRight {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletsReorder())
|
||||
}.onLeft { error ->
|
||||
Timber.e("Failed to apply wallet list sorting: $error")
|
||||
TangemLogger.e("Failed to apply wallet list sorting: $error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ dependencies {
|
|||
|
||||
/** Other dependencies */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import android.webkit.WebResourceError
|
|||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import com.google.accompanist.web.AccompanistWebViewClient
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* [AccompanistWebViewClient] for Disclaimer [WebView]
|
||||
|
|
@ -24,7 +24,7 @@ internal class DisclaimerWebViewClient(
|
|||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
|
||||
Timber.d("onPageStarted: $url")
|
||||
TangemLogger.d("onPageStarted: $url")
|
||||
if (url != null) {
|
||||
loadedUrls[url] = false
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ internal class DisclaimerWebViewClient(
|
|||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
|
||||
Timber.d("onPageFinished: $url")
|
||||
TangemLogger.d("onPageFinished: $url")
|
||||
if (url != null && loadedUrls.containsKey(url)) {
|
||||
loadedUrls[url] = true
|
||||
onLoadingFinished(false)
|
||||
|
|
@ -45,7 +45,7 @@ internal class DisclaimerWebViewClient(
|
|||
|
||||
val url = request?.url?.toString()
|
||||
|
||||
Timber.d("onReceivedError: $url")
|
||||
TangemLogger.d("onReceivedError: $url")
|
||||
|
||||
if (url != null && loadedUrls.containsKey(url)) {
|
||||
loadedUrls[url] = true
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ interface AddToPortfolioComponent : ComposableBottomSheetComponent {
|
|||
|
||||
interface Callback {
|
||||
fun onDismiss()
|
||||
// todo swap add new onSuccess with full data of added token
|
||||
fun onSuccess(addedToken: CryptoCurrency)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
interface AddToPortfolioManager {
|
||||
|
||||
// todo swap make updatable
|
||||
val token: TokenMarketParams
|
||||
val analyticsParams: AnalyticsParams?
|
||||
val portfolioFetcher: PortfolioFetcher
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ dependencies {
|
|||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.earn)
|
||||
implementation(projects.domain.search)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
|
|
@ -75,7 +76,6 @@ dependencies {
|
|||
|
||||
/* Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
|
||||
/* Core */
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
|
|
@ -87,14 +86,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
innerRouter.push(
|
||||
route = FeedEntryChildFactory.Child.TokenList(
|
||||
params = DefaultMarketsTokenListComponent.Params(
|
||||
onBackClicked = { onChildBack() },
|
||||
onTokenClick = { token, currency ->
|
||||
onMarketItemClick(
|
||||
token = token,
|
||||
appCurrency = currency,
|
||||
source = AnalyticsParam.ScreensSources.Market.value,
|
||||
)
|
||||
},
|
||||
preselectedSortType = sortBy ?: SortByTypeUM.Rating,
|
||||
shouldAlwaysShowSearchBar = sortBy == null,
|
||||
),
|
||||
|
|
@ -134,11 +125,11 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun onOpenEarnPage() {
|
||||
innerRouter.push(
|
||||
FeedEntryChildFactory.Child.Earn(
|
||||
params = DefaultEarnComponent.Params(onBackClick = { onChildBack() }),
|
||||
),
|
||||
)
|
||||
innerRouter.push(FeedEntryChildFactory.Child.Earn)
|
||||
}
|
||||
|
||||
override fun openSearch() {
|
||||
innerRouter.push(FeedEntryChildFactory.Child.Search)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,14 +235,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
)
|
||||
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
|
||||
DefaultMarketsTokenListComponent.Params(
|
||||
onBackClicked = { router.pop() },
|
||||
onTokenClick = { token, currency ->
|
||||
clickIntents.onMarketItemClick(
|
||||
token = token,
|
||||
appCurrency = currency,
|
||||
source = AnalyticsParam.ScreensSources.Market.value,
|
||||
)
|
||||
},
|
||||
preselectedSortType = SortByTypeUM.Rating,
|
||||
shouldAlwaysShowSearchBar = false,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,15 +7,18 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
|
||||
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent.Params
|
||||
import com.tangem.features.feed.components.search.DefaultSearchComponent
|
||||
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import kotlinx.serialization.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -53,9 +56,14 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
|
||||
@Serializable
|
||||
@Immutable
|
||||
data class Earn(val params: DefaultEarnComponent.Params) : Child
|
||||
data object Earn : Child
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
data object Search : Child
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun createChild(
|
||||
child: Child,
|
||||
appComponentContext: AppComponentContext,
|
||||
|
|
@ -75,6 +83,17 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
DefaultMarketsTokenListComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = child.params,
|
||||
clickIntents = DefaultMarketsTokenListComponent.ClickIntents(
|
||||
onBackClicked = onBackClicked,
|
||||
onSearchClicked = feedEntryClickIntents::openSearch,
|
||||
onTokenClick = { token, currency ->
|
||||
feedEntryClickIntents.onMarketItemClick(
|
||||
token = token,
|
||||
appCurrency = currency,
|
||||
source = AnalyticsParam.ScreensSources.Market.value,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
is Child.NewsDetails -> {
|
||||
|
|
@ -86,7 +105,7 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
Child.NewsList -> {
|
||||
DefaultNewsListComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = DefaultNewsListComponent.Params(
|
||||
params = Params(
|
||||
onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig ->
|
||||
feedEntryClickIntents.onArticleClick(
|
||||
articleId = currentArticle,
|
||||
|
|
@ -102,7 +121,7 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
Child.Feed -> {
|
||||
DefaultFeedComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents),
|
||||
params = FeedParams(feedClickIntents = feedEntryClickIntents),
|
||||
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
|
||||
promoBannersBlockComponentFactory = promoBannersBlockComponentFactory,
|
||||
newPromoBannersFeatureToggles = newPromoBannersFeatureToggles,
|
||||
|
|
@ -111,10 +130,19 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
is Child.Earn -> {
|
||||
DefaultEarnComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = child.params,
|
||||
params = DefaultEarnComponent.Params(
|
||||
onBackClick = onBackClicked,
|
||||
onSearchClicked = feedEntryClickIntents::openSearch,
|
||||
),
|
||||
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
|
||||
)
|
||||
}
|
||||
Child.Search -> DefaultSearchComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = DefaultSearchComponent.Params(
|
||||
onBackClick = onBackClicked,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,19 @@
|
|||
package com.tangem.features.feed.components.earn
|
||||
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
|
|
@ -15,15 +25,20 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.components.feed.FeedBottomSheetRoute
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.model.earn.EarnModel
|
||||
import com.tangem.features.feed.ui.components.FeedSearchBar
|
||||
import com.tangem.features.feed.ui.earn.EarnContent
|
||||
import kotlinx.serialization.Serializable
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
|
||||
internal class DefaultEarnComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
|
|
@ -44,23 +59,63 @@ internal class DefaultEarnComponent(
|
|||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val state by earnModel.state.collectAsStateWithLifecycle()
|
||||
TangemTopAppBar(
|
||||
containerColor = background,
|
||||
title = stringResourceSafe(R.string.earn_title),
|
||||
startButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onClicked = state.onBackClick,
|
||||
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
),
|
||||
)
|
||||
if (LocalRedesignEnabled.current) {
|
||||
FeedSearchBar(
|
||||
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
feedListSearchBar = state.feedListSearchBar,
|
||||
modifier = Modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.hazeEffectTangem {
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = .55f,
|
||||
endIntensity = 0f,
|
||||
preferPerformance = true,
|
||||
easing = EaseOut,
|
||||
)
|
||||
},
|
||||
startContent = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.neutral.primary,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x11)
|
||||
.background(
|
||||
color = TangemTheme.colors2.button.backgroundSecondary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickableSingle(
|
||||
onClick = state.onBackClick,
|
||||
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
)
|
||||
.padding(TangemTheme.dimens2.x2),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
TangemTopAppBar(
|
||||
containerColor = background,
|
||||
title = stringResourceSafe(R.string.earn_title),
|
||||
startButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onClicked = state.onBackClick,
|
||||
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
override fun Content(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
val state by earnModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
EarnContent(
|
||||
contentPadding = contentPadding,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
@ -91,8 +146,8 @@ internal class DefaultEarnComponent(
|
|||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val onBackClick: () -> Unit,
|
||||
val onSearchClicked: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.feed.components.feed
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -18,14 +19,14 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
|||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent
|
||||
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent.Params
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import com.tangem.features.feed.model.feed.FeedModelClickIntents
|
||||
import com.tangem.features.feed.ui.feed.FeedList
|
||||
import com.tangem.features.feed.ui.feed.FeedListHeader
|
||||
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
|
||||
internal class DefaultFeedComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
|
|
@ -64,7 +65,11 @@ internal class DefaultFeedComponent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
override fun Content(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
feedComponentModel.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
|
|
@ -78,6 +83,7 @@ internal class DefaultFeedComponent(
|
|||
modifier = modifier,
|
||||
state = state,
|
||||
promoBannersBlockComponent = promoBannersBlockComponent,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
package com.tangem.features.feed.components.market.details
|
||||
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
|
|
@ -12,9 +21,16 @@ 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.R
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -24,6 +40,7 @@ import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnal
|
|||
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
|
||||
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent
|
||||
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -86,19 +103,73 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
MarketsTokenDetailsTopBar(
|
||||
onBackClick = { params.onBackClicked() },
|
||||
isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
shouldShowPriceSubtitle = state.shouldShowPriceSubtitle,
|
||||
tokenName = state.tokenName,
|
||||
tokenPrice = state.priceText,
|
||||
backgroundColor = background,
|
||||
onShareClick = state.onShareClick,
|
||||
)
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TangemTopBar(
|
||||
modifier = Modifier.hazeEffectTangem {
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = .55f,
|
||||
endIntensity = 0f,
|
||||
preferPerformance = true,
|
||||
easing = EaseOut,
|
||||
)
|
||||
},
|
||||
startContent = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.neutral.primary,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x11)
|
||||
.background(
|
||||
color = TangemTheme.colors2.button.backgroundSecondary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickableSingle(
|
||||
onClick = { params.onBackClicked() },
|
||||
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
)
|
||||
.padding(TangemTheme.dimens2.x2),
|
||||
)
|
||||
},
|
||||
endContent = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_share_new_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.neutral.primary,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x11)
|
||||
.background(
|
||||
color = TangemTheme.colors2.button.backgroundSecondary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickableSingle(
|
||||
onClick = state.onShareClick,
|
||||
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
)
|
||||
.padding(TangemTheme.dimens2.x2_5),
|
||||
)
|
||||
},
|
||||
type = TangemTopBarType.BottomSheet,
|
||||
)
|
||||
} else {
|
||||
MarketsTokenDetailsTopBar(
|
||||
onBackClick = { params.onBackClicked() },
|
||||
isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
shouldShowPriceSubtitle = state.shouldShowPriceSubtitle,
|
||||
tokenName = state.tokenName,
|
||||
tokenPrice = state.priceText,
|
||||
backgroundColor = background,
|
||||
onShareClick = state.onShareClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
override fun Content(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
model.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
|
|
@ -112,6 +183,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
}
|
||||
|
||||
MarketsTokenDetailsContent(
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
backgroundColor = LocalMainBottomSheetColor.current.value,
|
||||
state = state,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.markets.TokenMarketParams
|
|||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -28,7 +29,7 @@ internal class AvailableToAddDataConverter @Inject constructor(
|
|||
availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
marketParams: TokenMarketParams,
|
||||
): AvailableToAddData {
|
||||
suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? {
|
||||
suspend fun AccountStatus.CryptoPortfolio.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? {
|
||||
val currencies = availableNetworks
|
||||
.mapNotNull { network ->
|
||||
createCryptoCurrency(
|
||||
|
|
@ -64,7 +65,7 @@ internal class AvailableToAddDataConverter @Inject constructor(
|
|||
val (_, balance) = entry
|
||||
val wallet = balance.userWallet
|
||||
val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks)
|
||||
val accounts = balance.accountsBalance.accountStatuses
|
||||
val accounts = balance.accountsBalance.accountStatuses.filterCryptoPortfolio()
|
||||
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount> = accounts
|
||||
.mapNotNull { accountStatus ->
|
||||
val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null
|
||||
|
|
@ -103,17 +104,13 @@ internal class AvailableToAddDataConverter @Inject constructor(
|
|||
userWallet: UserWallet,
|
||||
network: TokenMarketInfo.Network,
|
||||
marketParams: TokenMarketParams,
|
||||
account: Account,
|
||||
account: Account.CryptoPortfolio,
|
||||
): CryptoCurrency? {
|
||||
val derivationIndex = when (account) {
|
||||
is Account.CryptoPortfolio -> account.derivationIndex
|
||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||
}
|
||||
return getTokenMarketCryptoCurrency(
|
||||
userWalletId = userWallet.walletId,
|
||||
tokenMarketParams = marketParams,
|
||||
network = network,
|
||||
accountIndex = derivationIndex,
|
||||
accountIndex = account.derivationIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ import kotlinx.coroutines.channels.Channel
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TOKEN_ACTIONS_DELAY = 500L
|
||||
|
|
@ -205,7 +205,7 @@ internal class AddToPortfolioModel @Inject constructor(
|
|||
finishFlow()
|
||||
}
|
||||
.catch { throwable ->
|
||||
Timber.e(throwable)
|
||||
TangemLogger.e("Error", throwable)
|
||||
params.callback.onDismiss()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
|
|||
finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId)
|
||||
}
|
||||
.catch { throwable ->
|
||||
Timber.e(throwable)
|
||||
TangemLogger.e("Error", throwable)
|
||||
params.callback.onDismiss()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
|||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.feed.impl.R
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
class CheckCurrencyUnsupportedDelegate @Inject constructor(
|
||||
|
|
@ -29,14 +29,14 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor(
|
|||
networkId = rawNetworkId,
|
||||
isMainNetwork = isMainNetwork,
|
||||
).getOrElse { throwable ->
|
||||
Timber.e(
|
||||
throwable,
|
||||
TangemLogger.e(
|
||||
"""
|
||||
Failed to check currency unsupported state
|
||||
|- User wallet ID: $userWalletId
|
||||
|- Network ID: $rawNetworkId
|
||||
|- Is main network: $isMainNetwork
|
||||
""".trimIndent(),
|
||||
throwable,
|
||||
)
|
||||
|
||||
val message = SnackbarMessage(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.markets.TokenMarketInfo
|
|||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -192,15 +193,12 @@ internal class MarketsPortfolioDelegate @AssistedInject constructor(
|
|||
}.distinctUntilChanged()
|
||||
|
||||
private fun AccountStatusList.filterByRawID(): List<AccountWithAdded> {
|
||||
fun AccountStatus.filterByRawID(): List<CryptoCurrencyStatus> = when (this) {
|
||||
is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies()
|
||||
.filter { status ->
|
||||
val currencyId = status.currency.id.rawCurrencyId ?: return@filter false
|
||||
getTokenIdIfL2Network(currencyId.value) == currencyRawId.value
|
||||
}
|
||||
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
|
||||
}
|
||||
return accountStatuses.map { accountStatus ->
|
||||
fun AccountStatus.CryptoPortfolio.filterByRawID(): List<CryptoCurrencyStatus> = tokenList.flattenCurrencies()
|
||||
.filter { status ->
|
||||
val currencyId = status.currency.id.rawCurrencyId ?: return@filter false
|
||||
getTokenIdIfL2Network(currencyId.value) == currencyRawId.value
|
||||
}
|
||||
return accountStatuses.filterCryptoPortfolio().map { accountStatus ->
|
||||
AccountWithAdded(
|
||||
accountStatus = accountStatus,
|
||||
addedCurrency = accountStatus.filterByRawID(),
|
||||
|
|
|
|||
|
|
@ -1,45 +1,110 @@
|
|||
package com.tangem.features.feed.components.market.list
|
||||
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.model.market.list.MarketsListModel
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.components.FeedSearchBar
|
||||
import com.tangem.features.feed.ui.market.list.MarketsList
|
||||
import com.tangem.features.feed.ui.market.list.TopBarWithSearch
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal class DefaultMarketsTokenListComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
private val clickIntents: ClickIntents,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: MarketsListModel = getOrCreateModel<MarketsListModel, Params>(params = params)
|
||||
private val model: MarketsListModel = getOrCreateModel<MarketsListModel, ModelParams>(
|
||||
params = ModelParams(
|
||||
params = params,
|
||||
clickIntents = clickIntents,
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bsState by bottomSheetState
|
||||
TopBarWithSearch(
|
||||
onBackClick = params.onBackClicked,
|
||||
onSearchClick = state.onSearchClicked,
|
||||
marketsSearchBar = state.marketsSearchBar,
|
||||
bottomSheetState = bsState,
|
||||
)
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
||||
if (LocalRedesignEnabled.current) {
|
||||
FeedSearchBar(
|
||||
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
feedListSearchBar = state.feedListSearchBar,
|
||||
modifier = Modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.hazeEffectTangem {
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = .55f,
|
||||
endIntensity = 0f,
|
||||
preferPerformance = true,
|
||||
easing = EaseOut,
|
||||
)
|
||||
},
|
||||
startContent = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.neutral.primary,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x11)
|
||||
.background(
|
||||
color = TangemTheme.colors2.button.backgroundSecondary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickableSingle(
|
||||
onClick = clickIntents.onBackClicked,
|
||||
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
)
|
||||
.padding(TangemTheme.dimens2.x2),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
TopBarWithSearch(
|
||||
onBackClick = clickIntents.onBackClicked,
|
||||
onSearchClick = state.onSearchClicked,
|
||||
marketsSearchBar = state.marketsSearchBar,
|
||||
bottomSheetState = bsState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
override fun Content(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
model.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
|
|
@ -55,6 +120,7 @@ internal class DefaultMarketsTokenListComponent(
|
|||
}
|
||||
|
||||
MarketsList(
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
|
|
@ -62,9 +128,18 @@ internal class DefaultMarketsTokenListComponent(
|
|||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val onBackClicked: () -> Unit,
|
||||
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
|
||||
val preselectedSortType: SortByTypeUM,
|
||||
val shouldAlwaysShowSearchBar: Boolean,
|
||||
)
|
||||
|
||||
data class ClickIntents(
|
||||
val onBackClicked: () -> Unit,
|
||||
val onSearchClicked: () -> Unit,
|
||||
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
|
||||
)
|
||||
|
||||
data class ModelParams(
|
||||
val params: Params,
|
||||
val clickIntents: ClickIntents,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.feed.components.news.details
|
||||
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
|
@ -18,11 +20,11 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -32,6 +34,7 @@ import com.tangem.domain.news.model.NewsListConfig
|
|||
import com.tangem.features.feed.model.news.details.NewsDetailsModel
|
||||
import com.tangem.features.feed.ui.news.details.NewsDetailsContent
|
||||
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal class DefaultNewsDetailsComponent(
|
||||
|
|
@ -47,7 +50,14 @@ internal class DefaultNewsDetailsComponent(
|
|||
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TangemTopBar(
|
||||
title = resourceReference(R.string.common_news),
|
||||
modifier = Modifier.hazeEffectTangem {
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = .55f,
|
||||
endIntensity = 0f,
|
||||
preferPerformance = true,
|
||||
easing = EaseOut,
|
||||
)
|
||||
},
|
||||
type = TangemTopBarType.BottomSheet,
|
||||
startContent = {
|
||||
Icon(
|
||||
|
|
@ -90,7 +100,6 @@ internal class DefaultNewsDetailsComponent(
|
|||
} else {
|
||||
TangemTopAppBar(
|
||||
containerColor = background,
|
||||
title = null,
|
||||
startButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onClicked = state.onBackClick,
|
||||
|
|
@ -107,11 +116,16 @@ internal class DefaultNewsDetailsComponent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
override fun Content(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
|
||||
NewsDetailsContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.feed.components.news.list
|
||||
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
|
@ -18,6 +20,7 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
|
|
@ -30,6 +33,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import com.tangem.features.feed.model.news.list.NewsListModel
|
||||
import com.tangem.features.feed.ui.news.list.NewsListContent
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal class DefaultNewsListComponent(
|
||||
|
|
@ -45,6 +49,14 @@ internal class DefaultNewsListComponent(
|
|||
val state by newsListModel.state.collectAsStateWithLifecycle()
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TangemTopBar(
|
||||
modifier = Modifier.hazeEffectTangem {
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = .55f,
|
||||
endIntensity = 0f,
|
||||
preferPerformance = true,
|
||||
easing = EaseOut,
|
||||
)
|
||||
},
|
||||
title = resourceReference(R.string.common_news),
|
||||
type = TangemTopBarType.BottomSheet,
|
||||
startContent = {
|
||||
|
|
@ -80,11 +92,16 @@ internal class DefaultNewsListComponent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
override fun Content(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val state by newsListModel.state.collectAsStateWithLifecycle()
|
||||
NewsListContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.features.feed.components.search
|
||||
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.ds.field.search.TangemFieldShape
|
||||
import com.tangem.core.ui.ds.field.search.TangemSearchField
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
import com.tangem.features.feed.model.search.SearchModel
|
||||
import com.tangem.features.feed.ui.search.SearchContent
|
||||
import com.tangem.features.feed.ui.search.state.SearchCallbacks
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
|
||||
internal class DefaultSearchComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model = getOrCreateModel<SearchModel, Params>(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(bottomSheetState.value) {
|
||||
if (bottomSheetState.value == BottomSheetState.EXPANDED) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
TangemTopBar(
|
||||
modifier = Modifier.hazeEffectTangem {
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = .55f,
|
||||
endIntensity = 0f,
|
||||
preferPerformance = true,
|
||||
easing = EaseOut,
|
||||
)
|
||||
},
|
||||
type = TangemTopBarType.BottomSheet,
|
||||
reserveSlotSpace = false,
|
||||
content = {
|
||||
TangemSearchField(
|
||||
state = state.searchBar,
|
||||
shape = TangemFieldShape.Circle,
|
||||
focusRequester = focusRequester,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val searchCallbacks = remember {
|
||||
SearchCallbacks(
|
||||
onLoadMore = model::loadMore,
|
||||
onClearHintsClick = model::clearSearchHistory,
|
||||
onTextHintClick = model::onTextHintClick,
|
||||
onResultMarketTokenClick = model::onResultMarketTokenClick,
|
||||
)
|
||||
}
|
||||
SearchContent(
|
||||
modifier = modifier,
|
||||
content = state.content,
|
||||
searchCallbacks = searchCallbacks,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val onBackClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
|
|
@ -44,7 +44,7 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc
|
|||
tokenId = rawTokenId,
|
||||
tokenSymbol = "", // used for analytics
|
||||
).getOrElse {
|
||||
Timber.e("Failed to get market token info")
|
||||
TangemLogger.e("Failed to get market token info")
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
|
|
@ -25,7 +25,7 @@ internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor(
|
|||
scope.launch {
|
||||
val articleId = extractArticleIdFromUri(deeplinkUri)
|
||||
if (articleId == null) {
|
||||
Timber.e(
|
||||
TangemLogger.e(
|
||||
"""
|
||||
Failed to extract article ID from deep link
|
||||
|- Received URI: $deeplinkUri
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
|
|||
import com.tangem.features.feed.model.market.list.MarketsListModel
|
||||
import com.tangem.features.feed.model.news.details.NewsDetailsModel
|
||||
import com.tangem.features.feed.model.news.list.NewsListModel
|
||||
import com.tangem.features.feed.model.search.SearchModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -65,4 +66,9 @@ internal interface ModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(EarnTypeFilterModel::class)
|
||||
fun provideEarnTypeFilterModel(model: EarnTypeFilterModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SearchModel::class)
|
||||
fun provideSearchModel(model: SearchModel): Model
|
||||
}
|
||||
|
|
@ -35,10 +35,11 @@ internal class EarnTokenWithCurrencyToListItemUMConverter(
|
|||
formatArgs = wrappedList(convertPercent(value.earnToken.apy)),
|
||||
)
|
||||
},
|
||||
earnType = when (value.earnToken.type) {
|
||||
earnTypeTitle = when (value.earnToken.type) {
|
||||
EarnType.STAKING -> TextReference.Res(R.string.common_staking)
|
||||
EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode)
|
||||
},
|
||||
earnType = value.earnToken.type,
|
||||
onItemClick = { onItemClick(value) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
|
|||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
|
|
@ -110,7 +111,12 @@ internal class MarketsTokenItemConverter(
|
|||
|
||||
return MarketsListItemUM.Price(
|
||||
text = priceText,
|
||||
annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated(
|
||||
appCurrencyCode = appCurrency.code,
|
||||
appCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
changeType = changeType,
|
||||
fiatPrice = tokenQuotesShort.currentPrice,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ internal class EarnModel @Inject constructor(
|
|||
onNetworkFilterClick = ::onNetworkFilterClick,
|
||||
onTypeFilterClick = ::onTypeFilterClick,
|
||||
onScroll = ::onMostlyUsedScrolled,
|
||||
onSearchBarClicked = params.onSearchClicked,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.features.feed.model.earn.state
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.feed.model.earn.state.transformers.EarnUMTransformer
|
||||
import com.tangem.features.feed.ui.earn.state.*
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -34,6 +36,10 @@ internal class EarnStateController @Inject constructor() {
|
|||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
onSliderScroll = {},
|
||||
feedListSearchBar = FeedListSearchBar(
|
||||
placeholderText = TextReference.EMPTY,
|
||||
onBarClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
|
||||
internal class UpdateEarnUMInitialStateTransformer(
|
||||
private val onBackClick: () -> Unit,
|
||||
private val onNetworkFilterClick: () -> Unit,
|
||||
private val onTypeFilterClick: () -> Unit,
|
||||
private val onScroll: () -> Unit,
|
||||
private val onSearchBarClicked: () -> Unit,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
|
|
@ -15,6 +19,10 @@ internal class UpdateEarnUMInitialStateTransformer(
|
|||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
onSliderScroll = onScroll,
|
||||
feedListSearchBar = FeedListSearchBar(
|
||||
onBarClick = onSearchBarClicked,
|
||||
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
|
|
@ -66,6 +67,7 @@ internal class FeedComponentModel @Inject constructor(
|
|||
private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase,
|
||||
private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase,
|
||||
private val appRouter: AppRouter,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
|
|
@ -232,10 +234,20 @@ internal class FeedComponentModel @Inject constructor(
|
|||
return FeedListUM(
|
||||
currentDate = getCurrentDate(),
|
||||
feedListSearchBar = FeedListSearchBar(
|
||||
placeholderText = resourceReference(R.string.markets_search_header_title),
|
||||
placeholderText = resourceReference(
|
||||
id = if (designFeatureToggles.isRedesignEnabled) {
|
||||
R.string.markets_search_title_placeholder
|
||||
} else {
|
||||
R.string.markets_search_header_title
|
||||
},
|
||||
),
|
||||
onBarClick = {
|
||||
analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked())
|
||||
params.feedClickIntents.onMarketOpenClick(null)
|
||||
if (designFeatureToggles.isRedesignEnabled) {
|
||||
params.feedClickIntents.openSearch()
|
||||
} else {
|
||||
params.feedClickIntents.onMarketOpenClick(null)
|
||||
}
|
||||
},
|
||||
),
|
||||
feedListCallbacks = FeedListCallbacks(
|
||||
|
|
|
|||
|
|
@ -29,4 +29,6 @@ internal interface FeedModelClickIntents {
|
|||
fun onOpenAllNews()
|
||||
|
||||
fun onOpenEarnPage()
|
||||
|
||||
fun openSearch()
|
||||
}
|
||||
|
|
@ -188,6 +188,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
|
||||
marketChartLook.copy(
|
||||
type = percentChangeType.toChartType(),
|
||||
isMinMaxLook = designFeatureToggles.isRedesignEnabled,
|
||||
xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24),
|
||||
yAxisFormatter = { value ->
|
||||
value.format {
|
||||
|
|
@ -229,6 +230,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
).price()
|
||||
},
|
||||
priceAnnotated = params.token.tokenQuotes.currentPrice.toMarketsTokenDetailsPriceAnnotated(
|
||||
currentAppCurrency.value,
|
||||
),
|
||||
dateTimeText = resourceReference(R.string.common_today),
|
||||
priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() },
|
||||
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
|
||||
|
|
@ -504,6 +508,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
).price()
|
||||
},
|
||||
priceAnnotated = newInfo.quotes.currentPrice.toMarketsTokenDetailsPriceAnnotated(
|
||||
currentAppCurrency.value,
|
||||
),
|
||||
priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval(
|
||||
interval = marketsTokenDetailsUM.selectedInterval,
|
||||
),
|
||||
|
|
@ -603,7 +610,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
)
|
||||
} ?: getDefaultDateTimeString(currentState.selectedInterval)
|
||||
|
||||
val priceText = (price ?: currentQuotes.value.currentPrice).format {
|
||||
val amountForPrice = price ?: currentQuotes.value.currentPrice
|
||||
val priceText = amountForPrice.format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
|
|
@ -624,6 +632,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
isMarkerSet = markerTimestamp != null,
|
||||
dateTimeText = dateTimeText,
|
||||
priceText = priceText,
|
||||
priceAnnotated = amountForPrice.toMarketsTokenDetailsPriceAnnotated(currentAppCurrency.value),
|
||||
priceChangePercentText = percentText,
|
||||
priceChangeType = percent.percentChangeType(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package com.tangem.features.feed.model.market.details.converter
|
||||
|
||||
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.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
|
|
@ -37,7 +35,7 @@ internal class MetricsConverter(
|
|||
metrics = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
value = marketCap.formatAmount(),
|
||||
value = marketCap.formatAmount() ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
|
|
@ -65,7 +63,7 @@ internal class MetricsConverter(
|
|||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
value = volume24h.formatAmount(),
|
||||
value = volume24h.formatAmount() ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
|
|
@ -79,7 +77,7 @@ internal class MetricsConverter(
|
|||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
value = fullyDilutedValuation.formatAmount(),
|
||||
value = fullyDilutedValuation.formatAmount() ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
|
|
@ -95,7 +93,7 @@ internal class MetricsConverter(
|
|||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
value = circulatingSupply.formatAmount(crypto = true),
|
||||
value = circulatingSupply.formatAmount(crypto = true) ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
|
|
@ -109,7 +107,7 @@ internal class MetricsConverter(
|
|||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_max_supply),
|
||||
value = maxSupply.formatMaxSupply(),
|
||||
value = maxSupply.formatMaxSupply() ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
|
|
@ -135,7 +133,7 @@ internal class MetricsConverter(
|
|||
val liquidity = getLiquidity(value.volume24h, value.marketCap)
|
||||
persistentListOf(
|
||||
InfoPointUMV2.MarketCap(
|
||||
capitalizationValue = stringReference(marketCap.formatAmount()),
|
||||
capitalizationValue = marketCap.formatAmount()?.let(::stringReference),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
|
|
@ -150,7 +148,7 @@ internal class MetricsConverter(
|
|||
},
|
||||
),
|
||||
InfoPointUMV2.TradingVolume(
|
||||
tradingValue = stringReference(volume24h.formatAmount()),
|
||||
tradingValue = volume24h.formatAmount()?.let(::stringReference),
|
||||
liquidity = liquidity,
|
||||
trendingVolumeLiquidityType = getTrendingVolumeLiquidityType(liquidity),
|
||||
onInfoClick = {
|
||||
|
|
@ -165,7 +163,9 @@ internal class MetricsConverter(
|
|||
},
|
||||
),
|
||||
InfoPointUMV2.MarketPosition(
|
||||
position = marketRating?.toString() ?: StringsSigns.DASH_SIGN,
|
||||
position = marketRating?.let {
|
||||
stringReference(it.toString())
|
||||
},
|
||||
rangeValue = getMarketRatingRangeValue(marketRating),
|
||||
marketRatingType = getMarketRatingType(marketRating),
|
||||
onInfoClick = {
|
||||
|
|
@ -176,12 +176,16 @@ internal class MetricsConverter(
|
|||
),
|
||||
)
|
||||
},
|
||||
marketRatingChange24H = getMarketRatingChange(marketRatingChange24h),
|
||||
),
|
||||
InfoPointUMV2.FullyDilutedValuation(
|
||||
value = resourceReference(
|
||||
R.string.markets_token_details_valuation_value_in_total,
|
||||
wrappedList(fullyDilutedValuation.formatAmount()),
|
||||
),
|
||||
value = fullyDilutedValuation?.formatAmount()?.let { formatted ->
|
||||
resourceReference(
|
||||
id = R.string.markets_token_details_valuation_value_in_total,
|
||||
formatArgs = wrappedList(formatted),
|
||||
)
|
||||
},
|
||||
fullyDilutedValuationChange24 = fullyDilutedValuationChange24?.convertChange(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
|
|
@ -196,20 +200,29 @@ internal class MetricsConverter(
|
|||
},
|
||||
),
|
||||
InfoPointUMV2.CirculatingSupply(
|
||||
currentValue = stringReference(circulatingSupply.formatAmount(crypto = true)),
|
||||
maxValue = maxSupply?.let { supply ->
|
||||
if (supply > BigDecimal.ZERO) {
|
||||
stringReference(supply.formatMaxSupply())
|
||||
} else {
|
||||
null
|
||||
currentValue = circulatingSupply?.formatAmount(true)?.let(::stringReference),
|
||||
maxValue = when (maxSupply) {
|
||||
null -> null
|
||||
BigDecimal.ZERO -> {
|
||||
resourceReference(
|
||||
R.string
|
||||
.markets_token_details_metrics_no_limited,
|
||||
)
|
||||
}
|
||||
else -> maxSupply.formatAmount(true)?.let(::stringReference)
|
||||
},
|
||||
fillValue = getCirculatingSupplyFillValue(circulatingSupply, maxSupply),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_max_supply_full),
|
||||
body = resourceReference(R.string.markets_token_details_total_supply_description),
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_max_supply_and_circulation_full,
|
||||
),
|
||||
body = combinedReference(
|
||||
resourceReference(R.string.markets_token_details_circulating_supply_description),
|
||||
stringReference("\n\n"),
|
||||
resourceReference(R.string.markets_token_details_total_supply_description),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -234,7 +247,7 @@ internal class MetricsConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatMaxSupply(): String {
|
||||
private fun BigDecimal?.formatMaxSupply(): String? {
|
||||
when (this) {
|
||||
null -> return StringsSigns.DASH_SIGN
|
||||
BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN
|
||||
|
|
@ -243,8 +256,8 @@ internal class MetricsConverter(
|
|||
return this.formatAmount(crypto = true)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
|
||||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String? {
|
||||
if (this == null) return null
|
||||
|
||||
return if (crypto) {
|
||||
format {
|
||||
|
|
@ -265,10 +278,20 @@ internal class MetricsConverter(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float {
|
||||
if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return 0f
|
||||
private fun BigDecimal?.convertChange(): TextReference? {
|
||||
if (this == null) return null
|
||||
val amount = this.abs().formatAmount()
|
||||
val value = when {
|
||||
this > BigDecimal.ZERO -> StringsSigns.PLUS + amount
|
||||
this < BigDecimal.ZERO -> StringsSigns.MINUS + amount
|
||||
else -> amount
|
||||
}
|
||||
return value?.let(::stringReference)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float? {
|
||||
if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return null
|
||||
val ratio = volume24h
|
||||
.divide(marketCap, 6, RoundingMode.HALF_UP)
|
||||
.toFloat()
|
||||
|
|
@ -277,8 +300,9 @@ internal class MetricsConverter(
|
|||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getTrendingVolumeLiquidityType(liquidity: Float): TrendingVolumeLiquidityType {
|
||||
private fun getTrendingVolumeLiquidityType(liquidity: Float?): TrendingVolumeLiquidityType {
|
||||
return when {
|
||||
liquidity == null -> TrendingVolumeLiquidityType.UNKNOWN
|
||||
liquidity >= 0.5f -> TrendingVolumeLiquidityType.HIGH
|
||||
liquidity in 0.2f..<0.5f -> TrendingVolumeLiquidityType.MEDIUM
|
||||
else -> TrendingVolumeLiquidityType.LOW
|
||||
|
|
@ -296,8 +320,8 @@ internal class MetricsConverter(
|
|||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getMarketRatingRangeValue(marketRating: Int?): Float {
|
||||
if (marketRating == null) return 0f
|
||||
private fun getMarketRatingRangeValue(marketRating: Int?): Float? {
|
||||
if (marketRating == null) return null
|
||||
|
||||
return when {
|
||||
marketRating <= 20 -> {
|
||||
|
|
@ -352,4 +376,12 @@ internal class MetricsConverter(
|
|||
|
||||
return ratio.coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
private fun getMarketRatingChange(change: Int?): MarketRatingChange24H {
|
||||
return when {
|
||||
change == null || change == 0 -> MarketRatingChange24H.NoChanges
|
||||
change < 0 -> MarketRatingChange24H.Down(-change)
|
||||
else -> MarketRatingChange24H.Up(change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.features.feed.model.market.details.formatter
|
||||
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
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.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.format.bigdecimal.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -73,4 +75,14 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
|
|||
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
|
||||
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BigDecimal.toMarketsTokenDetailsPriceAnnotated(appCurrency: AppCurrency): TextReference {
|
||||
return formatStyled {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.tertiary) },
|
||||
).price()
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ internal class QuotesStateUpdater(
|
|||
fiatCurrencyCode = currentAppCurrency().code,
|
||||
).price()
|
||||
},
|
||||
priceAnnotated = newQuotes.currentPrice.toMarketsTokenDetailsPriceAnnotated(currentAppCurrency()),
|
||||
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
|
||||
interval = stateToUpdate.selectedInterval,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
|
||||
private val updateQuotesJob = JobHolder()
|
||||
|
||||
private val params = paramsContainer.require<DefaultMarketsTokenListComponent.Params>()
|
||||
private val modelParams = paramsContainer.require<DefaultMarketsTokenListComponent.ModelParams>()
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
|
|
@ -65,10 +65,11 @@ internal class MarketsListModel @Inject constructor(
|
|||
onRetryButtonClicked = { activeListManager.reload() },
|
||||
onTokenClick = { onTokenUIClicked(it) },
|
||||
onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) },
|
||||
shouldAlwaysShowSearchBar = Provider { params.shouldAlwaysShowSearchBar },
|
||||
preselectedSortType = Provider { params.preselectedSortType },
|
||||
onBackClick = params.onBackClicked,
|
||||
shouldAlwaysShowSearchBar = Provider { modelParams.params.shouldAlwaysShowSearchBar },
|
||||
preselectedSortType = Provider { modelParams.params.preselectedSortType },
|
||||
onBackClick = modelParams.clickIntents.onBackClicked,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onSearchBarClick = modelParams.clickIntents.onSearchClicked,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +262,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
private fun onTokenUIClicked(token: MarketsListItemUM) {
|
||||
modelScope.launch {
|
||||
activeListManager.getTokenById(token.id)?.let { found ->
|
||||
params.onTokenClick(found.toSerializableParam(), currentAppCurrency.value)
|
||||
modelParams.clickIntents.onTokenClick(found.toSerializableParam(), currentAppCurrency.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.core.ui.event.StateEvent
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class MarketsListUM(
|
||||
|
|
@ -20,6 +21,8 @@ internal data class MarketsListUM(
|
|||
val onIntervalClick: (TrendInterval) -> Unit,
|
||||
val onSortByButtonClick: () -> Unit,
|
||||
val onSearchClicked: () -> Unit,
|
||||
val feedListSearchBar: FeedListSearchBar,
|
||||
val sortByMenuUM: SortByMenuUM,
|
||||
) {
|
||||
val isInSearchMode
|
||||
get() = marketsSearchBar.searchBarUM.query.isNotEmpty()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.feed.model.market.list.state
|
||||
|
||||
internal data class SortByMenuUM(
|
||||
val selectedOption: SortByTypeUM,
|
||||
val onOptionClicked: (SortByTypeUM) -> Unit,
|
||||
)
|
||||
|
|
@ -75,6 +75,11 @@ internal class MarketsListBatchFlowManager(
|
|||
private val resultBatches = MutableStateFlow(ResultBatches())
|
||||
private val uiBatches = stateManager.state.map { it.uiBatches }
|
||||
|
||||
val rawItems: Flow<List<TokenMarket>>
|
||||
get() = batchFlow.state
|
||||
.map { state -> state.data.flatMap { batch -> batch.data } }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
|
||||
get() = uiBatches
|
||||
.map { batches ->
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.list.state.*
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -33,6 +34,7 @@ internal class MarketsListUMStateManager(
|
|||
private val onShowTokensUnder100kClicked: () -> Unit,
|
||||
private val onBackClick: () -> Unit,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onSearchBarClick: () -> Unit,
|
||||
) {
|
||||
|
||||
val state = MutableStateFlow(state())
|
||||
|
|
@ -64,6 +66,9 @@ internal class MarketsListUMStateManager(
|
|||
selectedOption = value,
|
||||
),
|
||||
),
|
||||
sortByMenuUM = marketsListUM.sortByMenuUM.copy(
|
||||
selectedOption = value,
|
||||
),
|
||||
list = if (marketsListUM.list is ListUM.Content && marketsListUM.selectedSortBy != value) {
|
||||
marketsListUM.list.copy(
|
||||
triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() },
|
||||
|
|
@ -232,16 +237,24 @@ internal class MarketsListUMStateManager(
|
|||
onDismissRequest = { isSortByBottomSheetShown = false },
|
||||
content = SortByBottomSheetContentUM(
|
||||
selectedOption = preselectedSortType(),
|
||||
onOptionClicked = ::onBottomSheetOptionClicked,
|
||||
onOptionClicked = ::onBottomSheetOrMenuOptionClicked,
|
||||
),
|
||||
),
|
||||
onSearchClicked = {
|
||||
analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked())
|
||||
changeSearchBarIsActive(true)
|
||||
},
|
||||
feedListSearchBar = FeedListSearchBar(
|
||||
onBarClick = onSearchBarClick,
|
||||
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
|
||||
),
|
||||
sortByMenuUM = SortByMenuUM(
|
||||
selectedOption = preselectedSortType(),
|
||||
onOptionClicked = ::onBottomSheetOrMenuOptionClicked,
|
||||
),
|
||||
)
|
||||
|
||||
private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) {
|
||||
private fun onBottomSheetOrMenuOptionClicked(sortByTypeUM: SortByTypeUM) {
|
||||
state.update { marketsListUM ->
|
||||
marketsListUM.copy(
|
||||
selectedSortBy = sortByTypeUM,
|
||||
|
|
@ -251,6 +264,7 @@ internal class MarketsListUMStateManager(
|
|||
selectedOption = sortByTypeUM,
|
||||
),
|
||||
),
|
||||
sortByMenuUM = marketsListUM.sortByMenuUM.copy(selectedOption = sortByTypeUM),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import com.tangem.domain.markets.TokenMarketUpdateRequest
|
|||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchUpdateResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
internal fun logStatus(tag: String, status: PaginationStatus<List<TokenMarket>>) {
|
||||
Timber.tag(tag).d(
|
||||
TangemLogger.withTag(tag).d(
|
||||
"""
|
||||
Status
|
||||
$status
|
||||
|
|
@ -19,19 +19,19 @@ internal fun logStatus(tag: String, status: PaginationStatus<List<TokenMarket>>)
|
|||
|
||||
internal fun logAction(tag: String, action: BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>) {
|
||||
when (action) {
|
||||
is BatchAction.Reload -> Timber.tag(tag).d(
|
||||
is BatchAction.Reload -> TangemLogger.withTag(tag).d(
|
||||
"""
|
||||
Reload = ${action.requestParams}
|
||||
""".trimIndent(),
|
||||
)
|
||||
is BatchAction.UpdateBatches -> Timber.tag(tag).d(
|
||||
is BatchAction.UpdateBatches -> TangemLogger.withTag(tag).d(
|
||||
"""
|
||||
To update:
|
||||
keys: ${action.keys.toList()}
|
||||
updateType: ${action.updateRequest.javaClass.simpleName}
|
||||
""".trimIndent(),
|
||||
)
|
||||
else -> Timber.tag(tag).d(
|
||||
else -> TangemLogger.withTag(tag).d(
|
||||
"""
|
||||
$action
|
||||
""".trimIndent(),
|
||||
|
|
@ -48,7 +48,7 @@ internal fun logUpdateResults(
|
|||
is BatchUpdateResult.Error -> s.throwable.toString()
|
||||
}
|
||||
|
||||
Timber.tag(tag).d(
|
||||
TangemLogger.withTag(tag).d(
|
||||
"""
|
||||
updateResults
|
||||
request: ${updateResult.first}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ internal class NewsDetailsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
toggleArticleLikedUseCase
|
||||
.toggleLiked(articleId)
|
||||
.onLeft { Timber.e(it) }
|
||||
.onLeft { TangemLogger.e("Error", it) }
|
||||
analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsLikeClicked(articleId))
|
||||
}
|
||||
}
|
||||
|
|
@ -231,7 +231,7 @@ internal class NewsDetailsModel @Inject constructor(
|
|||
}
|
||||
// global request executing error
|
||||
newsId < 0 -> {
|
||||
Timber.e(error)
|
||||
TangemLogger.e("Error", error)
|
||||
}
|
||||
else -> {
|
||||
val (code, message) = when (error) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
|
|||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.*
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
|
|
@ -68,7 +69,12 @@ internal class RelatedTokenConverter(private val appCurrency: AppCurrency) :
|
|||
|
||||
return MarketsListItemUM.Price(
|
||||
text = priceText,
|
||||
annotated = tokenInfo.quotes.currentPrice.toMarketsListItemPriceAnnotated(
|
||||
appCurrencyCode = appCurrency.code,
|
||||
appCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
changeType = null,
|
||||
fiatPrice = tokenInfo.quotes.currentPrice,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,352 @@
|
|||
package com.tangem.features.feed.model.search
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
|
||||
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
|
||||
import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase
|
||||
import com.tangem.domain.search.usecase.SaveSearchQueryUseCase
|
||||
import com.tangem.features.feed.components.search.DefaultSearchComponent
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager
|
||||
import com.tangem.features.feed.model.search.converter.MarketsListItemUMToRecentSearchTokenConverter
|
||||
import com.tangem.features.feed.model.search.converter.MarketsListItemUMWithAppCurrency
|
||||
import com.tangem.features.feed.model.search.converter.RecentSearchTokenToMarketsListItemUMConverter
|
||||
import com.tangem.features.feed.model.search.converter.RecentSearchTokenWithAppCurrency
|
||||
import com.tangem.features.feed.model.search.state.SearchStateController
|
||||
import com.tangem.features.feed.model.search.state.transformers.*
|
||||
import com.tangem.features.feed.ui.search.state.*
|
||||
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.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
|
||||
private const val MARKET_SEARCH_DEBOUNCE_MS = 500L
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class SearchModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getSearchResultsUseCase: GetSearchResultsUseCase,
|
||||
private val saveSearchQueryUseCase: SaveSearchQueryUseCase,
|
||||
private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase,
|
||||
private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase,
|
||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
private val stateController: SearchStateController,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<DefaultSearchComponent.Params>()
|
||||
|
||||
private val updateQuotesJob = JobHolder()
|
||||
private val searchResultsJob = JobHolder()
|
||||
private val marketSearchDebounceJob = JobHolder()
|
||||
private var shouldShowAllTokensIncludingUnder100k = false
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val marketsListItemToRecentSearchTokenConverter by lazy {
|
||||
MarketsListItemUMToRecentSearchTokenConverter()
|
||||
}
|
||||
|
||||
private val recentSearchTokenToMarketsListItemConverter by lazy {
|
||||
RecentSearchTokenToMarketsListItemUMConverter()
|
||||
}
|
||||
|
||||
private val priceAndTimePointValuesConverter by lazy {
|
||||
PriceAndTimePointValuesConverter(shouldFormatAxis = false)
|
||||
}
|
||||
|
||||
private val searchMarketsListManager by lazy {
|
||||
MarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
currentTrendInterval = Provider { MarketsListUM.TrendInterval.H24 },
|
||||
currentSortByType = Provider { SortByTypeUM.Rating },
|
||||
currentSearchText = Provider { stateController.value.searchBar.query },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
val state: StateFlow<SearchUM> get() = stateController.uiState
|
||||
|
||||
init {
|
||||
initCallbacks()
|
||||
subscribeToQueryChanges()
|
||||
subscribeToMarketUiItems()
|
||||
subscribeToQuotesPolling()
|
||||
subscribeToAppCurrencyChanges()
|
||||
loadHistory()
|
||||
}
|
||||
|
||||
fun loadMore() {
|
||||
val content = stateController.value.content
|
||||
if (content is SearchContentUM.Results) {
|
||||
val market = content.marketTokens
|
||||
if (market is MarketSearchResultUM.Content) {
|
||||
searchMarketsListManager.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSearchHistory() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
clearSearchHistoryUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
fun onTextHintClick(text: String) {
|
||||
stateController.update(UpdateSearchBarQueryTransformer(text))
|
||||
}
|
||||
|
||||
fun onResultMarketTokenClick(item: MarketsListItemUM) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
val appCurrency = currentAppCurrency.value
|
||||
val input = MarketsListItemUMWithAppCurrency(
|
||||
item = item,
|
||||
appCurrencyCode = appCurrency.code,
|
||||
appCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
saveRecentSearchTokenUseCase(marketsListItemToRecentSearchTokenConverter.convert(input))
|
||||
saveSearchQueryUseCase(stateController.value.searchBar.query)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initCallbacks() {
|
||||
stateController.update(object : SearchUMTransformer {
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
return prevState.copy(
|
||||
searchBar = prevState.searchBar.copy(
|
||||
onQueryChange = ::onQueryChange,
|
||||
onActiveChange = ::onActiveChange,
|
||||
onClearClick = ::onClearClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onQueryChange(query: String) {
|
||||
stateController.update(UpdateSearchBarQueryTransformer(query))
|
||||
}
|
||||
|
||||
private fun onActiveChange(isActive: Boolean) {
|
||||
if (!isActive) params.onBackClick()
|
||||
}
|
||||
|
||||
private fun onClearClick() {
|
||||
stateController.update(UpdateSearchBarQueryTransformer(""))
|
||||
}
|
||||
|
||||
private fun subscribeToQueryChanges() {
|
||||
stateController.uiState
|
||||
.map { it.searchBar.query.trim() }
|
||||
.distinctUntilChanged()
|
||||
.onEach { query ->
|
||||
shouldShowAllTokensIncludingUnder100k = false
|
||||
if (query.isEmpty()) {
|
||||
marketSearchDebounceJob.cancel()
|
||||
searchMarketsListManager.clearStateAndStopAllActions()
|
||||
updateQuotesJob.cancel()
|
||||
loadHistory()
|
||||
} else {
|
||||
stateController.update(SetSearchResultsLoadingTransformer())
|
||||
subscribeToSearchResults(query)
|
||||
modelScope.launch(dispatchers.default) {
|
||||
delay(MARKET_SEARCH_DEBOUNCE_MS)
|
||||
val latestQuery = stateController.value.searchBar.query.trim()
|
||||
if (latestQuery.isNotEmpty()) {
|
||||
searchMarketsListManager.reload(searchText = latestQuery)
|
||||
}
|
||||
}.saveIn(marketSearchDebounceJob)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToSearchResults(query: String) {
|
||||
modelScope.launch {
|
||||
getSearchResultsUseCase(query = query).collectLatest { searchResult ->
|
||||
val userAssets = searchResult.userAssets.map { entry ->
|
||||
UserAssetItemUM(
|
||||
id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" +
|
||||
"_${entry.currencyStatus.currency.id.value}",
|
||||
tokenIconUrl = entry.currencyStatus.currency.iconUrl,
|
||||
tokenName = entry.currencyStatus.currency.name,
|
||||
tokenSymbol = entry.currencyStatus.currency.symbol,
|
||||
accountName = entry.accountName.toDisplayString(),
|
||||
onClick = {
|
||||
// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task.
|
||||
},
|
||||
)
|
||||
}.toImmutableList()
|
||||
stateController.update(UpdateUserAssetsTransformer(userAssets))
|
||||
}
|
||||
}.saveIn(searchResultsJob)
|
||||
}
|
||||
|
||||
private fun subscribeToQuotesPolling() {
|
||||
searchMarketsListManager.onLastBatchLoadedSuccess.onEach { batchKey ->
|
||||
searchMarketsListManager.loadCharts(setOf(batchKey), MarketsListUM.TrendInterval.H24)
|
||||
modelScope.loadQuotesWithTimer(UPDATE_QUOTES_TIMER_MILLIS)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToAppCurrencyChanges() {
|
||||
currentAppCurrency.drop(1).onEach {
|
||||
val query = stateController.value.searchBar.query
|
||||
if (query.isNotEmpty()) {
|
||||
searchMarketsListManager.reload()
|
||||
}
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToMarketUiItems() {
|
||||
combine(
|
||||
flow = stateController.uiState.map { it.searchBar.query }.distinctUntilChanged(),
|
||||
flow2 = searchMarketsListManager.uiItems,
|
||||
flow3 = searchMarketsListManager.isSearchNotFoundState,
|
||||
flow4 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
) { query, uiItems, isSearchNotFound, isInErrorState ->
|
||||
if (query.isEmpty()) return@combine null
|
||||
val marketResult = when {
|
||||
isSearchNotFound -> MarketSearchResultUM.NotFound
|
||||
isInErrorState -> MarketSearchResultUM.NotFound
|
||||
uiItems.isEmpty() -> MarketSearchResultUM.Empty
|
||||
else -> buildMarketContent(uiItems)
|
||||
}
|
||||
SearchMarketBatchUiSnapshot(
|
||||
marketResult = marketResult,
|
||||
isSearchNotFound = isSearchNotFound,
|
||||
isInErrorState = isInErrorState,
|
||||
)
|
||||
}
|
||||
.filterNotNull()
|
||||
.onEach { snapshot ->
|
||||
stateController.update(ApplySearchMarketBatchTransformer(snapshot))
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun buildMarketContent(allItems: ImmutableList<MarketsListItemUM>): MarketSearchResultUM.Content {
|
||||
if (shouldShowAllTokensIncludingUnder100k) {
|
||||
return MarketSearchResultUM.Content(items = allItems)
|
||||
}
|
||||
|
||||
val filtered = allItems.filter { !it.isUnder100kMarketCap }.toImmutableList()
|
||||
val hasUnder100k = filtered.size != allItems.size
|
||||
|
||||
return if (hasUnder100k) {
|
||||
MarketSearchResultUM.Content(
|
||||
items = filtered,
|
||||
shouldShowUnder100kNotification = true,
|
||||
onShowUnder100kClick = {
|
||||
shouldShowAllTokensIncludingUnder100k = true
|
||||
stateController.update(
|
||||
UpdateMarketItemsTransformer(
|
||||
MarketSearchResultUM.Content(items = allItems),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
MarketSearchResultUM.Content(items = allItems)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadHistory() {
|
||||
modelScope.launch {
|
||||
getSearchResultsUseCase(query = "").collectLatest { searchResult ->
|
||||
val textHints = searchResult.textHints.map { hint ->
|
||||
TextHintItemUM(text = hint.text)
|
||||
}.toImmutableList()
|
||||
|
||||
val appCurrency = currentAppCurrency.value
|
||||
val recentTokens = searchResult.recentTokens.map { token ->
|
||||
recentSearchTokenToMarketsListItemConverter.convert(
|
||||
RecentSearchTokenWithAppCurrency(token = token, appCurrency = appCurrency),
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
stateController.update(UpdateHistoryTransformer(textHints, recentTokens))
|
||||
|
||||
loadRecentTokenCharts(recentTokens, appCurrency)
|
||||
}
|
||||
}.saveIn(searchResultsJob)
|
||||
}
|
||||
|
||||
private suspend fun loadRecentTokenCharts(tokens: ImmutableList<MarketsListItemUM>, appCurrency: AppCurrency) {
|
||||
coroutineScope {
|
||||
tokens.map { token ->
|
||||
async(dispatchers.io) {
|
||||
val chart = getTokenPriceChartUseCase(
|
||||
appCurrency = appCurrency,
|
||||
interval = PriceChangeInterval.H24,
|
||||
tokenId = token.id,
|
||||
tokenSymbol = token.currencySymbol,
|
||||
preview = true,
|
||||
).getOrElse { return@async }
|
||||
|
||||
val chartData = priceAndTimePointValuesConverter.convert(
|
||||
MarketChartData.Data(
|
||||
y = chart.priceY.toImmutableList(),
|
||||
x = chart.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted(),
|
||||
)
|
||||
|
||||
stateController.update(UpdateRecentTokenChartTransformer(token.id, chartData))
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun AccountName.toDisplayString(): String {
|
||||
return when (this) {
|
||||
is AccountName.DefaultMain -> "Main" // TODO [REDACTED_TASK_KEY] localize
|
||||
is AccountName.Custom -> value
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
|
||||
launch {
|
||||
while (true) {
|
||||
delay(timeMillis)
|
||||
searchMarketsListManager.updateQuotes()
|
||||
}
|
||||
}.saveIn(updateQuotesJob)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.feed.model.search.converter
|
||||
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.search.model.RecentSearchToken
|
||||
import com.tangem.domain.search.model.TokenPriceChangeDirection
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class MarketsListItemUMToRecentSearchTokenConverter :
|
||||
Converter<MarketsListItemUMWithAppCurrency, RecentSearchToken> {
|
||||
|
||||
override fun convert(value: MarketsListItemUMWithAppCurrency): RecentSearchToken {
|
||||
val item = value.item
|
||||
val direction = when (item.trendType) {
|
||||
PriceChangeType.UP -> TokenPriceChangeDirection.UP
|
||||
PriceChangeType.DOWN -> TokenPriceChangeDirection.DOWN
|
||||
PriceChangeType.NEUTRAL -> TokenPriceChangeDirection.NEUTRAL
|
||||
}
|
||||
val stakingText = when (val rate = item.stakingRate) {
|
||||
is TextReference.Str -> rate.value
|
||||
is TextReference.StyledStr -> rate.value
|
||||
else -> null
|
||||
}
|
||||
return RecentSearchToken(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
symbol = item.currencySymbol,
|
||||
imageUrl = item.iconUrl,
|
||||
timestamp = item.updateTimestamp ?: System.currentTimeMillis(),
|
||||
appCurrencyCode = value.appCurrencyCode,
|
||||
appCurrencySymbol = value.appCurrencySymbol,
|
||||
price = item.price.fiatPrice,
|
||||
priceChangePercent = item.trendPercentText,
|
||||
priceChangeDirection = direction,
|
||||
marketCap = item.marketCap,
|
||||
ratingPosition = item.ratingPosition,
|
||||
isUnder100kMarketCap = item.isUnder100kMarketCap,
|
||||
stakingRateText = stakingText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.feed.model.search.converter
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
|
||||
internal data class MarketsListItemUMWithAppCurrency(
|
||||
val item: MarketsListItemUM,
|
||||
val appCurrencyCode: String,
|
||||
val appCurrencySymbol: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.feed.model.search.converter
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.search.model.TokenPriceChangeDirection
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class RecentSearchTokenToMarketsListItemUMConverter :
|
||||
Converter<RecentSearchTokenWithAppCurrency, MarketsListItemUM> {
|
||||
|
||||
override fun convert(value: RecentSearchTokenWithAppCurrency): MarketsListItemUM {
|
||||
val token = value.token
|
||||
val currencyCode = token.appCurrencyCode.ifEmpty { value.appCurrency.code }
|
||||
val currencySymbol = token.appCurrencySymbol.ifEmpty { value.appCurrency.symbol }
|
||||
val trendType = when (token.priceChangeDirection) {
|
||||
TokenPriceChangeDirection.UP -> PriceChangeType.UP
|
||||
TokenPriceChangeDirection.DOWN -> PriceChangeType.DOWN
|
||||
TokenPriceChangeDirection.NEUTRAL -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
val priceText = token.price.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currencyCode,
|
||||
fiatCurrencySymbol = currencySymbol,
|
||||
).price()
|
||||
}
|
||||
return MarketsListItemUM(
|
||||
id = token.id,
|
||||
name = token.name,
|
||||
currencySymbol = token.symbol,
|
||||
iconUrl = token.imageUrl,
|
||||
ratingPosition = token.ratingPosition,
|
||||
marketCap = token.marketCap,
|
||||
price = MarketsListItemUM.Price(
|
||||
text = priceText,
|
||||
annotated = token.price.toMarketsListItemPriceAnnotated(
|
||||
appCurrencyCode = currencyCode,
|
||||
appCurrencySymbol = currencySymbol,
|
||||
),
|
||||
changeType = null,
|
||||
fiatPrice = token.price,
|
||||
),
|
||||
trendPercentText = token.priceChangePercent,
|
||||
trendType = trendType,
|
||||
chartData = null,
|
||||
isUnder100kMarketCap = token.isUnder100kMarketCap,
|
||||
stakingRate = token.stakingRateText?.let { TextReference.Str(it) },
|
||||
updateTimestamp = token.timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.feed.model.search.converter
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.search.model.RecentSearchToken
|
||||
|
||||
internal data class RecentSearchTokenWithAppCurrency(
|
||||
val token: RecentSearchToken,
|
||||
val appCurrency: AppCurrency,
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.features.feed.model.search.state
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.model.search.state.transformers.SearchUMTransformer
|
||||
import com.tangem.features.feed.ui.search.state.SearchContentUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class SearchStateController @Inject constructor() {
|
||||
|
||||
private val mutableUiState: MutableStateFlow<SearchUM> = MutableStateFlow(value = getInitialState())
|
||||
|
||||
val uiState: StateFlow<SearchUM> = mutableUiState.asStateFlow()
|
||||
|
||||
val value: SearchUM
|
||||
get() = uiState.value
|
||||
|
||||
fun update(transformer: SearchUMTransformer) {
|
||||
mutableUiState.update(function = transformer::transform)
|
||||
}
|
||||
|
||||
private fun getInitialState(): SearchUM {
|
||||
return SearchUM(
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = {},
|
||||
onClearClick = {},
|
||||
),
|
||||
content = SearchContentUM.InitialEmpty,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchContentUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
|
||||
internal class ApplySearchMarketBatchTransformer(
|
||||
private val snapshot: SearchMarketBatchUiSnapshot,
|
||||
) : SearchUMTransformer {
|
||||
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
if (shouldIgnoreTransientMarketEmpty(prevState, snapshot)) return prevState
|
||||
return UpdateMarketItemsTransformer(snapshot.marketResult).transform(prevState)
|
||||
}
|
||||
|
||||
private fun shouldIgnoreTransientMarketEmpty(prevState: SearchUM, snapshot: SearchMarketBatchUiSnapshot): Boolean {
|
||||
if (snapshot.marketResult !is MarketSearchResultUM.Empty) return false
|
||||
if (snapshot.isSearchNotFound || snapshot.isInErrorState) return false
|
||||
val prev = prevState.content as? SearchContentUM.Results ?: return false
|
||||
return when (prev.marketTokens) {
|
||||
is MarketSearchResultUM.Content, is MarketSearchResultUM.Loading -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Inputs from the search market list flow before merging into [SearchUM]. */
|
||||
internal data class SearchMarketBatchUiSnapshot(
|
||||
val marketResult: MarketSearchResultUM,
|
||||
val isSearchNotFound: Boolean,
|
||||
val isInErrorState: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
|
||||
internal interface SearchUMTransformer {
|
||||
fun transform(prevState: SearchUM): SearchUM
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchContentUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SetSearchResultsLoadingTransformer : SearchUMTransformer {
|
||||
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
val currentContent = prevState.content
|
||||
val currentUserAssets = if (currentContent is SearchContentUM.Results) {
|
||||
currentContent.userAssets
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
// Keep showing the previous market list while the new query loads (stale-while-revalidate).
|
||||
// Replacing with Loading + empty batch flashes skeletons / empty section even when the API
|
||||
// returns the same tokens for a refined query.
|
||||
val nextMarketTokens = if (currentContent is SearchContentUM.Results) {
|
||||
when (val market = currentContent.marketTokens) {
|
||||
is MarketSearchResultUM.Content -> market
|
||||
else -> MarketSearchResultUM.Loading
|
||||
}
|
||||
} else {
|
||||
MarketSearchResultUM.Loading
|
||||
}
|
||||
return prevState.copy(
|
||||
content = SearchContentUM.Results(
|
||||
userAssets = currentUserAssets,
|
||||
marketTokens = nextMarketTokens,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchContentUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
import com.tangem.features.feed.ui.search.state.TextHintItemUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal class UpdateHistoryTransformer(
|
||||
private val textHints: ImmutableList<TextHintItemUM>,
|
||||
private val recentTokens: ImmutableList<MarketsListItemUM>,
|
||||
) : SearchUMTransformer {
|
||||
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
return prevState.copy(
|
||||
content = SearchContentUM.History(
|
||||
textHints = textHints,
|
||||
recentTokens = recentTokens,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchContentUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class UpdateMarketItemsTransformer(
|
||||
private val marketResult: MarketSearchResultUM,
|
||||
) : SearchUMTransformer {
|
||||
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
val currentContent = prevState.content
|
||||
|
||||
val currentUserAssets = if (currentContent is SearchContentUM.Results) {
|
||||
currentContent.userAssets
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
return prevState.copy(
|
||||
content = SearchContentUM.Results(
|
||||
userAssets = currentUserAssets,
|
||||
marketTokens = marketResult,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.search.state.SearchContentUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class UpdateRecentTokenChartTransformer(
|
||||
private val tokenId: CryptoCurrency.RawID,
|
||||
private val chartData: MarketChartRawData,
|
||||
) : SearchUMTransformer {
|
||||
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
val content = prevState.content
|
||||
if (content !is SearchContentUM.History) return prevState
|
||||
|
||||
val updatedTokens = content.recentTokens.map { token ->
|
||||
if (token.id == tokenId) {
|
||||
token.copy(chartData = chartData)
|
||||
} else {
|
||||
token
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
return prevState.copy(
|
||||
content = content.copy(recentTokens = updatedTokens),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
|
||||
internal class UpdateSearchBarQueryTransformer(
|
||||
private val query: String,
|
||||
) : SearchUMTransformer {
|
||||
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
return prevState.copy(
|
||||
searchBar = prevState.searchBar.copy(query = query),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchContentUM
|
||||
import com.tangem.features.feed.ui.search.state.SearchUM
|
||||
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal class UpdateUserAssetsTransformer(
|
||||
private val userAssets: ImmutableList<UserAssetItemUM>,
|
||||
) : SearchUMTransformer {
|
||||
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
val currentContent = prevState.content
|
||||
val currentMarketTokens = if (currentContent is SearchContentUM.Results) {
|
||||
currentContent.marketTokens
|
||||
} else {
|
||||
MarketSearchResultUM.Loading
|
||||
}
|
||||
return prevState.copy(
|
||||
content = SearchContentUM.Results(
|
||||
userAssets = userAssets,
|
||||
marketTokens = currentMarketTokens,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +1,66 @@
|
|||
package com.tangem.features.feed.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.res.LocalHazeState
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.features.feed.components.FeedEntryChildFactory
|
||||
import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation
|
||||
import com.tangem.features.feed.ui.utils.topBarFeedEntryStackAnimation
|
||||
import com.tangem.features.feed.ui.utils.topBarFeedEntryAnimatedContentTransitionSpec
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
@Composable
|
||||
internal fun EntryContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
isOpenedInBottomSheet: Boolean,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
EntryContentV2(
|
||||
bottomSheetState = bottomSheetState,
|
||||
stackState = stackState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
isOpenedInBottomSheet = isOpenedInBottomSheet,
|
||||
)
|
||||
} else {
|
||||
EntryContentV1(
|
||||
bottomSheetState = bottomSheetState,
|
||||
stackState = stackState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
isOpenedInBottomSheet = isOpenedInBottomSheet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EntryContentV1(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
isOpenedInBottomSheet: Boolean,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val animationContent = remember { contentFeedEntryStackAnimation() }
|
||||
val animationAppBar = remember { topBarFeedEntryStackAnimation() }
|
||||
val stackAnimation = remember { contentFeedEntryStackAnimation() }
|
||||
|
||||
Surface(contentColor = background) {
|
||||
Scaffold(
|
||||
|
|
@ -55,7 +84,7 @@ internal fun EntryContent(
|
|||
}
|
||||
},
|
||||
stack = stackState.value,
|
||||
animation = animationAppBar,
|
||||
animation = stackAnimation,
|
||||
) { child ->
|
||||
child.instance.Title(bottomSheetState)
|
||||
}
|
||||
|
|
@ -63,14 +92,76 @@ internal fun EntryContent(
|
|||
content = { contentPadding ->
|
||||
Children(
|
||||
stack = stackState.value,
|
||||
animation = animationContent,
|
||||
animation = stackAnimation,
|
||||
) { child ->
|
||||
child.instance.Content(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
bottomSheetState = bottomSheetState,
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EntryContentV2(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
isOpenedInBottomSheet: Boolean,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val animationContent = remember { contentFeedEntryStackAnimation() }
|
||||
val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) }
|
||||
var topBarHeight by remember { mutableStateOf(0.dp) }
|
||||
val hazeState = rememberHazeState()
|
||||
|
||||
Surface(contentColor = background) {
|
||||
CompositionLocalProvider(LocalHazeState provides hazeState) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Children(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
stack = stackState.value,
|
||||
animation = animationContent,
|
||||
) { child ->
|
||||
child.instance.Content(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.hazeSourceTangem(zIndex = 0f, state = hazeState),
|
||||
contentPadding = PaddingValues(top = topBarHeight),
|
||||
bottomSheetState = bottomSheetState,
|
||||
)
|
||||
}
|
||||
AnimatedContent(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.then(
|
||||
if (!isOpenedInBottomSheet) {
|
||||
Modifier.statusBarsPadding()
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
val height = coordinates.size.height.toDp()
|
||||
topBarHeight = height
|
||||
onHeaderSizeChange(height)
|
||||
}
|
||||
}
|
||||
},
|
||||
targetState = stackState.value.active,
|
||||
transitionSpec = animationAppBar,
|
||||
contentKey = { it.key },
|
||||
label = "FeedEntryAppBar",
|
||||
) { state ->
|
||||
state.instance.Title(bottomSheetState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.tangem.features.feed.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.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.components.SpacerW
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
|
||||
@Composable
|
||||
internal fun FeedSearchBar(
|
||||
isSearchBarClickable: Boolean,
|
||||
feedListSearchBar: FeedListSearchBar,
|
||||
modifier: Modifier = Modifier,
|
||||
startContent: @Composable (() -> Unit)? = null,
|
||||
endContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
FeedSearchBarV2(
|
||||
isSearchBarClickable = isSearchBarClickable,
|
||||
feedListSearchBar = feedListSearchBar,
|
||||
modifier = modifier,
|
||||
startContent = startContent,
|
||||
endContent = endContent,
|
||||
)
|
||||
} else {
|
||||
FeedSearchBarV1(
|
||||
isSearchBarClickable = isSearchBarClickable,
|
||||
feedListSearchBar = feedListSearchBar,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedSearchBarV1(
|
||||
isSearchBarClickable: Boolean,
|
||||
feedListSearchBar: FeedListSearchBar,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(36.dp))
|
||||
.background(color = TangemTheme.colors.field.focused)
|
||||
.conditional(condition = isSearchBarClickable) {
|
||||
clickable(onClick = feedListSearchBar.onBarClick)
|
||||
}
|
||||
.padding(14.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size20),
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(14.dp)
|
||||
|
||||
Text(
|
||||
text = feedListSearchBar.placeholderText.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedSearchBarV2(
|
||||
isSearchBarClickable: Boolean,
|
||||
feedListSearchBar: FeedListSearchBar,
|
||||
modifier: Modifier = Modifier,
|
||||
startContent: @Composable (() -> Unit)? = null,
|
||||
endContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
TangemTopBar(
|
||||
modifier = modifier,
|
||||
startContent = startContent,
|
||||
endContent = endContent,
|
||||
type = TangemTopBarType.BottomSheet,
|
||||
reserveSlotSpace = false,
|
||||
content = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = if (startContent != null) TangemTheme.dimens2.x3 else 0.dp,
|
||||
end = if (endContent != null) TangemTheme.dimens2.x3 else 0.dp,
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(color = TangemTheme.colors2.button.backgroundSecondary)
|
||||
.conditional(condition = isSearchBarClickable) {
|
||||
clickable(onClick = feedListSearchBar.onBarClick)
|
||||
}
|
||||
.padding(TangemTheme.dimens2.x3),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x5),
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_default_24),
|
||||
tint = TangemTheme.colors2.markers.iconGray,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(TangemTheme.dimens2.x1)
|
||||
|
||||
Text(
|
||||
text = feedListSearchBar.placeholderText.resolveReference(),
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -22,19 +22,18 @@ import com.tangem.core.ui.components.*
|
|||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.domain.models.earn.EarnType
|
||||
import com.tangem.features.feed.ui.earn.components.*
|
||||
import com.tangem.features.feed.ui.earn.state.*
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private const val EARN_LOAD_MORE_BUFFER = 3
|
||||
|
||||
@Composable
|
||||
internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
||||
internal fun EarnContent(contentPadding: PaddingValues, state: EarnUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val density = LocalDensity.current
|
||||
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
|
@ -53,7 +52,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
|||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(background),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
|
||||
) {
|
||||
item(key = "mostly_used_header") {
|
||||
SectionHeader(
|
||||
|
|
@ -393,6 +392,7 @@ private fun EarnContentPreviewV1() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
contentPadding = PaddingValues(),
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
|
|
@ -435,6 +435,7 @@ private fun EarnContentPreviewV2() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
contentPadding = PaddingValues(),
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
|
|
@ -477,6 +478,7 @@ private fun EarnContentLoadingPreviewV1() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
contentPadding = PaddingValues(),
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
|
|
@ -496,6 +498,7 @@ private fun EarnContentLoadingPreviewV2() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
contentPadding = PaddingValues(),
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
|
|
@ -515,6 +518,7 @@ private fun EarnContentErrorPreviewV1() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
contentPadding = PaddingValues(),
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
|
|
@ -543,6 +547,7 @@ private fun EarnContentErrorPreviewV2() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
contentPadding = PaddingValues(),
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
|
|
@ -571,6 +576,7 @@ private fun EarnContentEmptyPreviewV1() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
contentPadding = PaddingValues(),
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
|
|
@ -599,6 +605,7 @@ private fun EarnContentEmptyPreviewV2() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
contentPadding = PaddingValues(),
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
|
|
@ -635,7 +642,8 @@ private fun previewEarnListItemUM(
|
|||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
earnTypeTitle = stringReference("Yield"),
|
||||
earnType = EarnType.YIELD,
|
||||
onItemClick = {},
|
||||
)
|
||||
|
||||
|
|
@ -655,6 +663,10 @@ private fun previewEarnUM(
|
|||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
onSliderScroll = {},
|
||||
feedListSearchBar = FeedListSearchBar(
|
||||
placeholderText = TextReference.Str("Search tokens & news"),
|
||||
onBarClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
private const val PLACEHOLDER_ITEMS_COUNT = 8
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ 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.res.*
|
||||
import com.tangem.domain.models.earn.EarnType
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
|
||||
@Composable
|
||||
|
|
@ -95,7 +96,7 @@ private fun EarnListItemV1(item: EarnListItemUM, modifier: Modifier = Modifier)
|
|||
)
|
||||
SpacerH(2.dp)
|
||||
Text(
|
||||
text = item.earnType.resolveReference(),
|
||||
text = item.earnTypeTitle.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
maxLines = 1,
|
||||
|
|
@ -147,6 +148,7 @@ private fun EarnListItemV2(item: EarnListItemUM, modifier: Modifier = Modifier)
|
|||
ModeBlock(
|
||||
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM),
|
||||
earnType = item.earnType,
|
||||
earnTypeTitle = item.earnTypeTitle,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -200,19 +202,24 @@ private fun TokenTitle(name: String, symbol: String, modifier: Modifier = Modifi
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ModeBlock(earnType: TextReference, modifier: Modifier = Modifier) {
|
||||
private fun ModeBlock(earnType: EarnType, earnTypeTitle: TextReference, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_staking_new_16),
|
||||
imageVector = ImageVector.vectorResource(
|
||||
id = when (earnType) {
|
||||
EarnType.STAKING -> R.drawable.ic_staking_new_16
|
||||
EarnType.YIELD -> R.drawable.ic_yield_mode_16
|
||||
},
|
||||
),
|
||||
tint = TangemTheme.colors2.markers.iconGray,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = earnType.resolveReference(),
|
||||
text = earnTypeTitle.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
)
|
||||
|
|
@ -239,7 +246,8 @@ private fun EarnListItemPreviewV1() {
|
|||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 8.50%"),
|
||||
earnType = stringReference("Yield"),
|
||||
earnTypeTitle = stringReference("Yield"),
|
||||
earnType = EarnType.YIELD,
|
||||
onItemClick = {},
|
||||
),
|
||||
)
|
||||
|
|
@ -257,7 +265,8 @@ private fun EarnListItemPreviewV1() {
|
|||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 8.50%"),
|
||||
earnType = stringReference("Yield"),
|
||||
earnTypeTitle = stringReference("Yield"),
|
||||
earnType = EarnType.YIELD,
|
||||
onItemClick = {},
|
||||
),
|
||||
)
|
||||
|
|
@ -286,7 +295,8 @@ private fun EarnListItemPreviewV2() {
|
|||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 8.50%"),
|
||||
earnType = stringReference("Yield"),
|
||||
earnTypeTitle = stringReference("Yield"),
|
||||
earnType = EarnType.YIELD,
|
||||
onItemClick = {},
|
||||
),
|
||||
)
|
||||
|
|
@ -305,7 +315,8 @@ private fun EarnListItemPreviewV2() {
|
|||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 8.50%"),
|
||||
earnType = stringReference("Yield"),
|
||||
earnTypeTitle = stringReference("Yield"),
|
||||
earnType = EarnType.YIELD,
|
||||
onItemClick = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ 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.domain.models.earn.EarnType
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
|
||||
@Composable
|
||||
|
|
@ -161,7 +162,8 @@ private fun EarnListItemPreviewV1() {
|
|||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
earnTypeTitle = stringReference("Yield"),
|
||||
earnType = EarnType.YIELD,
|
||||
onItemClick = {},
|
||||
),
|
||||
onClick = {},
|
||||
|
|
@ -187,7 +189,8 @@ private fun EarnListItemPreviewV2() {
|
|||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
earnTypeTitle = stringReference("Yield"),
|
||||
earnType = EarnType.YIELD,
|
||||
onItemClick = {},
|
||||
),
|
||||
onClick = {},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.feed.ui.earn.state
|
|||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.earn.EarnType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
|
|
@ -32,6 +33,7 @@ internal data class EarnListItemUM(
|
|||
val tokenName: TextReference,
|
||||
val currencyIconState: CurrencyIconState,
|
||||
val earnValue: TextReference,
|
||||
val earnType: TextReference,
|
||||
val earnType: EarnType,
|
||||
val earnTypeTitle: TextReference,
|
||||
val onItemClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
|
||||
@Immutable
|
||||
internal data class EarnUM(
|
||||
|
|
@ -11,4 +12,5 @@ internal data class EarnUM(
|
|||
val onNetworkFilterClick: () -> Unit,
|
||||
val onTypeFilterClick: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
val feedListSearchBar: FeedListSearchBar,
|
||||
)
|
||||
|
|
@ -1,42 +1,35 @@
|
|||
package com.tangem.features.feed.ui.feed
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
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.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.components.FeedSearchBar
|
||||
import com.tangem.features.feed.ui.feed.components.*
|
||||
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.GlobalFeedState
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
|
||||
@Composable
|
||||
internal fun FeedListHeader(
|
||||
|
|
@ -50,14 +43,26 @@ internal fun FeedListHeader(
|
|||
feedListSearchBar = feedListSearchBar,
|
||||
modifier = modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
.conditionalCompose(
|
||||
condition = LocalRedesignEnabled.current,
|
||||
modifier = {
|
||||
hazeEffectTangem {
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = .75f,
|
||||
endIntensity = 0f,
|
||||
preferPerformance = true,
|
||||
easing = EaseOut,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.testTag(SEARCH_BAR),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun FeedList(
|
||||
contentPadding: PaddingValues,
|
||||
state: FeedListUM,
|
||||
modifier: Modifier = Modifier,
|
||||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
|
|
@ -73,19 +78,23 @@ internal fun FeedList(
|
|||
FeedListLoading(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.drawBehind { drawRect(background) },
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(top = contentPadding.calculateTopPadding())
|
||||
.verticalScroll(rememberScrollState()),
|
||||
)
|
||||
}
|
||||
is GlobalFeedState.Error -> {
|
||||
FeedListGlobalError(
|
||||
onRetryClick = animatedState.onRetryClicked,
|
||||
modifier = Modifier.drawBehind { drawRect(background) },
|
||||
modifier = Modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(top = contentPadding.calculateTopPadding()),
|
||||
currentDate = state.currentDate,
|
||||
)
|
||||
}
|
||||
is GlobalFeedState.Content -> {
|
||||
FeedListContent(
|
||||
contentPadding = contentPadding,
|
||||
modifier = Modifier,
|
||||
state = state,
|
||||
promoBannersBlockComponent = promoBannersBlockComponent,
|
||||
|
|
@ -95,41 +104,9 @@ internal fun FeedList(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedSearchBar(
|
||||
isSearchBarClickable: Boolean,
|
||||
feedListSearchBar: FeedListSearchBar,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(36.dp))
|
||||
.background(color = TangemTheme.colors.field.focused)
|
||||
.conditional(condition = isSearchBarClickable) {
|
||||
clickable(onClick = feedListSearchBar.onBarClick)
|
||||
}
|
||||
.padding(14.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size20),
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(14.dp)
|
||||
|
||||
Text(
|
||||
text = feedListSearchBar.placeholderText.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedListContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: FeedListUM,
|
||||
modifier: Modifier = Modifier,
|
||||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
|
|
@ -142,6 +119,9 @@ private fun FeedListContent(
|
|||
.padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
|
||||
.drawBehind { drawRect(background) },
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
SpacerH(contentPadding.calculateTopPadding())
|
||||
}
|
||||
DateBlock(state.currentDate)
|
||||
SpacerH(32.dp)
|
||||
|
||||
|
|
@ -174,6 +154,6 @@ private fun FeedListContent(
|
|||
@Composable
|
||||
private fun FeedListPreview() {
|
||||
TangemThemePreview {
|
||||
FeedList(state = createFeedPreviewState())
|
||||
FeedList(state = createFeedPreviewState(), contentPadding = PaddingValues())
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource
|
|||
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.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
|
|
@ -23,13 +24,16 @@ import com.tangem.core.ui.res.LocalRedesignEnabled
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun Header(
|
||||
internal fun ColumnScope.Header(
|
||||
onSeeAllClick: () -> Unit,
|
||||
isLoading: Boolean,
|
||||
shouldShowSeeAll: Boolean,
|
||||
title: @Composable () -> Unit,
|
||||
) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
if (isRedesignEnabled) {
|
||||
SpacerH(20.dp)
|
||||
}
|
||||
AnimatedContent(isLoading) { animatedState ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -9,10 +9,20 @@ import androidx.compose.ui.unit.dp
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun DateBlock(currentDate: String) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
DateBlockV2(currentDate)
|
||||
} else {
|
||||
DateBlockV1(currentDate)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateBlockV1(currentDate: String) {
|
||||
SpacerH(20.dp)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
|
|
@ -30,4 +40,26 @@ internal fun DateBlock(currentDate: String) {
|
|||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateBlockV2(currentDate: String) {
|
||||
SpacerH(TangemTheme.dimens2.x1)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp),
|
||||
text = stringResourceSafe(R.string.feed_market_and_news),
|
||||
style = TangemTheme.typography2.headingRegular28,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp),
|
||||
text = currentDate,
|
||||
style = TangemTheme.typography2.headingRegular28,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
)
|
||||
SpacerH(TangemTheme.dimens2.x6)
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
|
|||
isLoading = currentChart is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
SpacerH(if (isRedesignEnabled) 20.dp else 12.dp)
|
||||
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
|
|
@ -89,7 +89,7 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
|
|||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
internal fun ColumnScope.MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val onSeeAllClick by rememberUpdatedState {
|
||||
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
|
||||
|
|
|
|||
|
|
@ -17,11 +17,10 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
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.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
|
|
@ -31,6 +30,8 @@ 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.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
|
||||
internal const val FOURTH_ITEM_INDEX = 3
|
||||
|
|
@ -87,8 +88,16 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = if (isRedesignEnabled) {
|
||||
TangemTheme.typography2.headingSemibold20
|
||||
} else {
|
||||
TangemTheme.typography.h3
|
||||
},
|
||||
color = if (isRedesignEnabled) {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
)
|
||||
|
||||
SpacerW(4.dp)
|
||||
|
|
@ -113,7 +122,11 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
append(stringResourceSafe(R.string.feed_tangem_ai))
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
style = if (isRedesignEnabled) {
|
||||
TangemTheme.typography2.bodyRegular16.copy(fontWeight = FontWeight.Medium)
|
||||
} else {
|
||||
TangemTheme.typography.subtitle1
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.earn.EarnType
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import kotlinx.collections.immutable.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal object FeedListPreviewDataProvider {
|
||||
|
|
@ -246,7 +248,11 @@ internal object FeedListPreviewDataProvider {
|
|||
iconUrl = null,
|
||||
ratingPosition = rating,
|
||||
marketCap = marketCap,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "31 285.72$",
|
||||
fiatPrice = BigDecimal(123123),
|
||||
annotated = stringReference("31 285.72$"),
|
||||
),
|
||||
trendPercentText = percent,
|
||||
trendType = trendType,
|
||||
chartData = MarketChartRawData(
|
||||
|
|
@ -273,7 +279,8 @@ internal object FeedListPreviewDataProvider {
|
|||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
earnTypeTitle = stringReference("Yield"),
|
||||
earnType = EarnType.YIELD,
|
||||
onItemClick = {},
|
||||
)
|
||||
}.toPersistentList()
|
||||
|
|
|
|||
|
|
@ -32,9 +32,12 @@ 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.ds.tabs.TangemSegmentUM
|
||||
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
|
||||
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.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
|
|
@ -49,18 +52,21 @@ import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
|
|||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun MarketsTokenDetailsContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: MarketsTokenDetailsUM,
|
||||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
Content(
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
state = state,
|
||||
|
|
@ -77,6 +83,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun Content(
|
||||
contentPadding: PaddingValues,
|
||||
state: MarketsTokenDetailsUM,
|
||||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -100,7 +107,7 @@ private fun Content(
|
|||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
|
|
@ -176,6 +183,7 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
|
|||
Column(modifier = Modifier.weight(1f)) {
|
||||
TokenPriceText(
|
||||
price = state.priceText,
|
||||
priceAnnotated = state.priceAnnotated,
|
||||
triggerPriceChange = state.triggerPriceChange,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
|
|
@ -206,6 +214,28 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
|
|||
|
||||
@Composable
|
||||
private fun TokenPriceText(
|
||||
price: String,
|
||||
triggerPriceChange: StateEvent<PriceChangeType>,
|
||||
priceAnnotated: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
TokenPriceTextV2(
|
||||
priceAnnotated = priceAnnotated,
|
||||
triggerPriceChange = triggerPriceChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
TokenPriceTextV1(
|
||||
price = price,
|
||||
triggerPriceChange = triggerPriceChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceTextV1(
|
||||
price: String,
|
||||
triggerPriceChange: StateEvent<PriceChangeType>,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -238,6 +268,40 @@ private fun TokenPriceText(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceTextV2(
|
||||
priceAnnotated: TextReference,
|
||||
triggerPriceChange: StateEvent<PriceChangeType>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val growColor = TangemTheme.colors2.graphic.status.accent
|
||||
val fallColor = TangemTheme.colors2.graphic.status.warning
|
||||
val generalColor = TangemTheme.colors2.text.neutral.primary
|
||||
|
||||
val color = remember(generalColor) { Animatable(generalColor) }
|
||||
|
||||
EventEffect(triggerPriceChange) { priceChangeType ->
|
||||
val nextColor = when (priceChangeType) {
|
||||
PriceChangeType.UP,
|
||||
-> growColor
|
||||
PriceChangeType.DOWN -> fallColor
|
||||
PriceChangeType.NEUTRAL -> return@EventEffect
|
||||
}
|
||||
|
||||
color.animateTo(nextColor, snap())
|
||||
color.animateTo(generalColor, tween(durationMillis = 500))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = priceAnnotated.resolveAnnotatedReference(),
|
||||
modifier = modifier,
|
||||
color = color.value,
|
||||
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography2.headingBold34.fontSize),
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography2.headingBold34,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IntervalSelector(
|
||||
trendInterval: PriceChangeInterval,
|
||||
|
|
@ -245,6 +309,26 @@ private fun IntervalSelector(
|
|||
onIntervalClick: (PriceChangeInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
val items = remember {
|
||||
PriceChangeInterval.entries
|
||||
.map { TangemSegmentUM(it.toString(), it.getText()) }.toImmutableList()
|
||||
}
|
||||
val selectedItem = remember(trendInterval) {
|
||||
items.firstOrNull { it.id == trendInterval.toString() }
|
||||
}
|
||||
|
||||
TangemSegmentedPicker(
|
||||
items = items,
|
||||
initialSelectedItem = selectedItem,
|
||||
isFixed = true,
|
||||
modifier = modifier,
|
||||
onClick = { onIntervalClick(PriceChangeInterval.valueOf(it.id)) },
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
|
|
@ -296,7 +380,6 @@ private fun ShowPriceSubtitleEffect(lazyListState: LazyListState, onShouldShowPr
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PriceChangeInterval.getText(): TextReference {
|
||||
return when (this) {
|
||||
PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title)
|
||||
|
|
@ -321,6 +404,7 @@ private fun MarketsTokenDetailsContent_Preview(
|
|||
state = params,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
portfolioBlock = {},
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,20 +111,23 @@ private fun InsightsBlockV1(state: InsightsUM, modifier: Modifier = Modifier) {
|
|||
|
||||
@Composable
|
||||
private fun InsightsBlockV2(state: InsightsUM, modifier: Modifier = Modifier) {
|
||||
val segmentItems = persistentListOf(
|
||||
TangemSegmentUM(
|
||||
id = PriceChangeInterval.H24.name,
|
||||
title = resourceReference(R.string.markets_token_details_insight_day_timeline),
|
||||
),
|
||||
TangemSegmentUM(
|
||||
id = PriceChangeInterval.WEEK.name,
|
||||
title = resourceReference(R.string.markets_token_details_insight_week_timeline),
|
||||
),
|
||||
TangemSegmentUM(
|
||||
id = PriceChangeInterval.MONTH.name,
|
||||
title = resourceReference(R.string.markets_token_details_insight_month_timeline),
|
||||
),
|
||||
)
|
||||
val segmentItems = remember {
|
||||
persistentListOf(
|
||||
TangemSegmentUM(
|
||||
id = PriceChangeInterval.H24.name,
|
||||
title = resourceReference(R.string.markets_token_details_insight_day_timeline),
|
||||
),
|
||||
TangemSegmentUM(
|
||||
id = PriceChangeInterval.WEEK.name,
|
||||
title = resourceReference(R.string.markets_token_details_insight_week_timeline),
|
||||
),
|
||||
TangemSegmentUM(
|
||||
id = PriceChangeInterval.MONTH.name,
|
||||
title = resourceReference(R.string.markets_token_details_insight_month_timeline),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
var currentInterval by remember { mutableStateOf(segmentItems.first()) }
|
||||
|
||||
TokenMarketInformationBlock(
|
||||
|
|
@ -143,7 +146,6 @@ private fun InsightsBlockV2(state: InsightsUM, modifier: Modifier = Modifier) {
|
|||
TangemSegmentedPicker(
|
||||
items = segmentItems,
|
||||
initialSelectedItem = segmentItems.first(),
|
||||
hasSeparator = true,
|
||||
isFixed = false,
|
||||
isAltSurface = true,
|
||||
minSegmentWidth = 48.dp,
|
||||
|
|
|
|||
|
|
@ -21,14 +21,12 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
|
||||
import com.tangem.core.ui.ds.progress.TangemLinearProgressIndicatorWithDot
|
||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||
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.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -36,6 +34,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
|||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.components.MetricsCard
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUMV2
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketRatingChange24H
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketRatingType
|
||||
import com.tangem.features.feed.ui.market.detailed.state.TrendingVolumeLiquidityType
|
||||
|
||||
|
|
@ -45,15 +44,7 @@ internal fun MarketCapCard(item: InfoPointUMV2.MarketCap) {
|
|||
modifier = Modifier
|
||||
.heightIn(120.dp)
|
||||
.fillMaxWidth(),
|
||||
title = {
|
||||
Text(
|
||||
text = item.capitalizationValue.resolveReference(),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
title = { MetricValueText(value = item.capitalizationValue) },
|
||||
content = {
|
||||
InformationTextBlock(
|
||||
text = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
|
|
@ -69,25 +60,22 @@ internal fun TradingVolumeCard(item: InfoPointUMV2.TradingVolume) {
|
|||
TrendingVolumeLiquidityType.HIGH -> TangemTheme.colors2.markers.backgroundSolidGreen
|
||||
TrendingVolumeLiquidityType.MEDIUM -> TangemTheme.colors2.graphic.status.attention
|
||||
TrendingVolumeLiquidityType.LOW -> TangemTheme.colors2.graphic.status.warning
|
||||
TrendingVolumeLiquidityType.UNKNOWN -> TangemTheme.colors2.surface.level3
|
||||
}
|
||||
val valueColor = metricValueColor(hasData = item.tradingValue != null)
|
||||
|
||||
MetricsCard(
|
||||
modifier = Modifier
|
||||
.heightIn(120.dp)
|
||||
.fillMaxWidth(),
|
||||
title = {
|
||||
Row {
|
||||
Text(
|
||||
text = item.tradingValue.resolveReference(),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
MetricValueText(item.tradingValue)
|
||||
Text(
|
||||
modifier = Modifier.padding(TangemTheme.dimens2.x1),
|
||||
text = stringResourceSafe(R.string.markets_token_details_trading_interval),
|
||||
style = TangemTheme.typography2.captionSemibold11,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
color = valueColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
|
@ -95,14 +83,17 @@ internal fun TradingVolumeCard(item: InfoPointUMV2.TradingVolume) {
|
|||
},
|
||||
content = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
TangemLinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(6.dp),
|
||||
progress = { item.liquidity },
|
||||
color = tradingColor,
|
||||
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
|
||||
)
|
||||
if (item.liquidity != null) {
|
||||
TangemLinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(6.dp),
|
||||
progress = { item.liquidity },
|
||||
color = tradingColor,
|
||||
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||
.copy(alpha = .1f),
|
||||
)
|
||||
}
|
||||
SpacerH(12.dp)
|
||||
InformationTextBlock(
|
||||
text = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
|
|
@ -127,37 +118,25 @@ internal fun MarketPositionCard(item: InfoPointUMV2.MarketPosition) {
|
|||
.fillMaxWidth(),
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_left_20),
|
||||
tint = ratingColor,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = item.position,
|
||||
color = ratingColor,
|
||||
style = TangemTheme.typography2.headingSemibold20.copy(letterSpacing = 0.sp),
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_right_20),
|
||||
tint = ratingColor,
|
||||
contentDescription = null,
|
||||
)
|
||||
MarketPositionValue(position = item.position, ratingColor = ratingColor)
|
||||
if (item.position != null) {
|
||||
SpacerW(6.dp)
|
||||
RatingChangeIndicator(change = item.marketRatingChange24H)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
TangemLinearProgressIndicatorWithDot(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(6.dp),
|
||||
progress = { item.rangeValue },
|
||||
dotColor = TangemTheme.colors2.fill.neutral.primaryInvertedConstant,
|
||||
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
|
||||
)
|
||||
if (item.rangeValue != null) {
|
||||
TangemLinearProgressIndicatorWithDot(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(6.dp),
|
||||
progress = { item.rangeValue },
|
||||
dotColor = TangemTheme.colors2.fill.neutral.primaryInvertedConstant,
|
||||
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
|
||||
)
|
||||
}
|
||||
SpacerH(12.dp)
|
||||
InformationTextBlock(
|
||||
text = resourceReference(R.string.markets_token_details_market_rating),
|
||||
|
|
@ -178,19 +157,41 @@ internal fun FDVCard(item: InfoPointUMV2.FullyDilutedValuation) {
|
|||
.heightIn(120.dp)
|
||||
.fillMaxWidth(),
|
||||
title = {
|
||||
Text(
|
||||
text = item.value.resolveReference(),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (item.fullyDilutedValuationChange24 != null) {
|
||||
Row {
|
||||
MetricValueText(value = item.fullyDilutedValuationChange24)
|
||||
Text(
|
||||
modifier = Modifier.padding(TangemTheme.dimens2.x1),
|
||||
text = stringResourceSafe(R.string.markets_token_details_trading_interval),
|
||||
style = TangemTheme.typography2.captionSemibold11,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
MetricValueText(value = item.value)
|
||||
}
|
||||
},
|
||||
content = {
|
||||
InformationTextBlock(
|
||||
text = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
onInfoClick = item.onInfoClick,
|
||||
)
|
||||
Column {
|
||||
if (item.fullyDilutedValuationChange24 != null) {
|
||||
Text(
|
||||
text = item.value?.resolveReference()
|
||||
?: stringResourceSafe(R.string.token_market_metrics_no_data),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
SpacerH(4.dp)
|
||||
}
|
||||
|
||||
InformationTextBlock(
|
||||
text = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
onInfoClick = item.onInfoClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -212,15 +213,11 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
MetricValueText(
|
||||
value = item.currentValue,
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp)
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM),
|
||||
text = item.currentValue.resolveReference(),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
|
|
@ -238,7 +235,7 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
|
|||
.padding(top = 12.dp)
|
||||
.layoutId(TangemRowLayoutId.END_BOTTOM),
|
||||
text = item.maxValue.resolveReference(),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
style = TangemTheme.typography2.headingSemibold22,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
@ -253,7 +250,8 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
|
|||
.fillMaxWidth()
|
||||
.height(6.dp),
|
||||
color = TangemTheme.colors2.graphic.status.accent,
|
||||
trackColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
|
||||
trackColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||
.copy(alpha = .1f),
|
||||
progress = { item.fillValue },
|
||||
strokeCap = StrokeCap.Round,
|
||||
drawStopIndicator = {},
|
||||
|
|
@ -265,6 +263,87 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
|
|||
)
|
||||
}
|
||||
|
||||
// region Private helpers
|
||||
|
||||
@Composable
|
||||
private fun MetricValueText(value: TextReference?, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = value?.resolveReference() ?: stringResourceSafe(R.string.token_market_metrics_no_data),
|
||||
style = TangemTheme.typography2.headingSemibold22,
|
||||
color = metricValueColor(hasData = value != null),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun metricValueColor(hasData: Boolean): Color {
|
||||
return if (hasData) TangemTheme.colors2.text.neutral.primary else TangemTheme.colors2.text.neutral.tertiary
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketPositionValue(position: TextReference?, ratingColor: Color) {
|
||||
if (position != null) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_left_20),
|
||||
tint = ratingColor,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = position.resolveReference(),
|
||||
color = ratingColor,
|
||||
style = TangemTheme.typography2.headingSemibold22.copy(letterSpacing = 0.sp),
|
||||
maxLines = 1,
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_right_20),
|
||||
tint = ratingColor,
|
||||
contentDescription = null,
|
||||
)
|
||||
} else {
|
||||
MetricValueText(value = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RatingChangeIndicator(change: MarketRatingChange24H) {
|
||||
when (change) {
|
||||
is MarketRatingChange24H.Up -> RatingChangeContent(
|
||||
iconRes = R.drawable.ic_arrow_up_8,
|
||||
iconTint = TangemTheme.colors2.markers.iconGreen,
|
||||
changeValue = change.changeValue.toString(),
|
||||
textColor = TangemTheme.colors2.text.status.positive,
|
||||
)
|
||||
is MarketRatingChange24H.Down -> RatingChangeContent(
|
||||
iconRes = R.drawable.ic_arrow_down_8,
|
||||
iconTint = TangemTheme.colors2.markers.iconRed,
|
||||
changeValue = change.changeValue.toString(),
|
||||
textColor = TangemTheme.colors2.text.status.warning,
|
||||
)
|
||||
MarketRatingChange24H.NoChanges -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RatingChangeContent(iconRes: Int, iconTint: Color, changeValue: String, textColor: Color) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x3),
|
||||
imageVector = ImageVector.vectorResource(id = iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerW(2.dp)
|
||||
Text(
|
||||
text = changeValue,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = textColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketRatingType.baseColor(): Color {
|
||||
val isDarkTheme = LocalIsInDarkTheme.current
|
||||
|
|
@ -295,6 +374,8 @@ private fun mapRatingToCardColor(marketRatingType: MarketRatingType): Color {
|
|||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private const val GOLD_PLACE_COLOR_NIGHT = 0xFFFBEE76
|
||||
private const val GOLD_PLACE_COLOR_LIGHT = 0xFFD9B900
|
||||
private const val SILVER_PLACE_COLOR_NIGHT = 0xFFAABEF7
|
||||
|
|
@ -353,17 +434,19 @@ private fun MetricsCardsPreview() {
|
|||
|
||||
MarketPositionCard(
|
||||
item = InfoPointUMV2.MarketPosition(
|
||||
position = "1",
|
||||
position = stringReference("1"),
|
||||
rangeValue = 0.02f,
|
||||
marketRatingType = MarketRatingType.GOLD,
|
||||
onInfoClick = {},
|
||||
marketRatingChange24H = MarketRatingChange24H.NoChanges,
|
||||
),
|
||||
)
|
||||
|
||||
MarketPositionCard(
|
||||
item = InfoPointUMV2.MarketPosition(
|
||||
position = "2",
|
||||
position = stringReference("2"),
|
||||
rangeValue = 0.05f,
|
||||
marketRatingChange24H = MarketRatingChange24H.Up(1),
|
||||
marketRatingType = MarketRatingType.SILVER,
|
||||
onInfoClick = {},
|
||||
),
|
||||
|
|
@ -371,25 +454,28 @@ private fun MetricsCardsPreview() {
|
|||
|
||||
MarketPositionCard(
|
||||
item = InfoPointUMV2.MarketPosition(
|
||||
position = "3",
|
||||
rangeValue = 0.08f,
|
||||
marketRatingType = MarketRatingType.BRONZE,
|
||||
position = null,
|
||||
rangeValue = null,
|
||||
marketRatingType = MarketRatingType.OTHER,
|
||||
onInfoClick = {},
|
||||
marketRatingChange24H = MarketRatingChange24H.NoChanges,
|
||||
),
|
||||
)
|
||||
|
||||
MarketPositionCard(
|
||||
item = InfoPointUMV2.MarketPosition(
|
||||
position = "42",
|
||||
position = stringReference("42"),
|
||||
rangeValue = 0.42f,
|
||||
marketRatingType = MarketRatingType.OTHER,
|
||||
onInfoClick = {},
|
||||
marketRatingChange24H = MarketRatingChange24H.Down(15),
|
||||
),
|
||||
)
|
||||
|
||||
FDVCard(
|
||||
item = InfoPointUMV2.FullyDilutedValuation(
|
||||
value = stringReference("$ 1.5 T"),
|
||||
fullyDilutedValuationChange24 = stringReference("$ 2.44 M in total"),
|
||||
onInfoClick = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ internal object MarketsTokenDetailsPreview {
|
|||
onScroll = {},
|
||||
),
|
||||
onShareClick = {},
|
||||
priceAnnotated = stringReference("$0.00000000324"),
|
||||
)
|
||||
|
||||
val contentState = MarketsTokenDetailsUM(
|
||||
|
|
@ -144,5 +145,6 @@ internal object MarketsTokenDetailsPreview {
|
|||
onScroll = {},
|
||||
),
|
||||
onShareClick = {},
|
||||
priceAnnotated = stringReference("$0.00000000324"),
|
||||
)
|
||||
}
|
||||
|
|
@ -2,18 +2,19 @@ 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.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
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal data class MarketsTokenDetailsUM(
|
||||
val tokenName: String,
|
||||
val priceText: String,
|
||||
val priceAnnotated: TextReference,
|
||||
val iconUrl: String?,
|
||||
val dateTimeText: TextReference,
|
||||
val priceChangePercentText: String?,
|
||||
|
|
|
|||
|
|
@ -14,35 +14,37 @@ internal sealed interface InfoPointUMV2 {
|
|||
|
||||
@Immutable
|
||||
data class MarketCap(
|
||||
val capitalizationValue: TextReference,
|
||||
val capitalizationValue: TextReference?,
|
||||
val onInfoClick: () -> Unit,
|
||||
) : InfoPointUMV2
|
||||
|
||||
@Immutable
|
||||
data class TradingVolume(
|
||||
val tradingValue: TextReference,
|
||||
val liquidity: Float,
|
||||
val tradingValue: TextReference?,
|
||||
val liquidity: Float?,
|
||||
val trendingVolumeLiquidityType: TrendingVolumeLiquidityType,
|
||||
val onInfoClick: () -> Unit,
|
||||
) : InfoPointUMV2
|
||||
|
||||
@Immutable
|
||||
data class MarketPosition(
|
||||
val position: String,
|
||||
val rangeValue: Float,
|
||||
val position: TextReference?,
|
||||
val rangeValue: Float?,
|
||||
val marketRatingChange24H: MarketRatingChange24H,
|
||||
val marketRatingType: MarketRatingType,
|
||||
val onInfoClick: () -> Unit,
|
||||
) : InfoPointUMV2
|
||||
|
||||
@Immutable
|
||||
data class FullyDilutedValuation(
|
||||
val value: TextReference,
|
||||
val value: TextReference?,
|
||||
val fullyDilutedValuationChange24: TextReference?,
|
||||
val onInfoClick: () -> Unit,
|
||||
) : InfoPointUMV2
|
||||
|
||||
@Immutable
|
||||
data class CirculatingSupply(
|
||||
val currentValue: TextReference,
|
||||
val currentValue: TextReference?,
|
||||
val maxValue: TextReference?,
|
||||
val fillValue: Float?,
|
||||
val onInfoClick: () -> Unit,
|
||||
|
|
@ -60,9 +62,18 @@ internal data class MetricsV2UM(
|
|||
}
|
||||
|
||||
internal enum class TrendingVolumeLiquidityType {
|
||||
HIGH, MEDIUM, LOW,
|
||||
HIGH, MEDIUM, LOW, UNKNOWN
|
||||
}
|
||||
|
||||
internal enum class MarketRatingType {
|
||||
GOLD, SILVER, BRONZE, OTHER
|
||||
}
|
||||
|
||||
internal sealed interface MarketRatingChange24H {
|
||||
|
||||
data class Up(val changeValue: Int) : MarketRatingChange24H
|
||||
|
||||
data class Down(val changeValue: Int) : MarketRatingChange24H
|
||||
|
||||
data object NoChanges : MarketRatingChange24H
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -18,33 +17,30 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
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.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.model.market.list.state.*
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
import com.tangem.features.feed.ui.market.list.components.MarketsListLazyColumn
|
||||
import com.tangem.features.feed.ui.market.list.components.MarketsListSortByBottomSheet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.features.feed.ui.market.list.components.Options
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
|
@ -106,7 +102,7 @@ internal fun TopBarWithSearch(
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) {
|
||||
internal fun MarketsList(contentPadding: PaddingValues, state: MarketsListUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -114,7 +110,7 @@ internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) {
|
|||
.imePadding()
|
||||
.drawBehind { drawRect(background) },
|
||||
) {
|
||||
Content(state = state)
|
||||
Content(state = state, contentPadding = contentPadding)
|
||||
}
|
||||
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
|
||||
KeyboardEvents(isSortByBottomSheetShown = state.sortByBottomSheet.isShown)
|
||||
|
|
@ -122,11 +118,21 @@ internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) {
|
|||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modifier) {
|
||||
val strokeColor = TangemTheme.colors.stroke.primary
|
||||
private fun ColumnScope.Content(contentPadding: PaddingValues, state: MarketsListUM, modifier: Modifier = Modifier) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
|
||||
val hazeState = rememberHazeState()
|
||||
|
||||
val strokeColor = if (isRedesignEnabled) {
|
||||
TangemTheme.colors2.border.neutral.primary
|
||||
} else {
|
||||
TangemTheme.colors.stroke.primary
|
||||
}
|
||||
|
||||
val scrolledState = remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier.padding(horizontal = TangemTheme.dimens.size16)) {
|
||||
SpacerH(contentPadding.calculateTopPadding())
|
||||
AnimatedVisibility(
|
||||
visible = scrolledState.value.not() &&
|
||||
state.isInSearchMode &&
|
||||
|
|
@ -145,11 +151,19 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
|
|||
Column {
|
||||
AnimatedVisibility(!state.isInSearchMode && !state.marketsSearchBar.shouldAlwaysShowSearchBar) {
|
||||
Options(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier.padding(
|
||||
bottom = if (isRedesignEnabled) {
|
||||
TangemTheme.dimens2.x2
|
||||
} else {
|
||||
TangemTheme.dimens.spacing12
|
||||
},
|
||||
),
|
||||
sortByTypeUM = state.selectedSortBy,
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onIntervalClick,
|
||||
onSortByClick = state.onSortByButtonClick,
|
||||
sortMenuUM = state.sortByMenuUM,
|
||||
hazeState = hazeState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -172,65 +186,18 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
|
|||
},
|
||||
)
|
||||
ItemsList(
|
||||
modifier = Modifier.conditionalCompose(
|
||||
condition = isRedesignEnabled,
|
||||
modifier = {
|
||||
hazeSourceTangem(zIndex = 0f, state = hazeState)
|
||||
},
|
||||
),
|
||||
scrolledState = scrolledState,
|
||||
isInSearchMode = state.isInSearchMode,
|
||||
state = state.list,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Options(
|
||||
sortByTypeUM: SortByTypeUM,
|
||||
trendInterval: MarketsListUM.TrendInterval,
|
||||
onSortByClick: () -> Unit,
|
||||
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(IntrinsicSize.Max)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = sortByTypeUM.text,
|
||||
onClick = onSortByClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
),
|
||||
)
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
MarketsListUM.TrendInterval.H24,
|
||||
MarketsListUM.TrendInterval.D7,
|
||||
MarketsListUM.TrendInterval.M1,
|
||||
),
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
initialSelectedItem = trendInterval,
|
||||
onClick = onIntervalClick,
|
||||
modifier = Modifier
|
||||
.width(160.dp)
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.text.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemsList(
|
||||
scrolledState: MutableState<Boolean>,
|
||||
|
|
@ -307,6 +274,7 @@ private fun Preview() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(primaryBackground) },
|
||||
) {
|
||||
MarketsList(
|
||||
contentPadding = PaddingValues(),
|
||||
state = MarketsListUM(
|
||||
list = ListUM.Content(
|
||||
items = MarketChartListItemPreviewDataProvider().values
|
||||
|
|
@ -343,6 +311,14 @@ private fun Preview() {
|
|||
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
||||
),
|
||||
onSearchClicked = {},
|
||||
feedListSearchBar = FeedListSearchBar(
|
||||
onBarClick = {},
|
||||
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
|
||||
),
|
||||
sortByMenuUM = SortByMenuUM(
|
||||
selectedOption = SortByTypeUM.Rating,
|
||||
onOptionClicked = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.features.feed.ui.market.list.components
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM.Companion.TOKEN_LAZY_LIST_ID_SEPARATOR
|
||||
|
|
@ -19,9 +20,12 @@ 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.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -40,6 +44,7 @@ internal fun MarketsListLazyColumn(
|
|||
lazyListState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
|
@ -93,11 +98,22 @@ internal fun MarketsListLazyColumn(
|
|||
}
|
||||
}
|
||||
is ListUM.Content -> {
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = state.items,
|
||||
key = { it.getComposeKey() },
|
||||
) { item ->
|
||||
key = { _, item -> item.getComposeKey() },
|
||||
) { index, item ->
|
||||
MarketsListItem(
|
||||
modifier = Modifier.conditionalCompose(
|
||||
condition = isRedesignEnabled,
|
||||
modifier = {
|
||||
roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.items.lastIndex,
|
||||
backgroundColor = TangemTheme.colors2.surface.level3,
|
||||
radius = TangemTheme.dimens2.x5,
|
||||
)
|
||||
},
|
||||
),
|
||||
model = item,
|
||||
onClick = { state.onItemClick(item) },
|
||||
)
|
||||
|
|
@ -144,8 +160,8 @@ private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifi
|
|||
Box(
|
||||
modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
package com.tangem.features.feed.ui.market.list.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton
|
||||
import com.tangem.core.ui.ds.button.TangemButtonShape
|
||||
import com.tangem.core.ui.ds.button.TangemButtonSize
|
||||
import com.tangem.core.ui.ds.tabs.TangemSegmentUM
|
||||
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByMenuUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.core.ui.ds.button.TangemButtonIconPosition as RedesignTangemButtonIconPosition
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun Options(
|
||||
sortMenuUM: SortByMenuUM,
|
||||
sortByTypeUM: SortByTypeUM,
|
||||
trendInterval: MarketsListUM.TrendInterval,
|
||||
hazeState: HazeState,
|
||||
onSortByClick: () -> Unit,
|
||||
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
OptionsV2(
|
||||
sortMenuUM = sortMenuUM,
|
||||
trendInterval = trendInterval,
|
||||
onIntervalClick = onIntervalClick,
|
||||
modifier = modifier,
|
||||
hazeState = hazeState,
|
||||
)
|
||||
} else {
|
||||
OptionsV1(
|
||||
sortByTypeUM = sortByTypeUM,
|
||||
trendInterval = trendInterval,
|
||||
onSortByClick = onSortByClick,
|
||||
onIntervalClick = onIntervalClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OptionsV1(
|
||||
sortByTypeUM: SortByTypeUM,
|
||||
trendInterval: MarketsListUM.TrendInterval,
|
||||
onSortByClick: () -> Unit,
|
||||
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(IntrinsicSize.Max)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = sortByTypeUM.text,
|
||||
onClick = onSortByClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
),
|
||||
)
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
MarketsListUM.TrendInterval.H24,
|
||||
MarketsListUM.TrendInterval.D7,
|
||||
MarketsListUM.TrendInterval.M1,
|
||||
),
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
initialSelectedItem = trendInterval,
|
||||
onClick = onIntervalClick,
|
||||
modifier = Modifier
|
||||
.width(160.dp)
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.text.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OptionsV2(
|
||||
sortMenuUM: SortByMenuUM,
|
||||
trendInterval: MarketsListUM.TrendInterval,
|
||||
hazeState: HazeState,
|
||||
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isShowDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val segmentItems = remember {
|
||||
persistentListOf(
|
||||
TangemSegmentUM(
|
||||
id = MarketsListUM.TrendInterval.H24.name,
|
||||
title = MarketsListUM.TrendInterval.H24.text,
|
||||
),
|
||||
TangemSegmentUM(
|
||||
id = MarketsListUM.TrendInterval.D7.name,
|
||||
title = MarketsListUM.TrendInterval.D7.text,
|
||||
),
|
||||
TangemSegmentUM(
|
||||
id = MarketsListUM.TrendInterval.M1.name,
|
||||
title = MarketsListUM.TrendInterval.M1.text,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(IntrinsicSize.Max)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
PrimaryInverseTangemButton(
|
||||
onClick = {
|
||||
isShowDropdownMenu = true
|
||||
},
|
||||
iconPosition = RedesignTangemButtonIconPosition.End,
|
||||
iconRes = R.drawable.ic_chewron_down_20,
|
||||
text = sortMenuUM.selectedOption.text,
|
||||
size = TangemButtonSize.X9,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
)
|
||||
|
||||
TangemSegmentedPicker(
|
||||
items = segmentItems,
|
||||
initialSelectedItem = segmentItems.firstOrNull { it.id == trendInterval.name },
|
||||
isFixed = false,
|
||||
isAltSurface = true,
|
||||
minSegmentWidth = 54.dp,
|
||||
onClick = { segment -> onIntervalClick(MarketsListUM.TrendInterval.valueOf(segment.id)) },
|
||||
)
|
||||
}
|
||||
|
||||
SortByMenu(
|
||||
sortMenuUM = sortMenuUM,
|
||||
showDropdownMenu = isShowDropdownMenu,
|
||||
onDropdownDismiss = { isShowDropdownMenu = false },
|
||||
modifier = Modifier.hazeEffectTangem(hazeState) {
|
||||
blurRadius = 10.dp
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.feed.ui.market.list.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.ds.contextmenu.TangemContextMenu
|
||||
import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem
|
||||
import com.tangem.features.feed.model.market.list.state.SortByMenuUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
internal fun SortByMenu(
|
||||
sortMenuUM: SortByMenuUM,
|
||||
showDropdownMenu: Boolean,
|
||||
onDropdownDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TangemContextMenu(
|
||||
expanded = showDropdownMenu,
|
||||
onDismissRequest = onDropdownDismiss,
|
||||
offset = DpOffset.Zero,
|
||||
modifier = modifier,
|
||||
) {
|
||||
SortByTypeUM.entries.fastForEach { sortType ->
|
||||
TangemContextMenuCheckboxItem(
|
||||
title = sortType.text,
|
||||
isChecked = sortMenuUM.selectedOption == sortType,
|
||||
onClick = {
|
||||
sortMenuUM.onOptionClicked(sortType)
|
||||
onDropdownDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,11 +13,9 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.components.pager.PagerIndicator
|
||||
import com.tangem.core.ui.ds.TangemPagerIndicator
|
||||
import com.tangem.core.ui.ds.TangemPagerIndicatorColors
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.features.feed.ui.news.details.components.ArticleDetail
|
||||
import com.tangem.features.feed.ui.news.details.components.NewsDetailsPlaceholder
|
||||
|
|
@ -26,7 +24,7 @@ import com.tangem.features.feed.ui.news.details.state.MockArticlesFactory
|
|||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
||||
|
||||
@Composable
|
||||
internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
|
||||
internal fun NewsDetailsContent(state: NewsDetailsUM, contentPadding: PaddingValues, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
AnimatedContent(
|
||||
targetState = state.articlesStateUM,
|
||||
|
|
@ -34,15 +32,16 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif
|
|||
) { animatedState ->
|
||||
when (animatedState) {
|
||||
ArticlesStateUM.Content -> {
|
||||
Content(state = state, background = background)
|
||||
Content(state = state, background = background, contentPadding = contentPadding)
|
||||
}
|
||||
ArticlesStateUM.Loading -> {
|
||||
NewsDetailsPlaceholder(background = background)
|
||||
NewsDetailsPlaceholder(background = background, contentPadding = contentPadding)
|
||||
}
|
||||
is ArticlesStateUM.LoadingError -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = contentPadding.calculateTopPadding())
|
||||
.background(background),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
|
|
@ -59,7 +58,7 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: NewsDetailsUM, background: Color) {
|
||||
private fun Content(contentPadding: PaddingValues, state: NewsDetailsUM, background: Color) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = state.selectedArticleIndex,
|
||||
|
|
@ -84,12 +83,6 @@ private fun Content(state: NewsDetailsUM, background: Color) {
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.conditionalCompose(
|
||||
condition = isRedesignEnabled,
|
||||
modifier = {
|
||||
hazeSourceTangem(zIndex = 1f)
|
||||
},
|
||||
)
|
||||
.background(background),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
|
@ -104,6 +97,7 @@ private fun Content(state: NewsDetailsUM, background: Color) {
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
onLikeClick = { state.onLikeClick(article.id) },
|
||||
relatedTokensUM = state.relatedTokensUM,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
if (state.articles.size > 1) {
|
||||
|
|
@ -150,6 +144,7 @@ private fun PreviewNewsDetailsContent() {
|
|||
onBackClick = {},
|
||||
onArticleIndexChanged = {},
|
||||
),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -174,6 +169,7 @@ private fun PreviewNewsDetailsContentV2() {
|
|||
onBackClick = {},
|
||||
onArticleIndexChanged = {},
|
||||
),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -28,6 +29,7 @@ import com.tangem.core.ui.ds.button.TangemButtonShape
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalHazeState
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -35,9 +37,11 @@ import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
|
|||
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||
import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM
|
||||
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
|
||||
@Composable
|
||||
internal fun ArticleDetail(
|
||||
contentPadding: PaddingValues,
|
||||
article: ArticleUM,
|
||||
onLikeClick: () -> Unit,
|
||||
relatedTokensUM: RelatedTokensUM,
|
||||
|
|
@ -45,6 +49,7 @@ internal fun ArticleDetail(
|
|||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
ArticleDetailV2(
|
||||
contentPadding = contentPadding,
|
||||
article = article,
|
||||
onLikeClick = onLikeClick,
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
|
|
@ -190,6 +195,7 @@ private fun ArticleDetailV1(
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun ArticleDetailV2(
|
||||
contentPadding: PaddingValues,
|
||||
article: ArticleUM,
|
||||
onLikeClick: () -> Unit,
|
||||
relatedTokensUM: RelatedTokensUM,
|
||||
|
|
@ -199,126 +205,127 @@ internal fun ArticleDetailV2(
|
|||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val pagerHeight = 32.dp
|
||||
val contentPadding = pagerHeight + 56.dp + with(density) {
|
||||
val bottomPadding = pagerHeight + 56.dp + with(density) {
|
||||
WindowInsets.navigationBars.getBottom(this).div(this.density)
|
||||
}.dp
|
||||
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.hazeSourceTangem(zIndex = 0f)
|
||||
.background(background),
|
||||
contentPadding = PaddingValues(bottom = contentPadding),
|
||||
) {
|
||||
item("content") {
|
||||
ArticleHeader(
|
||||
title = article.title,
|
||||
createdAt = article.createdAt.resolveReference(),
|
||||
score = article.score,
|
||||
tags = article.tags,
|
||||
isTrending = article.isTrending,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.shortContent.isNotEmpty()) {
|
||||
QuickRecap(
|
||||
content = article.shortContent,
|
||||
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.hazeSourceTangem(zIndex = -1f)
|
||||
.background(background),
|
||||
contentPadding = PaddingValues(bottom = bottomPadding, top = contentPadding.calculateTopPadding()),
|
||||
) {
|
||||
item("content") {
|
||||
ArticleHeader(
|
||||
title = article.title,
|
||||
createdAt = article.createdAt.resolveReference(),
|
||||
score = article.score,
|
||||
tags = article.tags,
|
||||
isTrending = article.isTrending,
|
||||
modifier = Modifier
|
||||
.padding(top = 32.dp)
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = article.content,
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
SpacerH(24.dp)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
SecondaryTangemButton(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = resourceReference(R.string.news_like),
|
||||
size = com.tangem.core.ui.ds.button.TangemButtonSize.X9,
|
||||
iconRes = if (article.isLiked) {
|
||||
R.drawable.ic_like_20
|
||||
} else {
|
||||
R.drawable.ic_heart_20
|
||||
},
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLikeClick()
|
||||
},
|
||||
shape = TangemButtonShape.Rounded,
|
||||
)
|
||||
|
||||
RelatedTokensBlock(
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
onItemClick = when (relatedTokensUM) {
|
||||
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
|
||||
else -> null
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
SpacerH(24.dp)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_sources),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
if (article.shortContent.isNotEmpty()) {
|
||||
QuickRecap(
|
||||
content = article.shortContent,
|
||||
modifier = Modifier
|
||||
.padding(top = 32.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
item("relatedArticles") {
|
||||
LazyRow(
|
||||
Text(
|
||||
text = article.content,
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(
|
||||
items = article.relatedArticles,
|
||||
key = RelatedArticleUM::id,
|
||||
) { article ->
|
||||
RelatedNewsItem(
|
||||
relatedArticle = article,
|
||||
modifier = Modifier.fillParentMaxHeight(),
|
||||
.padding(top = 12.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
SpacerH(24.dp)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
SecondaryTangemButton(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = resourceReference(R.string.news_like),
|
||||
size = com.tangem.core.ui.ds.button.TangemButtonSize.X9,
|
||||
iconRes = if (article.isLiked) {
|
||||
R.drawable.ic_like_20
|
||||
} else {
|
||||
R.drawable.ic_heart_20
|
||||
},
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLikeClick()
|
||||
},
|
||||
shape = TangemButtonShape.Rounded,
|
||||
)
|
||||
|
||||
RelatedTokensBlock(
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
onItemClick = when (relatedTokensUM) {
|
||||
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
|
||||
else -> null
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
SpacerH(24.dp)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_sources),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BottomFadeWithBlur(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.height(80.dp)
|
||||
.fillMaxWidth(),
|
||||
backgroundColor = background,
|
||||
)
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
item("relatedArticles") {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(
|
||||
items = article.relatedArticles,
|
||||
key = RelatedArticleUM::id,
|
||||
) { article ->
|
||||
RelatedNewsItem(
|
||||
relatedArticle = article,
|
||||
modifier = Modifier.fillParentMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BottomFadeWithBlur(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.height(80.dp)
|
||||
.fillMaxWidth(),
|
||||
backgroundColor = background,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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