diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 031784429c..f32af0c735 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -41,7 +41,7 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetTokenQuotesUseCase { - return GetTokenQuotesUseCase(quotesRepository = quotesRepository) + fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase { + return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt new file mode 100644 index 0000000000..6a337dae15 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.tokens + +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.res.TangemTheme + +/** + * Text view for token price. + * + * @param price Price of the token. + * @param priceChangeType Type of the price change. + */ +@Composable +fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { + val growColor = TangemTheme.colors.text.accent + val fallColor = TangemTheme.colors.text.warning + val generalColor = TangemTheme.colors.text.primary1 + + val color = remember(generalColor) { Animatable(generalColor) } + var animationSkipped by remember { mutableStateOf(false) } + + LaunchedEffect(price) { + if (animationSkipped.not()) { + animationSkipped = true + return@LaunchedEffect + } + + if (priceChangeType != null) { + val nextColor = when (priceChangeType) { + PriceChangeType.UP, + -> growColor + PriceChangeType.DOWN -> fallColor + PriceChangeType.NEUTRAL -> return@LaunchedEffect + } + + color.animateTo(nextColor, snap()) + color.animateTo(generalColor, tween(durationMillis = 500)) + } + } + + Text( + modifier = modifier, + text = price, + color = color.value, + maxLines = 1, + style = TangemTheme.typography.body2, + overflow = TextOverflow.Visible, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index ef485e1829..21b38fdfb0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -67,7 +67,16 @@ data class TokenMarketInfoResponse( val buyPressureChange: Change?, @Json(name = "experienced_buyer_change") val experiencedBuyerChange: Change?, - ) + @Json(name = "networks") + val sourceNetworks: List?, + ) { + data class SourceNetwork( + @Json(name = "network_id") + val id: String, + @Json(name = "network_name") + val name: String, + ) + } data class Change( @Json(name = "24h") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 3de373a0e3..9863c30ba0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController @@ -69,6 +70,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie textStyle = TangemTheme.typography.body2.copy( color = TangemTheme.colors.text.primary1, ), + cursorBrush = SolidColor(TangemTheme.colors.icon.primary1), decorationBox = @Composable { innerTextField -> DecorationBox( state = state, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 91ef672575..fc916ea18a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -336,6 +336,15 @@ object BigDecimalFormatter { ): String { if (amount == null) return EMPTY_BALANCE_SIGN + if (amount < BigDecimal.ONE) { + return formatFiatPriceUncapped( + fiatAmount = amount, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) + } + val rawAmount = formatCompactAmount( amount = amount, locale = locale, diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 15d69db683..b2d8906fb0 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -9,4 +9,5 @@ object StringsSigns { const val DASH_SIGN = "—" const val LOWER_SIGN = "<" const val TILDE_SIGN = "~" + const val NON_BREAKING_SPACE = '\u00A0' } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt index e7f3fbfe45..710a982835 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt @@ -7,8 +7,10 @@ import kotlinx.coroutines.yield import timber.log.Timber import kotlin.coroutines.cancellation.CancellationException -@Suppress("UnconditionalJumpStatementInLoop") -suspend fun retryOnError(priority: Boolean = false, call: suspend () -> T): T { +@Suppress("UnconditionalJumpStatementInLoop", "MagicNumber") +suspend fun retryOnError(priority: Boolean = false, startRetryDelay: Int = 500, call: suspend () -> T): T { + var currentDelay = startRetryDelay + var priorityCounter = 5 while (true) { return try { call() @@ -19,9 +21,12 @@ suspend fun retryOnError(priority: Boolean = false, call: suspend () -> T): Timber.e(e, "Error occurred during retryOnError block") - if (priority.not()) { + if (priority && priorityCounter > 0) { + --priorityCounter + } else { yield() - delay(timeMillis = 500) + delay(timeMillis = currentDelay.toLong()) + currentDelay *= 2 } continue diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index e9314b0d7e..d77d7618fe 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -57,6 +57,7 @@ internal object TokenMarketInfoConverter : Converter): Flow> { + override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { return appPreferencesStore.getObject( key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, ) .distinctUntilChanged() .filterNotNull() .flatMapLatest { appCurrency -> - fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false) + fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh) quotesStore.get(currenciesIds).map(quotesConverter::convertSet) } .cancellable() diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt index 7fe496e5f1..6f02eb0639 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -27,7 +27,13 @@ data class TokenMarketInfo( val liquidityChange: Change?, val buyPressureChange: Change?, val experiencedBuyerChange: Change?, - ) + val sourceNetworks: List, + ) { + data class SourceNetwork( + val id: String, + val name: String, + ) + } data class Change( val day: BigDecimal?, diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt new file mode 100644 index 0000000000..1ec184fbdc --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.markets + +import arrow.core.* +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.repository.QuotesRepository +import kotlinx.coroutines.flow.* + +@Suppress("UnusedPrivateMember") +class GetCurrencyQuotesUseCase( + private val quotesRepository: QuotesRepository, +) { + // TODO apply interval parameter [REDACTED_TASK_KEY] + operator fun invoke( + currencyID: CryptoCurrency.ID, + interval: PriceChangeInterval, + refresh: Boolean, + ): Flow> { + return quotesRepository.getQuotesUpdates( + currenciesIds = setOf(currencyID), + refresh = refresh, + ).map { it.firstOrNull().toOption() }.catch { emit(None) } + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt deleted file mode 100644 index 059c80ac82..0000000000 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain.markets - -import arrow.core.Either -import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.repository.QuotesRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -@Suppress("UnusedPrivateMember") -class GetTokenQuotesUseCase( - private val quotesRepository: QuotesRepository, -) { - operator fun invoke(tokenId: String, interval: PriceChangeInterval): Flow> { - return flowOf( - Either.catch { - Quote( - rawCurrencyId = "USD", - fiatRate = 100.toBigDecimal(), // mock - priceChange = 10.toBigDecimal(), // mock - ) - }.mapLeft {}, - ) - // TODO implement quotes fetching from repository [REDACTED_TASK_KEY] - // quotesRepository.getQuotesUpdates() - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 4ebd284421..57a8429697 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -12,12 +12,12 @@ interface QuotesRepository { /** * Retrieves updates of quotes for a set of specified cryptocurrencies, identified by their unique IDs. * - * Loads remote quotes if they have expired. + * Loads remote quotes if they have expired or if [refresh] is `true`. * * @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ - fun getQuotesUpdates(currenciesIds: Set): Flow> + fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean = false): Flow> /** * Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index 3d45f98879..393432cf16 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -13,7 +13,7 @@ internal class MockQuotesRepository( private val quotes: Flow>>, ) : QuotesRepository { - override fun getQuotesUpdates(currenciesIds: Set): Flow> { + override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { return quotes.map { it.getOrElse { e -> throw e } } } diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt index dbd1b4f877..9cc202bdb4 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.token.block import androidx.compose.runtime.Stable import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.serialization.Serializable @Stable @@ -10,6 +11,7 @@ interface TokenMarketBlockComponent : ComposableContentComponent { @Serializable data class Params( + val cryptoCurrencyID: CryptoCurrency.ID, val tokenId: String, val tokenName: String, val tokenSymbol: String, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 41a616a52f..3f01b427d9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -13,7 +13,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter @@ -24,6 +23,10 @@ import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter import com.tangem.features.markets.details.impl.model.formatter.* +import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice +import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween +import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval +import com.tangem.features.markets.details.impl.model.state.QuotesStateUpdater import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM @@ -76,6 +79,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( urlOpener.openUrl(it.url) }, ) + private val descriptionConverter = DescriptionConverter( onReadModeClicked = { showInfoBottomSheet(it) @@ -95,7 +99,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( BigDecimalFormatter.formatFiatPriceUncapped( fiatAmount = value, fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = "", + fiatCurrencySymbol = currentAppCurrency.value.symbol, ) }, ) @@ -115,7 +119,8 @@ internal class MarketsTokenDetailsModel @Inject constructor( ), ) - private var lastUpdatedTimestamp: Long = DateTime.now().millis + private val currentTokenInfo = MutableStateFlow(null) + private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis) val isVisibleOnScreen = MutableStateFlow(false) val networksState = MutableStateFlow(TokenNetworksState.Loading) @@ -155,6 +160,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( ), ) + private val quotesStateUpdater = QuotesStateUpdater( + currentAppCurrency = Provider { currentAppCurrency.value }, + state = state, + currentQuotes = currentQuotes, + lastUpdatedTimestamp = lastUpdatedTimestamp, + currentTokenInfo = currentTokenInfo, + ) + private val loadChartJobHolder = JobHolder() init { @@ -279,41 +292,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) tokenMarketInfo.fold( - ifRight = { result -> - currentQuotes.value = result.quotes - val percent = result.quotes.getPercentByInterval(interval = state.value.selectedInterval) - state.update { - it.copy( - priceText = result.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), - priceChangePercentText = result.quotes.getFormattedPercentByInterval( - interval = it.selectedInterval, - ), - priceChangeType = percent.percentChangeType(), - body = MarketsTokenDetailsUM.Body.Content( - description = descriptionConverter.convert(result), - infoBlocks = infoConverter.convert(result), - ), - ) - } - - val networks = result.networks?.filter { - BlockchainUtils.isSupportedNetworkId(it.networkId) - } - - networksState.value = if (networks.isNullOrEmpty()) { - TokenNetworksState.NoNetworksAvailable - } else { - TokenNetworksState.NetworksAvailable(networks) - } - - chartDataProducer.runTransaction { - updateLook { - it.copy( - type = getChartTypeByPercent(percent), - ) - } - } - }, + ifRight = { result -> updateInfo(result) }, ifLeft = { state.update { if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) { @@ -333,42 +312,50 @@ internal class MarketsTokenDetailsModel @Inject constructor( } } - private suspend fun updateQuotes(newQuotes: TokenQuotes) { - val triggerPriceChangeType = getFormattedPriceChange( - currentPrice = currentQuotes.value.currentPrice, - updatedPrice = newQuotes.currentPrice, - ) - val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { - triggeredEvent( - data = triggerPriceChangeType, - onConsume = { - state.update { it.copy(triggerPriceChange = consumedEvent()) } - }, + private fun updateInfo(newInfo: TokenMarketInfo) { + lastUpdatedTimestamp.value = DateTime.now().millis + + currentTokenInfo.value = newInfo + currentQuotes.value = newInfo.quotes + + val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval) + state.update { + it.copy( + priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( + interval = it.selectedInterval, + ), + priceChangeType = percent.percentChangeType(), + body = MarketsTokenDetailsUM.Body.Content( + description = descriptionConverter.convert(newInfo), + infoBlocks = infoConverter.convert(newInfo), + ), ) - } else { - consumedEvent() } + val networks = newInfo.networks?.filter { + BlockchainUtils.isSupportedNetworkId(it.networkId) + } + + networksState.value = if (networks.isNullOrEmpty()) { + TokenNetworksState.NoNetworksAvailable + } else { + TokenNetworksState.NetworksAvailable(networks) + } + + chartDataProducer.runTransaction { + updateLook { + it.copy( + type = getChartTypeByPercent(percent), + ) + } + } + } + + private suspend fun updateQuotes(newQuotes: TokenQuotes) { + quotesStateUpdater.updateQuotes(newQuotes) + val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) - val priceChangeType = percent.percentChangeType() - - // wait until marker is removed - state.first { it.markerSet.not() } - - currentQuotes.value = newQuotes - lastUpdatedTimestamp = DateTime.now().millis - - state.update { stateToUpdate -> - stateToUpdate.copy( - priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency.value), - priceChangePercentText = newQuotes.getFormattedPercentByInterval( - interval = stateToUpdate.selectedInterval, - ), - priceChangeType = priceChangeType, - triggerPriceChange = trigger, - dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval), - ) - } chartDataProducer.runTransaction { updateLook { @@ -489,9 +476,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( launch { while (true) { delay(timeMillis) - // Update quotes only when the container bottom sheet is in the expanded state - - // and is visible on the screen + // Update quotes only when content visible on the screen isVisibleOnScreen.first { it } loadQuotes() @@ -504,7 +489,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( interval = interval, startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( interval = interval, - currentTimestamp = lastUpdatedTimestamp, + currentTimestamp = lastUpdatedTimestamp.value, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt index 9bbcff51d9..376bf704a0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt @@ -2,7 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo @@ -14,7 +14,7 @@ import com.tangem.utils.Provider import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @Stable @@ -48,7 +48,10 @@ internal class InsightsConverter( onInfoClick( InfoBottomSheetContent( title = resourceReference(R.string.markets_token_details_insights), - body = stringReference("//TODO"), + body = resourceReference( + R.string.markets_insights_info_description_message, + wrappedList(value.sourceNetworks.joinToString { it.name }), + ), ), ) }, @@ -62,74 +65,79 @@ internal class InsightsConverter( liquidityChange: BigDecimal?, buyPressureChange: BigDecimal?, ): ImmutableList { - return persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = experiencedBuyerChange.convertChange(), - change = experiencedBuyerChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - body = resourceReference(R.string.markets_token_details_experienced_buyers_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = buyPressureChange.convertChange(isFiatValue = true), - change = buyPressureChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_buy_pressure), - body = resourceReference(R.string.markets_token_details_buy_pressure_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = holdersChange.convertChange(), - change = holdersChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_holders), - body = resourceReference(R.string.markets_token_details_holders_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = liquidityChange.convertChange(), - change = liquidityChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_liquidity), - body = resourceReference(R.string.markets_token_details_liquidity_description), - ), - ) - }, - ), - ) + return listOfNotNull( + experiencedBuyerChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = experiencedBuyerChange.convertChange(), + change = experiencedBuyerChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + body = resourceReference(R.string.markets_token_details_experienced_buyers_description), + ), + ) + }, + ) + }, + buyPressureChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = buyPressureChange.convertChange(isFiatValue = true), + change = buyPressureChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_buy_pressure), + body = resourceReference(R.string.markets_token_details_buy_pressure_description), + ), + ) + }, + ) + }, + holdersChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = holdersChange.convertChange(), + change = holdersChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_holders), + body = resourceReference(R.string.markets_token_details_holders_description), + ), + ) + }, + ) + }, + liquidityChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = liquidityChange.convertChange(), + change = liquidityChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_liquidity), + body = resourceReference(R.string.markets_token_details_liquidity_description), + ), + ) + }, + ) + }, + ).toImmutableList() } - private fun BigDecimal?.changeType(): InfoPointUM.ChangeType? { + private fun BigDecimal.changeType(): InfoPointUM.ChangeType? { return when { - this == null -> null this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN else -> null } } - private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String { - if (this == null) return StringsSigns.DASH_SIGN - + private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String { val value = if (isFiatValue) { val currency = appCurrency() BigDecimalFormatter.formatCompactFiatAmount( @@ -141,11 +149,9 @@ internal class InsightsConverter( BigDecimalFormatter.formatCompactAmount(amount = this.abs()) } - val spacing = if (isFiatValue) " " else "" - return when { - this > BigDecimal.ZERO -> StringsSigns.PLUS + spacing + value - this < BigDecimal.ZERO -> StringsSigns.MINUS + spacing + value + this > BigDecimal.ZERO -> StringsSigns.PLUS + value + this < BigDecimal.ZERO -> StringsSigns.MINUS + value this == BigDecimal.ZERO -> value else -> StringsSigns.DASH_SIGN } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt index 4bf9f1e111..c6c57682f7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt @@ -14,9 +14,6 @@ import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -import java.math.RoundingMode -import java.text.NumberFormat -import java.util.Locale @Stable internal class MetricsConverter( @@ -116,19 +113,15 @@ internal class MetricsConverter( private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { return if (crypto) { - val formatter = NumberFormat.getNumberInstance(Locale.getDefault()).apply { - maximumFractionDigits = 0 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - formatter.format(this) + BigDecimalFormatter.formatCompactAmount( + amount = this ?: BigDecimal.ZERO, + ) } else { val currency = appCurrency() - BigDecimalFormatter.formatFiatAmount( - fiatAmount = this, + BigDecimalFormatter.formatCompactFiatAmount( + amount = this, fiatCurrencyCode = currency.code, fiatCurrencySymbol = currency.symbol, - decimals = 0, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt index 0ccf01cde7..38ee8682bf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt @@ -7,24 +7,23 @@ import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM import com.tangem.utils.Provider import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter import java.math.BigDecimal import java.math.RoundingMode @Stable internal class PricePerformanceConverter( private val appCurrency: Provider, -) : Converter { +) { - override fun convert(value: TokenMarketInfo.PricePerformance): PricePerformanceUM { + fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM { return PricePerformanceUM( - h24 = value.day.convert(), - month = value.month.convert(), - all = value.allTime.convert(), + h24 = value.day.convert(currentPrice), + month = value.month.convert(currentPrice), + all = value.allTime.convert(currentPrice), ) } - private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value { + private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value { if (this == null) { return PricePerformanceUM.Value( low = StringsSigns.DASH_SIGN, @@ -36,7 +35,7 @@ internal class PricePerformanceConverter( return PricePerformanceUM.Value( low = low.convert(), high = high.convert(), - indicatorFraction = calculateFraction(), + indicatorFraction = calculateFraction(currentPrice), ) } @@ -50,10 +49,15 @@ internal class PricePerformanceConverter( ) } - private fun TokenMarketInfo.Range.calculateFraction(): Float { - if (low == null || high == null || low == BigDecimal.ZERO) return 0f - return (high!! - low!!).divide(low!!, RoundingMode.HALF_UP) - .setScale(2, RoundingMode.HALF_UP) - .toFloat().coerceAtMost(1f) + private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float { + return when { + low == null || high == null || high == BigDecimal.ZERO || currentPrice < low -> 0f + currentPrice > high || low == high -> 1f + else -> { + (currentPrice - low!!).divide(high!! - low!!, RoundingMode.HALF_UP) + .setScale(2, RoundingMode.HALF_UP) + .toFloat().coerceAtMost(1f) + } + } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt index f03cbff9c4..e90ad2b0d3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt @@ -17,6 +17,9 @@ internal class TokenMarketInfoConverter( ) : Converter { private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) + + @Suppress("UnusedPrivateMember") + // TODO second markets iteration private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick) private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency) @@ -25,9 +28,14 @@ internal class TokenMarketInfoConverter( override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { return MarketsTokenDetailsUM.InformationBlocks( insights = value.insights?.let { insightsConverter.convert(it) }, - securityScore = securityScoreConverter.convert(Unit), + securityScore = null, metrics = value.metrics?.let { metricsConverter.convert(it) }, - pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) }, + pricePerformance = value.pricePerformance?.let { + pricePerformanceConverter.convert( + value = it, + currentPrice = value.quotes.currentPrice, + ) + }, links = value.links?.let { linksConverter.convert(it) }, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt index acc0ec44fa..5abc296917 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt @@ -23,7 +23,7 @@ internal object MarketsDateTimeFormatters { private val dateFormatter = DateTimeFormatters.dateDDMMYYYY - internal fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { + fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { return when (interval) { PriceChangeInterval.H24 -> { value: BigDecimal -> value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter) @@ -44,7 +44,7 @@ internal object MarketsDateTimeFormatters { } } - internal fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { + fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { return when (interval) { PriceChangeInterval.H24 -> resourceReference(R.string.common_today) PriceChangeInterval.WEEK, @@ -78,10 +78,7 @@ internal object MarketsDateTimeFormatters { } } - internal fun formatDateByIntervalWithMarker( - interval: PriceChangeInterval, - markerTimestamp: BigDecimal, - ): TextReference { + fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference { return when (interval) { PriceChangeInterval.H24, PriceChangeInterval.WEEK, @@ -127,4 +124,14 @@ internal object MarketsDateTimeFormatters { PriceChangeInterval.ALL_TIME -> 0 } } + + fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference { + return formatDateByInterval( + interval = interval, + startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( + interval = interval, + currentTimestamp = currentTimestamp, + ), + ) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt new file mode 100644 index 0000000000..dd664b03ca --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt @@ -0,0 +1,83 @@ +package com.tangem.features.markets.details.impl.model.state + +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenQuotes +import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter +import com.tangem.features.markets.details.impl.model.formatter.* +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM +import com.tangem.utils.Provider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import org.joda.time.DateTime +import java.math.BigDecimal + +internal class QuotesStateUpdater( + private val currentAppCurrency: Provider, + private val state: MutableStateFlow, + private val currentQuotes: MutableStateFlow, + private val lastUpdatedTimestamp: MutableStateFlow, + private val currentTokenInfo: MutableStateFlow, +) { + private val pricePerformanceConverter = PricePerformanceConverter(currentAppCurrency) + + suspend fun updateQuotes(newQuotes: TokenQuotes) { + val triggerPriceChangeType = getFormattedPriceChange( + currentPrice = currentQuotes.value.currentPrice, + updatedPrice = newQuotes.currentPrice, + ) + val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { + triggeredEvent( + data = triggerPriceChangeType, + onConsume = { + state.update { it.copy(triggerPriceChange = consumedEvent()) } + }, + ) + } else { + consumedEvent() + } + + val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) + val priceChangeType = percent.percentChangeType() + + // wait until marker is removed + state.first { it.markerSet.not() } + + currentQuotes.value = newQuotes + lastUpdatedTimestamp.value = DateTime.now().millis + + state.update { stateToUpdate -> + stateToUpdate.copy( + priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()), + priceChangePercentText = newQuotes.getFormattedPercentByInterval( + interval = stateToUpdate.selectedInterval, + ), + priceChangeType = priceChangeType, + triggerPriceChange = trigger, + dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString( + stateToUpdate.selectedInterval, + currentTimestamp = lastUpdatedTimestamp.value, + ), + body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice), + ) + } + } + + private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body { + val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this + + return if (this is MarketsTokenDetailsUM.Body.Content) { + copy( + infoBlocks = infoBlocks.copy( + pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price), + ), + ) + } else { + this + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index e801ed3cb0..23e21f4840 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.details.impl.ui +import android.content.res.Configuration import androidx.compose.animation.Animatable import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween @@ -193,7 +194,7 @@ private fun TokenPriceText( val fallColor = TangemTheme.colors.text.warning val generalColor = TangemTheme.colors.text.primary1 - val color = remember { Animatable(generalColor) } + val color = remember(generalColor) { Animatable(generalColor) } EventEffect(triggerPriceChange) { val nextColor = when (it) { @@ -268,12 +269,11 @@ fun PriceChangeInterval.getText(): TextReference { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview() { TangemThemePreview { - Content( - modifier = Modifier.background(TangemTheme.colors.background.tertiary), - addTopBarStatusBarInsets = false, + MarketsTokenDetailsContent( state = MarketsTokenDetailsUM( tokenName = "Token Name", priceText = "$0.00000000324", @@ -303,6 +303,7 @@ private fun Preview() { onBackClick = {}, backgroundColor = TangemTheme.colors.background.tertiary, portfolioBlock = {}, + addTopBarStatusBarPadding = false, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt index e895b421f5..aca7dd32ae 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer import com.tangem.features.markets.impl.R +import com.tangem.utils.StringsSigns @Composable internal fun Description( @@ -33,7 +34,7 @@ internal fun Description( append(description.resolveReference()) } withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { - append(" " + stringResource(R.string.common_read_more)) + append(" " + stringResource(R.string.common_read_more).replace(' ', StringsSigns.NON_BREAKING_SPACE)) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt index ca7e4d4390..92d905ff2e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -60,6 +60,7 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { title = stringResource(id = R.string.markets_token_details_blockchain_site), links = state.blockchainSite, onLinkClick = state.onLinkClick, + lastBlock = true, ) } }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index 66a09fb533..057d1bbed6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -124,15 +124,16 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi ) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( + modifier = Modifier.weight(1f), text = state.low, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, ) - SpacerW8() Text( + modifier = Modifier.weight(1f), text = state.high, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 620e182622..5d9e93fd4e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -126,9 +126,10 @@ private fun LazyListScope.loadingInfoBlocks() { InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) } - item("securityScore-loading") { - SecurityScorePlaceHolder(modifier = Modifier.blockPaddings()) - } + // TODO second markets iteration + // item("securityScore-loading") { + // SecurityScorePlaceHolder(modifier = Modifier.blockPaddings()) + // } item("metrics-loading") { MetricsBlockPlaceholder(modifier = Modifier.blockPaddings()) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt index b1982093d4..8b28533fb3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.markets.details.impl.ui.state -import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.ImmutableList internal data class MetricsUM( - val metrics: PersistentList, + val metrics: ImmutableList, ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt index dccea6ad25..5f30e89585 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt @@ -30,7 +30,6 @@ internal fun EntryBottomSheetContent( modifier: Modifier = Modifier, ) { val primary = TangemTheme.colors.background.primary - val secondary = TangemTheme.colors.background.secondary val backgroundColor = remember { Animatable(primary) } LocalMainBottomSheetColor.current.value = backgroundColor.value @@ -73,7 +72,7 @@ private fun BackgroundColorEffects( bottomSheetState: State, ) { val primary = TangemTheme.colors.background.primary - val secondary = TangemTheme.colors.background.secondary + val tertiary = TangemTheme.colors.background.tertiary // Order of LaunchedEffects is important here @@ -81,7 +80,7 @@ private fun BackgroundColorEffects( when (activeChild) { is MarketsEntryChildFactory.Child.TokenDetails -> { backgroundColor.animateTo( - secondary, + tertiary, animationSpec = tween(durationMillis = 500), ) } @@ -99,7 +98,7 @@ private fun BackgroundColorEffects( when (bottomSheetState.value) { BottomSheetState.EXPANDED -> { backgroundColor.animateTo( - secondary, + tertiary, animationSpec = tween(durationMillis = 100), ) } @@ -113,12 +112,12 @@ private fun BackgroundColorEffects( } } - LaunchedEffect(primary, secondary) { + LaunchedEffect(primary, tertiary) { if (backgroundColor.isRunning) return@LaunchedEffect when (activeChild) { is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.snapTo(secondary) + backgroundColor.snapTo(tertiary) } MarketsEntryChildFactory.Child.TokenList -> { backgroundColor.snapTo(primary) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 42184de78e..5967eaa915 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -17,6 +17,8 @@ import com.tangem.domain.markets.* import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -29,7 +31,7 @@ internal class TokenMarketBlockModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, - private val getTokenQuotesUseCase: GetTokenQuotesUseCase, + private val getTokenQuotesUseCase: GetCurrencyQuotesUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : Model() { @@ -46,6 +48,7 @@ internal class TokenMarketBlockModel @Inject constructor( ) private var quotesState: QuotesState? = null + private val quotesUpdateJobHolder = JobHolder() val state = MutableStateFlow( TokenMarketBlockUM( @@ -65,10 +68,11 @@ internal class TokenMarketBlockModel @Inject constructor( private fun startFetching() { modelScope.launch { getTokenQuotesUseCase( - tokenId = params.tokenId, + currencyID = params.cryptoCurrencyID, interval = PriceChangeInterval.H24, + refresh = true, ).collect { - it.onRight { res -> + it.onSome { res -> quotesState = QuotesState( currentPrice = res.fiatRate, h24Percent = res.priceChange, @@ -90,7 +94,7 @@ internal class TokenMarketBlockModel @Inject constructor( ) } } - } + }.saveIn(quotesUpdateJobHolder) modelScope.launch(dispatchers.main) { val result = getTokenPriceChartUseCase( @@ -131,7 +135,6 @@ internal class TokenMarketBlockModel @Inject constructor( ), ) - // FIXME navigation crash [REDACTED_TASK_KEY] router.push( AppRoute.MarketsTokenDetails( token = tokenParam, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 67e5d7305e..8191f0addf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.charts.MarketChartMini import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.tokens.TokenPriceText import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.TextShimmer @@ -83,19 +84,21 @@ private fun LeftSide( FlowRow( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { - Text( - text = priceText, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, + TokenPriceText( + modifier = Modifier.alignByBaseline(), + price = priceText, + priceChangeType = type, ) Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { PriceChangeInPercent( + modifier = Modifier.alignByBaseline(), valueInPercent = percentText, type = type, ) Text( + modifier = Modifier.alignByBaseline(), text = stringResource(id = R.string.wallet_marketprice_block_update_time), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index 50170ce417..f22b26c622 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -192,7 +192,7 @@ internal class MarketsListUMStateManager( private fun state(): MarketsListUM = MarketsListUM( list = ListUM.Loading, searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + placeholderText = resourceReference(R.string.common_search), query = "", onQueryChange = { searchQuery = it }, isActive = false, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index e7164e73ab..fa2964129f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -256,7 +256,7 @@ private fun Preview() { onItemClick = {}, ), searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + placeholderText = resourceReference(R.string.common_search), query = "", onQueryChange = {}, isActive = false, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index 7658893fb7..c153024da0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -1,185 +1,46 @@ package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.IntOffset import com.tangem.common.ui.charts.MarketChartMini import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.tokens.TokenPriceText import com.tangem.core.ui.components.* 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.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.features.markets.impl.R -import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.utils.StringsSigns.MINUS -import kotlinx.coroutines.launch -import kotlin.math.roundToInt import kotlin.random.Random -internal enum class DragValue { Start, End } - -const val SWIPE_THRESHOLD_PERCENT = 0.8f -const val SWIPE_VELOCITY_THRESHOLD = 20f - -@Suppress("LongMethod") -@OptIn(ExperimentalFoundationApi::class) @Composable -fun MarketsListItem( - model: MarketsListItemUM, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, - onSwipeToAction: () -> Unit = {}, -) { - val actionWidth = TangemTheme.dimens.size68 - val actionWidthPx = with(LocalDensity.current) { actionWidth.toPx() } - - val hapticManager = LocalHapticManager.current - - val anchors = DraggableAnchors { - DragValue.Start at 0f - DragValue.End at -actionWidthPx - } - val state = remember { - AnchoredDraggableState( - initialValue = DragValue.Start, - anchors = anchors, - positionalThreshold = { it * (1 - SWIPE_THRESHOLD_PERCENT) }, - velocityThreshold = { SWIPE_VELOCITY_THRESHOLD }, - animationSpec = tween(easing = FastOutSlowInEasing), - confirmValueChange = { it == DragValue.Start }, - ) - } - val dragInteractionSource = remember { MutableInteractionSource() } - val clickInteractionSource = remember { MutableInteractionSource() } - val isInDraggedState by dragInteractionSource.collectIsDraggedAsState() - - LaunchedEffect(Unit) { - var actionPerformed = false - var releasePerformed = true - launch { - snapshotFlow { state.offset } - .collect { - val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT - if (it < border && actionPerformed.not()) { - hapticManager.perform(TangemHapticEffect.View.GestureThresholdActivate) - actionPerformed = true - releasePerformed = false - } - - if (it > border) { - if (releasePerformed.not()) { - hapticManager.perform(TangemHapticEffect.View.GestureThresholdDeactivate) - releasePerformed = true - } - actionPerformed = false - } - } - } - launch { - snapshotFlow { isInDraggedState } - .collect { - if (it.not() && actionPerformed) { - releasePerformed = true - onSwipeToAction() - } - } - } - } - - Box( - modifier = Modifier - .height(intrinsicSize = IntrinsicSize.Min) - .fillMaxWidth(), - ) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .fillMaxHeight() - .offset { - IntOffset( - x = actionWidthPx.roundToInt() + - state - .requireOffset() - .toInt(), - y = 0, - ) - } - .width(actionWidth) - .background(TangemTheme.colors.control.checked), - contentAlignment = Alignment.Center, - ) { - Image( - modifier = Modifier.size(TangemTheme.dimens.size28), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_mini_28), - colorFilter = ColorFilter.tint(TangemTheme.colors.icon.primary2), - contentDescription = null, - ) - } - - Box( - modifier = modifier - .align(Alignment.CenterStart) - .clip(RectangleShape) - .offset { - IntOffset( - x = state - .requireOffset() - .toInt(), - y = 0, - ) - } - .anchoredDraggable( - state = state, - orientation = Orientation.Horizontal, - interactionSource = dragInteractionSource, - ) - .clickable( - enabled = true, - interactionSource = clickInteractionSource, - indication = rememberRipple(), - onClick = onClick, - ), - ) { - MarketsListItemContent(model = model) - } - } +internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { + MarketsListItemContent( + modifier = modifier + .fillMaxWidth() + .clip(RectangleShape) + .clickable(onClick = onClick), + model = model, + ) } @Composable @@ -325,38 +186,6 @@ private fun RowScope.TokenMarketCapText(text: String) { ) } -@Composable -private fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { - val growColor = TangemTheme.colors.text.accent - val fallColor = TangemTheme.colors.text.warning - val generalColor = TangemTheme.colors.text.primary1 - - val color = remember { Animatable(generalColor) } - - LaunchedEffect(price) { - if (priceChangeType != null) { - val nextColor = when (priceChangeType) { - PriceChangeType.UP, - -> growColor - PriceChangeType.DOWN -> fallColor - PriceChangeType.NEUTRAL -> return@LaunchedEffect - } - - color.animateTo(nextColor, snap()) - color.animateTo(generalColor, tween(durationMillis = 500)) - } - } - - Text( - modifier = modifier, - text = price, - color = color.value, - maxLines = 1, - style = TangemTheme.typography.body2, - overflow = TextOverflow.Visible, - ) -} - @Composable private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) { val chartWidth = TangemTheme.dimens.size56 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index 9385875fb1..c942539d2f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -88,6 +88,7 @@ internal class TokenDetailsFragment : ComposeFragment() { val tokenId = id.rawCurrencyId ?: return null // token price is not available return TokenMarketBlockComponent.Params( + cryptoCurrencyID = id, tokenId = tokenId, tokenName = name, tokenSymbol = symbol,