Updated on 2026-08-14
This commit is contained in:
parent
cef0618791
commit
c6fc619530
76 changed files with 5247 additions and 176 deletions
|
|
@ -16,6 +16,9 @@ import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
|
|||
import com.tangem.features.createwalletstart.CreateWalletStartComponent
|
||||
import com.tangem.features.details.component.DetailsComponent
|
||||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryRoute
|
||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.hotwallet.*
|
||||
import com.tangem.features.kyc.KycComponent
|
||||
|
|
@ -38,10 +41,7 @@ import com.tangem.features.staking.api.StakingComponent
|
|||
import com.tangem.features.swap.SwapComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerOnMain
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerInSettings
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.*
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.wallet.WalletEntryComponent
|
||||
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
|
||||
|
|
@ -121,6 +121,8 @@ internal class ChildFactory @Inject constructor(
|
|||
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
|
||||
private val yieldSupplyActiveComponentFactory: YieldSupplyActiveComponent.Factory,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
) {
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -204,21 +206,39 @@ internal class ChildFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
is AppRoute.MarketsTokenDetails -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = MarketsTokenDetailsComponent.Params(
|
||||
token = route.token,
|
||||
appCurrency = route.appCurrency,
|
||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||
analyticsParams = route.analyticsParams?.let { params ->
|
||||
MarketsTokenDetailsComponent.AnalyticsParams(
|
||||
blockchain = params.blockchain,
|
||||
source = params.source,
|
||||
)
|
||||
},
|
||||
),
|
||||
componentFactory = marketsTokenDetailsComponentFactory,
|
||||
)
|
||||
if (feedFeatureToggle.isFeedEnabled) {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = FeedEntryRoute.MarketTokenDetails(
|
||||
token = route.token,
|
||||
appCurrency = route.appCurrency,
|
||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||
analyticsParams = route.analyticsParams?.let { params ->
|
||||
FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
|
||||
blockchain = params.blockchain,
|
||||
source = params.source,
|
||||
)
|
||||
},
|
||||
),
|
||||
componentFactory = feedEntryComponentFactory,
|
||||
)
|
||||
} else {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = MarketsTokenDetailsComponent.Params(
|
||||
token = route.token,
|
||||
appCurrency = route.appCurrency,
|
||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||
analyticsParams = route.analyticsParams?.let { params ->
|
||||
MarketsTokenDetailsComponent.AnalyticsParams(
|
||||
blockchain = params.blockchain,
|
||||
source = params.source,
|
||||
)
|
||||
},
|
||||
),
|
||||
componentFactory = marketsTokenDetailsComponentFactory,
|
||||
)
|
||||
}
|
||||
}
|
||||
is AppRoute.Onramp -> {
|
||||
createComponentChild(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -85,6 +86,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
|
|||
style = TangemTheme.typography.h3,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
fun AppBarWithBackButtonAndIcon(
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
backButtonEnabled: Boolean = true,
|
||||
endButtonEnabled: Boolean = true,
|
||||
text: String? = null,
|
||||
subtitle: String? = null,
|
||||
@DrawableRes backIconRes: Int? = null,
|
||||
|
|
@ -30,11 +32,13 @@ fun AppBarWithBackButtonAndIcon(
|
|||
startButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = backIconRes ?: R.drawable.ic_back_24,
|
||||
onClicked = onBackClick,
|
||||
isEnabled = backButtonEnabled,
|
||||
),
|
||||
endButton = if (iconRes != null && onIconClick != null) {
|
||||
TopAppBarButtonUM.Icon(
|
||||
iconRes = iconRes,
|
||||
onClicked = onIconClick,
|
||||
isEnabled = endButtonEnabled,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.core.ui.decompose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
/**
|
||||
* An interface describing the UI part of a component for a modular BottomSheet.
|
||||
*
|
||||
* Designed for use in Decompose components. It separates the UI into a title and content,
|
||||
* providing access to the [BottomSheetState] to react to changes in the sheet's state (collapsed/expanded).
|
||||
*/
|
||||
@Stable
|
||||
interface ComposableModularBottomSheetContentComponent {
|
||||
|
||||
/**
|
||||
* Renders the title of the bottom sheet.
|
||||
* @param bottomSheetState The current state of the bottom sheet. This can be used, for example,
|
||||
* to change navigation buttons (e.g., hiding the "Back" button when collapsed).
|
||||
*/
|
||||
@Composable
|
||||
fun Title(bottomSheetState: State<BottomSheetState>)
|
||||
|
||||
/**
|
||||
* Renders the main content of the bottom sheet.
|
||||
* @param bottomSheetState The current state of the bottom sheet. Useful for tracking visibility
|
||||
* (e.g., for analytics or lifecycle effects when the sheet is [BottomSheetState.EXPANDED]).
|
||||
*/
|
||||
@Composable
|
||||
fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier)
|
||||
}
|
||||
|
|
@ -6,10 +6,12 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
@Stable
|
||||
interface FeedEntryComponent {
|
||||
interface FeedEntryComponent : ComposableContentComponent {
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
|
|
@ -18,7 +20,7 @@ interface FeedEntryComponent {
|
|||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext): FeedEntryComponent
|
||||
interface Factory : ComponentFactory<FeedEntryRoute, FeedEntryComponent> {
|
||||
fun create(context: AppComponentContext, entryRoute: FeedEntryRoute?): FeedEntryComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.feed.entry.components
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface FeedEntryRoute {
|
||||
|
||||
@Serializable
|
||||
data class MarketTokenDetails(
|
||||
val token: TokenMarketParams,
|
||||
val appCurrency: AppCurrency,
|
||||
val shouldShowPortfolio: Boolean,
|
||||
val analyticsParams: AnalyticsParams? = null,
|
||||
) : FeedEntryRoute {
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsParams(
|
||||
val blockchain: String?,
|
||||
val source: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object MarketTokenList : FeedEntryRoute
|
||||
}
|
||||
|
|
@ -52,6 +52,8 @@ dependencies {
|
|||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.news)
|
||||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.yieldSupply)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
package com.tangem.features.feed.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.popWhile
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryRoute
|
||||
import com.tangem.features.feed.model.feed.FeedModelClickIntents
|
||||
import com.tangem.features.feed.ui.EntryBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.EntryContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -33,6 +33,9 @@ import dagger.assisted.AssistedInject
|
|||
@Stable
|
||||
internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted entryRoute: FeedEntryRoute?,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val feedEntryChildFactory: FeedEntryChildFactory,
|
||||
) : FeedEntryComponent, AppComponentContext by context {
|
||||
|
||||
|
|
@ -55,6 +58,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
blockchain = null,
|
||||
source = "Market",
|
||||
),
|
||||
onBackClicked = { onChildBack() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -82,23 +86,26 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>> = childStack(
|
||||
key = "main",
|
||||
source = stackNavigation,
|
||||
serializer = FeedEntryChildFactory.Child.serializer(),
|
||||
initialConfiguration = FeedEntryChildFactory.Child.Feed,
|
||||
handleBackButton = false,
|
||||
childFactory = { configuration, factoryContext ->
|
||||
feedEntryChildFactory.createChild(
|
||||
child = configuration,
|
||||
appComponentContext = childByContext(
|
||||
componentContext = factoryContext,
|
||||
router = innerRouter,
|
||||
),
|
||||
feedEntryClickIntents = clickIntents,
|
||||
)
|
||||
},
|
||||
)
|
||||
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>> =
|
||||
childStack(
|
||||
key = "main",
|
||||
source = stackNavigation,
|
||||
serializer = FeedEntryChildFactory.Child.serializer(),
|
||||
initialConfiguration = mapEntryRouteToChild(entryRoute),
|
||||
handleBackButton = false,
|
||||
childFactory = { configuration, factoryContext ->
|
||||
feedEntryChildFactory.createChild(
|
||||
child = configuration,
|
||||
appComponentContext = childByContext(
|
||||
componentContext = factoryContext,
|
||||
router = innerRouter,
|
||||
),
|
||||
feedEntryClickIntents = clickIntents,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
|
|
@ -106,27 +113,73 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val stackState by stack.subscribeAsState()
|
||||
|
||||
BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) {
|
||||
onChildBack()
|
||||
}
|
||||
|
||||
EntryBottomSheetContent(
|
||||
stackState = stackState,
|
||||
EntryContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
stackState = stack.subscribeAsState(),
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
isOpenedInBottomSheet = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val bottomSheetState = remember {
|
||||
derivedStateOf { BottomSheetState.EXPANDED }
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
EntryContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
stackState = stack.subscribeAsState(),
|
||||
onHeaderSizeChange = {},
|
||||
isOpenedInBottomSheet = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
if (stack.value.active.configuration !is FeedEntryChildFactory.Child.Feed) {
|
||||
stackNavigation.popWhile { it != FeedEntryChildFactory.Child.Feed }
|
||||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapEntryRouteToChild(entryRoute: FeedEntryRoute?): FeedEntryChildFactory.Child {
|
||||
return when (entryRoute) {
|
||||
is FeedEntryRoute.MarketTokenDetails -> FeedEntryChildFactory.Child.TokenDetails(
|
||||
params = DefaultMarketsTokenDetailsComponent.Params(
|
||||
token = entryRoute.token,
|
||||
appCurrency = entryRoute.appCurrency,
|
||||
shouldShowPortfolio = entryRoute.shouldShowPortfolio,
|
||||
analyticsParams = entryRoute.analyticsParams?.let { params ->
|
||||
DefaultMarketsTokenDetailsComponent.AnalyticsParams(
|
||||
blockchain = params.blockchain,
|
||||
source = params.source,
|
||||
)
|
||||
},
|
||||
onBackClicked = { router.pop() },
|
||||
),
|
||||
)
|
||||
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
|
||||
DefaultMarketsTokenListComponent.Params(
|
||||
onBackClicked = { router.pop() },
|
||||
onTokenClick = { token, currency -> clickIntents.onMarketItemClick(token, currency) },
|
||||
preselectedSortType = SortByTypeUM.Rating,
|
||||
shouldAlwaysShowSearchBar = false,
|
||||
),
|
||||
)
|
||||
null -> FeedEntryChildFactory.Child.Feed
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : FeedEntryComponent.Factory {
|
||||
override fun create(context: AppComponentContext): DefaultFeedEntryComponent
|
||||
override fun create(context: AppComponentContext, entryRoute: FeedEntryRoute?): DefaultFeedEntryComponent
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.features.feed.components
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
|
|
@ -43,12 +45,16 @@ internal class FeedEntryChildFactory @Inject constructor() {
|
|||
child: Child,
|
||||
appComponentContext: AppComponentContext,
|
||||
feedEntryClickIntents: FeedEntryClickIntents,
|
||||
): ComposableModularContentComponent {
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
): ComposableModularBottomSheetContentComponent {
|
||||
return when (child) {
|
||||
is Child.TokenDetails -> {
|
||||
DefaultMarketsTokenDetailsComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = child.params,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
}
|
||||
is Child.TokenList -> {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
package com.tangem.features.feed.components.feed
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
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.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import com.tangem.features.feed.model.feed.FeedModelClickIntents
|
||||
import com.tangem.features.feed.ui.feed.FeedList
|
||||
|
|
@ -16,18 +18,18 @@ import com.tangem.features.feed.ui.feed.FeedListHeader
|
|||
internal class DefaultFeedComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: FeedParams,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val feedComponentModel = getOrCreateModel<FeedComponentModel, FeedParams>(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by feedComponentModel.state.collectAsStateWithLifecycle()
|
||||
FeedListHeader(state.feedListSearchBar)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
feedComponentModel.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
|
|
@ -42,8 +44,5 @@ internal class DefaultFeedComponent(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() = Unit
|
||||
|
||||
data class FeedParams(val feedClickIntents: FeedModelClickIntents)
|
||||
}
|
||||
|
|
@ -1,28 +1,97 @@
|
|||
package com.tangem.features.feed.components.market.details
|
||||
|
||||
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.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
|
||||
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent
|
||||
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal class DefaultMarketsTokenDetailsComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
val params: Params,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
// TODO add portfolio in migrate [REDACTED_JIRA]
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
// applying l2 compatibility
|
||||
private val updatedParams = params.copy(
|
||||
token = params.token.copy(
|
||||
id = CryptoCurrency.RawID(getTokenIdIfL2Network(params.token.id.value)),
|
||||
),
|
||||
)
|
||||
private val analyticsParams = params.analyticsParams
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
|
||||
|
||||
init {
|
||||
// === Analytics ===
|
||||
if (analyticsParams != null) {
|
||||
analyticsEventHandler.send(
|
||||
MarketDetailsAnalyticsEvent.EventBuilder(
|
||||
token = params.token,
|
||||
).screenOpened(
|
||||
blockchain = analyticsParams.blockchain,
|
||||
source = analyticsParams.source,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
MarketsTokenDetailsTopBar(
|
||||
onBackClick = { params.onBackClicked() },
|
||||
isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
shouldShowPriceSubtitle = state.shouldShowPriceSubtitle,
|
||||
tokenName = state.tokenName,
|
||||
tokenPrice = state.priceText,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
model.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
model.isVisibleOnScreen.value = false
|
||||
}
|
||||
}
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bsState by bottomSheetState
|
||||
LaunchedEffect(bsState) {
|
||||
model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED
|
||||
}
|
||||
|
||||
MarketsTokenDetailsContent(
|
||||
modifier = modifier,
|
||||
backgroundColor = LocalMainBottomSheetColor.current.value,
|
||||
state = state,
|
||||
isAccountEnabled = accountsFeatureToggles.isFeatureEnabled,
|
||||
portfolioBlock = {
|
||||
// TODO add portfolio in migrate [REDACTED_JIRA]
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
@ -31,6 +100,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
val appCurrency: AppCurrency,
|
||||
val shouldShowPortfolio: Boolean,
|
||||
val analyticsParams: AnalyticsParams?,
|
||||
val onBackClicked: () -> Unit,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -1,56 +1,64 @@
|
|||
package com.tangem.features.feed.components.market.list
|
||||
|
||||
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.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.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
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.market.list.MarketsList
|
||||
import com.tangem.features.feed.ui.market.list.TopBarWithSearch
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal class DefaultMarketsTokenListComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: MarketsListModel = getOrCreateModel<MarketsListModel, Params>(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
TopBarWithSearch(
|
||||
onBackClick = params.onBackClicked,
|
||||
onSearchClick = state.onSearchClicked,
|
||||
marketsSearchBar = state.marketsSearchBar,
|
||||
buttonsEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
model.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
model.isVisibleOnScreen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val bsState by bottomSheetState
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(bsState) {
|
||||
model.containerBottomSheetState.value = bsState
|
||||
}
|
||||
|
||||
MarketsList(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() = Unit
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val onBackClicked: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
package com.tangem.features.feed.components.news.details
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
|
||||
internal class DefaultNewsDetailsComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
}
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
}
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {}
|
||||
}
|
||||
|
|
@ -1,23 +1,21 @@
|
|||
package com.tangem.features.feed.components.news.list
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
|
||||
internal class DefaultNewsListComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.feed.di
|
|||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
|
||||
import com.tangem.features.feed.model.market.list.MarketsListModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -23,4 +24,9 @@ internal interface ModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(MarketsListModel::class)
|
||||
fun provideMarketsListModel(model: MarketsListModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(MarketsTokenDetailsModel::class)
|
||||
fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.*
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
|
|||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.model.feed
|
|||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
|
||||
/**
|
||||
* Callback interface for feed model navigation actions.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,653 @@
|
|||
package com.tangem.features.feed.model.market.details
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.details.converter.DescriptionConverter
|
||||
import com.tangem.features.feed.model.market.details.converter.ExchangeItemStateConverter
|
||||
import com.tangem.features.feed.model.market.details.converter.TokenMarketInfoConverter
|
||||
import com.tangem.features.feed.model.market.details.formatter.*
|
||||
import com.tangem.features.feed.model.market.details.state.QuotesStateUpdater
|
||||
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class MarketsTokenDetailsModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getUserCountryUseCase: GetUserCountryUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase,
|
||||
private val getTokenExchangesUseCase: GetTokenExchangesUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model() {
|
||||
|
||||
private val quotesJob = JobHolder()
|
||||
private var userCountry: UserCountry? = null
|
||||
private val params = paramsContainer.require<DefaultMarketsTokenDetailsComponent.Params>()
|
||||
private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token)
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = params.appCurrency,
|
||||
)
|
||||
|
||||
private val infoConverter = TokenMarketInfoConverter(
|
||||
appCurrency = Provider { currentAppCurrency.value },
|
||||
onInfoClick = { showBottomSheet(it) },
|
||||
onListedOnClick = ::onListedOnClick,
|
||||
onLinkClick = { link ->
|
||||
urlOpener.openUrl(link.url)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title))
|
||||
},
|
||||
onSecurityScoreInfoClick = { content ->
|
||||
showBottomSheet(content)
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.securityScoreOpened())
|
||||
},
|
||||
onSecurityScoreProviderLinkClick = { securityScoreProviderUM ->
|
||||
securityScoreProviderUM.urlData?.fullUrl?.let { url ->
|
||||
urlOpener.openUrl(url)
|
||||
}
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.securityScoreProviderClicked(securityScoreProviderUM.name))
|
||||
},
|
||||
// === Analytics ===
|
||||
onPricePerformanceIntervalChanged = { interval ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
},
|
||||
onInsightsIntervalChanged = { interval ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Insights,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
},
|
||||
needApplyFCARestrictions = Provider {
|
||||
userCountry.needApplyFCARestrictions()
|
||||
},
|
||||
// ==================
|
||||
)
|
||||
|
||||
private val descriptionConverter = DescriptionConverter(
|
||||
onReadModeClicked = { content ->
|
||||
showBottomSheet(content)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked())
|
||||
},
|
||||
needApplyFCARestrictions = Provider {
|
||||
userCountry.needApplyFCARestrictions()
|
||||
},
|
||||
onGeneratedAINotificationClick = {
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase(
|
||||
type = FeedbackEmailType.CurrencyDescriptionError(
|
||||
currencyId = params.token.id.value,
|
||||
currencyName = params.token.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
|
||||
updateLook { marketChartLook ->
|
||||
val percentChangeType = params.token.tokenQuotes.h24Percent.percentChangeType()
|
||||
|
||||
marketChartLook.copy(
|
||||
type = percentChangeType.toChartType(),
|
||||
xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24),
|
||||
yAxisFormatter = { value ->
|
||||
value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
).price()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val currentQuotes = MutableStateFlow(
|
||||
TokenQuotes(
|
||||
currentPrice = params.token.tokenQuotes.currentPrice,
|
||||
h24ChangePercent = params.token.tokenQuotes.h24Percent,
|
||||
weekChangePercent = params.token.tokenQuotes.weekPercent,
|
||||
monthChangePercent = params.token.tokenQuotes.monthPercent,
|
||||
m3ChangePercent = null,
|
||||
m6ChangePercent = null,
|
||||
yearChangePercent = null,
|
||||
allTimeChangePercent = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val currentTokenInfo = MutableStateFlow<TokenMarketInfo?>(null)
|
||||
private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis)
|
||||
|
||||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
|
||||
|
||||
val state = MutableStateFlow(
|
||||
MarketsTokenDetailsUM(
|
||||
tokenName = params.token.name,
|
||||
priceText = params.token.tokenQuotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
).price()
|
||||
},
|
||||
dateTimeText = resourceReference(R.string.common_today),
|
||||
priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() },
|
||||
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
|
||||
iconUrl = params.token.imageUrl,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = chartDataProducer,
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = ::onMarkerPointSelected,
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = ::onSelectedIntervalChange,
|
||||
isMarkerSet = false,
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
triggerPriceChange = consumedEvent(),
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
shouldShowPriceSubtitle = false,
|
||||
onShouldShowPriceSubtitleChange = ::onShouldShowPriceSubtitleChange,
|
||||
),
|
||||
)
|
||||
|
||||
private val quotesStateUpdater = QuotesStateUpdater(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
state = state,
|
||||
currentQuotes = currentQuotes,
|
||||
lastUpdatedTimestamp = lastUpdatedTimestamp,
|
||||
currentTokenInfo = currentTokenInfo,
|
||||
onPricePerformanceIntervalChanged = { interval ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private val loadChartJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
|
||||
?: UserCountry.Other(Locale.getDefault().country)
|
||||
// reload screen if currency changed
|
||||
modelScope.launch {
|
||||
currentAppCurrency
|
||||
.filter { it != params.appCurrency }
|
||||
.collectLatest {
|
||||
initialLoad()
|
||||
}
|
||||
}
|
||||
|
||||
initialLoad()
|
||||
}
|
||||
|
||||
private fun initialLoad() {
|
||||
loadInfo()
|
||||
loadChart(state.value.selectedInterval)
|
||||
modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS)
|
||||
}
|
||||
|
||||
private fun loadQuotes() {
|
||||
modelScope.launch {
|
||||
val result = getTokenFullQuotesUseCase(
|
||||
tokenId = params.token.id,
|
||||
appCurrency = currentAppCurrency.value,
|
||||
tokenSymbol = params.token.symbol,
|
||||
)
|
||||
|
||||
result.onRight { res ->
|
||||
updateQuotes(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChart(interval: PriceChangeInterval) {
|
||||
modelScope.launch {
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(
|
||||
chartState = marketsTokenDetailsUM.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
}
|
||||
|
||||
val chart = getTokenPriceChartUseCase.invoke(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
interval = interval,
|
||||
tokenId = params.token.id,
|
||||
tokenSymbol = params.token.symbol,
|
||||
preview = false,
|
||||
)
|
||||
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(
|
||||
selectedInterval = interval,
|
||||
chartState = marketsTokenDetailsUM.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
chart
|
||||
.onRight { updateTokenChart(it) }
|
||||
.onLeft {
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(
|
||||
chartState = marketsTokenDetailsUM.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.ERROR,
|
||||
),
|
||||
body = if (marketsTokenDetailsUM.body is MarketsTokenDetailsUM.Body.Error) {
|
||||
MarketsTokenDetailsUM.Body.Nothing
|
||||
} else {
|
||||
marketsTokenDetailsUM.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.saveIn(loadChartJobHolder)
|
||||
}
|
||||
|
||||
private suspend fun updateTokenChart(tokenChart: TokenChart) {
|
||||
val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval)
|
||||
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data(
|
||||
y = tokenChart.priceY.toImmutableList(),
|
||||
x = tokenChart.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted()
|
||||
|
||||
updateLook { marketChartLook ->
|
||||
marketChartLook.copy(
|
||||
xAxisFormatter = xAxisFormatter,
|
||||
type = state.value.priceChangeType.toChartType(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(
|
||||
chartState = marketsTokenDetailsUM.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.DATA,
|
||||
),
|
||||
body = if (marketsTokenDetailsUM.body is MarketsTokenDetailsUM.Body.Nothing) {
|
||||
MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked)
|
||||
} else {
|
||||
marketsTokenDetailsUM.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadInfo() {
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
val tokenMarketInfo = getTokenMarketInfoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
tokenId = params.token.id,
|
||||
tokenSymbol = params.token.symbol,
|
||||
)
|
||||
|
||||
tokenMarketInfo.fold(
|
||||
ifRight = { result -> updateInfo(result) },
|
||||
ifLeft = {
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
if (marketsTokenDetailsUM.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
marketsTokenDetailsUM.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Error(
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
marketsTokenDetailsUM.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Nothing,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateInfo(newInfo: TokenMarketInfo) {
|
||||
lastUpdatedTimestamp.value = DateTime.now().millis
|
||||
|
||||
currentTokenInfo.value = newInfo
|
||||
currentQuotes.value = newInfo.quotes
|
||||
|
||||
val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(
|
||||
priceText = newInfo.quotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
).price()
|
||||
},
|
||||
priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval(
|
||||
interval = marketsTokenDetailsUM.selectedInterval,
|
||||
),
|
||||
priceChangeType = percent.percentChangeType(),
|
||||
body = MarketsTokenDetailsUM.Body.Content(
|
||||
description = descriptionConverter.convert(newInfo),
|
||||
infoBlocks = infoConverter.convert(newInfo),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val isAllWalletsIsHot = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
|
||||
|
||||
val networks = newInfo.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
blockchainId = network.networkId,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = isAllWalletsIsHot,
|
||||
)
|
||||
}
|
||||
|
||||
networksState.value = if (networks.isNullOrEmpty()) {
|
||||
TokenNetworksState.NoNetworksAvailable
|
||||
} else {
|
||||
TokenNetworksState.NetworksAvailable(networks)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(type = percent.percentChangeType().toChartType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
|
||||
val populatedNewQuotes = currentQuotes.value.populateWith(newQuotes)
|
||||
|
||||
quotesStateUpdater.updateQuotes(newQuotes = populatedNewQuotes)
|
||||
|
||||
val percent = populatedNewQuotes
|
||||
.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(type = percent.percentChangeType().toChartType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
|
||||
if (state.value.selectedInterval == interval) return
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
// ==================
|
||||
|
||||
val quotes = currentQuotes.value
|
||||
val priceChangePercent = quotes.getFormattedPercentByInterval(interval)
|
||||
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(
|
||||
priceChangePercentText = priceChangePercent,
|
||||
selectedInterval = interval,
|
||||
priceChangeType = quotes.getPercentByInterval(interval)?.percentChangeType()
|
||||
?: PriceChangeType.NEUTRAL,
|
||||
dateTimeText = getDefaultDateTimeString(interval),
|
||||
)
|
||||
}
|
||||
|
||||
loadChart(interval)
|
||||
|
||||
if (priceChangePercent.isEmpty()) {
|
||||
loadQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShouldShowPriceSubtitleChange(shouldShow: Boolean) {
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(shouldShowPriceSubtitle = shouldShow)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun onMarkerPointSelected(markerTimestamp: BigDecimal?, price: BigDecimal?) {
|
||||
val currentState = state.value
|
||||
|
||||
val dateTimeText = markerTimestamp?.let { bigDecimal ->
|
||||
MarketsDateTimeFormatters.formatDateByIntervalWithMarker(
|
||||
interval = currentState.selectedInterval,
|
||||
markerTimestamp = bigDecimal,
|
||||
)
|
||||
} ?: getDefaultDateTimeString(currentState.selectedInterval)
|
||||
|
||||
val priceText = (price ?: currentQuotes.value.currentPrice).format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
).price()
|
||||
}
|
||||
|
||||
val percent = price?.let { bigDecimal ->
|
||||
getChangePercentBetween(
|
||||
previousPrice = bigDecimal,
|
||||
currentPrice = currentQuotes.value.currentPrice,
|
||||
)
|
||||
} ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval)
|
||||
|
||||
val percentText = percent?.format { percent() }.orEmpty()
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
isMarkerSet = markerTimestamp != null,
|
||||
dateTimeText = dateTimeText,
|
||||
priceText = priceText,
|
||||
priceChangePercentText = percentText,
|
||||
priceChangeType = percent.percentChangeType(),
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook { marketChartLook ->
|
||||
marketChartLook.copy(
|
||||
type = percent.percentChangeType().toChartType(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBottomSheet(content: TangemBottomSheetConfigContent) {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(
|
||||
isShown = true,
|
||||
onDismissRequest = ::hideBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideBottomSheet() {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLoadRetryClicked() {
|
||||
val currentState = state.value
|
||||
|
||||
if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) {
|
||||
loadChart(currentState.selectedInterval)
|
||||
}
|
||||
|
||||
if (currentState.body is MarketsTokenDetailsUM.Body.Error ||
|
||||
currentState.body is MarketsTokenDetailsUM.Body.Nothing
|
||||
) {
|
||||
loadInfo()
|
||||
modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onListedOnClick(exchangesCount: Int) {
|
||||
modelScope.launch {
|
||||
analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened())
|
||||
|
||||
showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount))
|
||||
|
||||
val maybeExchanges = getTokenExchangesUseCase(tokenId = params.token.id)
|
||||
|
||||
// Delay to show the bottom sheet
|
||||
delay(timeMillis = 400L)
|
||||
|
||||
updateExchangeBSContent(maybeExchanges = maybeExchanges, exchangesCount = exchangesCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExchangeBSContent(
|
||||
maybeExchanges: Either<Throwable, List<TokenMarketExchange>>,
|
||||
exchangesCount: Int,
|
||||
) {
|
||||
val content = maybeExchanges
|
||||
.fold(
|
||||
ifLeft = { _ ->
|
||||
ExchangesBottomSheetContent.Error(onRetryClick = { onListedOnClick(exchangesCount) })
|
||||
},
|
||||
ifRight = { list ->
|
||||
ExchangesBottomSheetContent.Content(
|
||||
exchangeItems = ExchangeItemStateConverter.convertList(list).toImmutableList(),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(content = content),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
|
||||
launch {
|
||||
while (true) {
|
||||
delay(timeMillis)
|
||||
// Update quotes only when content is visible on the screen
|
||||
isVisibleOnScreen.first { it }
|
||||
|
||||
loadQuotes()
|
||||
}
|
||||
}.saveIn(quotesJob)
|
||||
}
|
||||
|
||||
private fun getDefaultDateTimeString(interval: PriceChangeInterval): TextReference {
|
||||
return MarketsDateTimeFormatters.formatDateByInterval(
|
||||
interval = interval,
|
||||
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
|
||||
interval = interval,
|
||||
currentTimestamp = lastUpdatedTimestamp.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.feed.model.market.details.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
|
||||
internal class MarketDetailsAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
|
||||
|
||||
data class EventBuilder(
|
||||
val token: TokenMarketParams,
|
||||
) {
|
||||
fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent(
|
||||
event = "Token Chart Screen Opened",
|
||||
params = buildMap {
|
||||
put("Token", token.symbol)
|
||||
blockchain?.let { put("blockchain", it) }
|
||||
put("Source", source)
|
||||
},
|
||||
)
|
||||
|
||||
fun intervalChanged(intervalType: IntervalType, interval: PriceChangeInterval) = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Period",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
"Period" to interval.toAnalyticsString(),
|
||||
"Source" to intervalType.source,
|
||||
),
|
||||
)
|
||||
|
||||
fun readMoreClicked() = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Read More",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
),
|
||||
)
|
||||
|
||||
fun linkClicked(linkTitle: String) = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Links",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
"Link" to linkTitle,
|
||||
),
|
||||
)
|
||||
|
||||
fun exchangesScreenOpened() = MarketDetailsAnalyticsEvent(
|
||||
event = "Exchanges Screen Opened",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
),
|
||||
)
|
||||
|
||||
fun securityScoreOpened() = MarketDetailsAnalyticsEvent(
|
||||
event = "Security Score Info",
|
||||
params = mapOf("Token" to token.symbol),
|
||||
)
|
||||
|
||||
fun securityScoreProviderClicked(provider: String) = MarketDetailsAnalyticsEvent(
|
||||
event = "Security Score Provider Clicked",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
"Provider" to provider,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
enum class IntervalType(val source: String) {
|
||||
Chart("Chart"),
|
||||
PricePerformance("Price"),
|
||||
Insights("Insights"),
|
||||
}
|
||||
}
|
||||
|
||||
private fun PriceChangeInterval.toAnalyticsString() = when (this) {
|
||||
PriceChangeInterval.H24 -> "24h"
|
||||
PriceChangeInterval.WEEK -> "7d"
|
||||
PriceChangeInterval.MONTH -> "1m"
|
||||
PriceChangeInterval.MONTH3 -> "3m"
|
||||
PriceChangeInterval.MONTH6 -> "6m"
|
||||
PriceChangeInterval.YEAR -> "1y"
|
||||
PriceChangeInterval.ALL_TIME -> "All"
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
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.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Suppress("NestedScopeFunctions")
|
||||
@Stable
|
||||
internal class DescriptionConverter(
|
||||
private val onReadModeClicked: (InfoBottomSheetContent) -> Unit,
|
||||
private val onGeneratedAINotificationClick: () -> Unit,
|
||||
private val needApplyFCARestrictions: Provider<Boolean>,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.Description?> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? {
|
||||
if (needApplyFCARestrictions()) return null
|
||||
return value.shortDescription?.let { desc ->
|
||||
MarketsTokenDetailsUM.Description(
|
||||
shortDescription = stringReference(desc),
|
||||
fullDescription = value.fullDescription?.let { fullDescription ->
|
||||
stringReference(fullDescription)
|
||||
},
|
||||
onReadMoreClick = {
|
||||
onReadModeClicked(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_about_token_title,
|
||||
wrappedList(
|
||||
value.name,
|
||||
),
|
||||
),
|
||||
body = stringReference(value.fullDescription.orEmpty()),
|
||||
generatedAINotificationUM = InfoBottomSheetContent.GeneratedAINotificationUM(
|
||||
onClick = onGeneratedAINotificationClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.feed.model.market.details.converter
|
||||
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.markets.TokenMarketExchange
|
||||
import com.tangem.domain.markets.TokenMarketExchange.TrustScore
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [TokenMarketExchange] to [TokenItemState]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object ExchangeItemStateConverter : Converter<TokenMarketExchange, TokenItemState> {
|
||||
|
||||
override fun convert(value: TokenMarketExchange): TokenItemState {
|
||||
return TokenItemState.Content(
|
||||
id = value.id,
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = value.imageUrl,
|
||||
fallbackResId = R.drawable.ic_alert_24,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value.name)),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(
|
||||
text = value.volumeInUsd.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = "USD",
|
||||
fiatCurrencySymbol = "$",
|
||||
).price()
|
||||
},
|
||||
),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference(value = if (value.isCentralized) "CEX" else "DEX"),
|
||||
),
|
||||
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
|
||||
auditLabelUM = value.trustScore.toAuditLabelUM(),
|
||||
),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TrustScore.toAuditLabelUM(): AuditLabelUM {
|
||||
return when (this) {
|
||||
TrustScore.Risky -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_risky),
|
||||
type = AuditLabelUM.Type.Prohibition,
|
||||
)
|
||||
TrustScore.Caution -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_caution),
|
||||
type = AuditLabelUM.Type.Warning,
|
||||
)
|
||||
TrustScore.Trusted -> AuditLabelUM(
|
||||
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_trusted),
|
||||
type = AuditLabelUM.Type.Permit,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
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.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.rawCompact
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InsightsUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
internal class InsightsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Insights, InsightsUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Insights): InsightsUM {
|
||||
return with(value) {
|
||||
InsightsUM(
|
||||
h24Info = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.day,
|
||||
holdersChange = holdersChange?.day,
|
||||
liquidityChange = liquidityChange?.day,
|
||||
buyPressureChange = buyPressureChange?.day,
|
||||
),
|
||||
weekInfo = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.week,
|
||||
holdersChange = holdersChange?.week,
|
||||
liquidityChange = liquidityChange?.week,
|
||||
buyPressureChange = buyPressureChange?.week,
|
||||
),
|
||||
monthInfo = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.month,
|
||||
holdersChange = holdersChange?.month,
|
||||
liquidityChange = liquidityChange?.month,
|
||||
buyPressureChange = buyPressureChange?.month,
|
||||
),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_insights),
|
||||
body = resourceReference(
|
||||
R.string.markets_insights_info_description_message,
|
||||
wrappedList(value.sourceNetworks.joinToString { it.name }),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onIntervalChanged = onIntervalChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInfoPointList(
|
||||
experiencedBuyerChange: BigDecimal?,
|
||||
holdersChange: BigDecimal?,
|
||||
liquidityChange: BigDecimal?,
|
||||
buyPressureChange: BigDecimal?,
|
||||
): ImmutableList<InfoPointUM> {
|
||||
return listOfNotNull(
|
||||
experiencedBuyerChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = experiencedBuyerChange.convertChange(),
|
||||
change = experiencedBuyerChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers_full),
|
||||
body = resourceReference(R.string.markets_token_details_experienced_buyers_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
buyPressureChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = buyPressureChange.convertChange(isFiatValue = true),
|
||||
change = buyPressureChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure_full),
|
||||
body = resourceReference(R.string.markets_token_details_buy_pressure_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
holdersChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = holdersChange.convertChange(),
|
||||
change = holdersChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_holders_full),
|
||||
body = resourceReference(R.string.markets_token_details_holders_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
liquidityChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = liquidityChange.convertChange(),
|
||||
change = liquidityChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity_full),
|
||||
body = resourceReference(R.string.markets_token_details_liquidity_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
private fun BigDecimal.changeType(): InfoPointUM.ChangeType? {
|
||||
return when {
|
||||
this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP
|
||||
this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String {
|
||||
val value = if (isFiatValue) {
|
||||
this.abs().format {
|
||||
val currency = appCurrency()
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).compact()
|
||||
}
|
||||
} else {
|
||||
this.abs().format {
|
||||
rawCompact()
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
this > BigDecimal.ZERO -> StringsSigns.PLUS + value
|
||||
this < BigDecimal.ZERO -> StringsSigns.MINUS + value
|
||||
this == BigDecimal.ZERO -> value
|
||||
else -> StringsSigns.DASH_SIGN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.features.feed.model.market.details.converter
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.LinksUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Stable
|
||||
internal class LinksConverter(
|
||||
private val onLinkClick: (LinksUM.Link) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Links, LinksUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Links): LinksUM {
|
||||
return LinksUM(
|
||||
officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
social = value.social?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Link.convert(): LinksUM.Link {
|
||||
return LinksUM.Link(
|
||||
title = title,
|
||||
iconRes = getIconById(id),
|
||||
url = link,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconById(id: String?): Int {
|
||||
return when (id) {
|
||||
"linkedin" -> R.drawable.ic_linkedin_24
|
||||
"discord" -> R.drawable.ic_discord_24
|
||||
"youtube" -> R.drawable.ic_youtube_24
|
||||
"telegram" -> R.drawable.ic_telegram_24
|
||||
"github" -> R.drawable.ic_github_24
|
||||
"twitter" -> R.drawable.ic_twitter_24
|
||||
"facebook" -> R.drawable.ic_facebook_24
|
||||
"reddit" -> R.drawable.ic_reddit_24
|
||||
"instagram" -> R.drawable.ic_instagram_24
|
||||
"whitepaper" -> R.drawable.ic_doc_24
|
||||
else -> R.drawable.ic_arrow_top_right_24
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
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.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MetricsUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
internal class MetricsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val tokenSymbol: String,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Metrics, MetricsUM> {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun convert(value: TokenMarketInfo.Metrics): MetricsUM {
|
||||
return with(value) {
|
||||
MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
value = marketCap.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_market_capitalization_full,
|
||||
),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_market_capitalization_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
value = marketRating?.toString() ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating_full),
|
||||
body = resourceReference(R.string.markets_token_details_market_rating_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
value = volume24h.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume_full),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_trading_volume_24h_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
value = fullyDilutedValuation.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_fully_diluted_valuation_full,
|
||||
),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_fully_diluted_valuation_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
value = circulatingSupply.formatAmount(crypto = true),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply_full),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_circulating_supply_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_max_supply),
|
||||
value = maxSupply.formatMaxSupply(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_max_supply_full),
|
||||
body = resourceReference(R.string.markets_token_details_total_supply_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatMaxSupply(): String {
|
||||
when (this) {
|
||||
null -> return StringsSigns.DASH_SIGN
|
||||
BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN
|
||||
}
|
||||
|
||||
return this.formatAmount(crypto = true)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
|
||||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
return if (crypto) {
|
||||
format {
|
||||
crypto(
|
||||
symbol = tokenSymbol,
|
||||
decimals = 2,
|
||||
).compact()
|
||||
}
|
||||
} else {
|
||||
val currency = appCurrency()
|
||||
|
||||
format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).compact()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.features.feed.model.market.details.converter
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.ui.market.detailed.state.PricePerformanceUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
@Stable
|
||||
internal class PricePerformanceConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) {
|
||||
|
||||
fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM {
|
||||
return PricePerformanceUM(
|
||||
h24 = value.day.convert(currentPrice),
|
||||
month = value.month.convert(currentPrice),
|
||||
all = value.allTime.convert(currentPrice),
|
||||
onIntervalChanged = onIntervalChanged,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value {
|
||||
if (this == null || this.low == null || this.high == null) {
|
||||
return PricePerformanceUM.Value(
|
||||
low = StringsSigns.DASH_SIGN,
|
||||
high = StringsSigns.DASH_SIGN,
|
||||
indicatorFraction = 0f,
|
||||
)
|
||||
}
|
||||
|
||||
return PricePerformanceUM.Value(
|
||||
low = low.convert(),
|
||||
high = high.convert(),
|
||||
indicatorFraction = calculateFraction(currentPrice),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.convert(): String {
|
||||
val currency = appCurrency()
|
||||
|
||||
return format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).price()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float {
|
||||
val currentLow = low ?: return 0f
|
||||
val currentHigh = high ?: return 0f
|
||||
return when {
|
||||
high == BigDecimal.ZERO || currentPrice < low -> 0f
|
||||
currentPrice > high || low == high -> 1f
|
||||
else -> {
|
||||
(currentPrice - currentLow).divide(currentHigh - currentLow, RoundingMode.HALF_UP)
|
||||
.setScale(2, RoundingMode.HALF_UP)
|
||||
.toFloat().coerceAtMost(1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.features.feed.model.market.details.converter
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.model.market.details.formatter.MarketsDateTimeFormatters
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
internal class SecurityScoreConverter(
|
||||
private val onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit,
|
||||
private val onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit,
|
||||
) : Converter<TokenMarketInfo.SecurityData, SecurityScoreUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.SecurityData): SecurityScoreUM {
|
||||
val ratingsCount = value.securityScoreProviderData.size
|
||||
return SecurityScoreUM(
|
||||
score = value.totalSecurityScore,
|
||||
description = pluralReference(
|
||||
id = R.plurals.markets_token_details_based_on_ratings,
|
||||
count = ratingsCount,
|
||||
formatArgs = wrappedList(ratingsCount),
|
||||
),
|
||||
onInfoClick = {
|
||||
onSecurityScoreInfoClick(
|
||||
SecurityScoreBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_security_score),
|
||||
description = resourceReference(R.string.markets_token_details_security_score_description),
|
||||
providers = value.securityScoreProviderData.map { securityScoreProvider ->
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = securityScoreProvider.providerName,
|
||||
lastAuditDate = securityScoreProvider.lastAuditDate?.let { date ->
|
||||
MarketsDateTimeFormatters.formatAsDate(date.millis)
|
||||
},
|
||||
score = securityScoreProvider.securityScore,
|
||||
urlData = securityScoreProvider.urlData?.let { urlData ->
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = urlData.fullUrl,
|
||||
rootHost = urlData.rootHost,
|
||||
)
|
||||
},
|
||||
iconUrl = securityScoreProvider.iconUrl,
|
||||
)
|
||||
},
|
||||
onProviderLinkClick = onSecurityScoreProviderLinkClick,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.features.feed.model.market.details.converter
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.ui.market.detailed.state.LinksUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ListedOnUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
@Suppress("LongParameterList")
|
||||
internal class TokenMarketInfoConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val needApplyFCARestrictions: Provider<Boolean>,
|
||||
private val onInfoClick: (TangemBottomSheetConfigContent) -> Unit,
|
||||
private val onListedOnClick: (Int) -> Unit,
|
||||
onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit,
|
||||
onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
onInsightsIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
|
||||
|
||||
private val insightsConverter = InsightsConverter(
|
||||
appCurrency = appCurrency,
|
||||
onInfoClick = onInfoClick,
|
||||
onIntervalChanged = onInsightsIntervalChanged,
|
||||
)
|
||||
|
||||
private val securityScoreConverter = SecurityScoreConverter(
|
||||
onSecurityScoreInfoClick = onSecurityScoreInfoClick,
|
||||
onSecurityScoreProviderLinkClick = onSecurityScoreProviderLinkClick,
|
||||
)
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(
|
||||
appCurrency = appCurrency,
|
||||
onIntervalChanged = onPricePerformanceIntervalChanged,
|
||||
)
|
||||
private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {
|
||||
val metricsConverter = MetricsConverter(
|
||||
tokenSymbol = value.symbol,
|
||||
appCurrency = appCurrency,
|
||||
onInfoClick = onInfoClick,
|
||||
)
|
||||
|
||||
val exchangesAmount = value.exchangesAmount
|
||||
val insights = if (needApplyFCARestrictions()) {
|
||||
null
|
||||
} else {
|
||||
value.insights?.let { insightsConverter.convert(it) }
|
||||
}
|
||||
val securityScore = if (needApplyFCARestrictions()) {
|
||||
null
|
||||
} else {
|
||||
value.securityData?.let { securityScoreConverter.convert(it) }
|
||||
}
|
||||
return MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = insights,
|
||||
securityScore = securityScore,
|
||||
metrics = value.metrics?.let { metricsConverter.convert(it) },
|
||||
pricePerformance = value.pricePerformance?.let { pricePerformance ->
|
||||
pricePerformanceConverter.convert(
|
||||
value = pricePerformance,
|
||||
currentPrice = value.quotes.currentPrice,
|
||||
)
|
||||
},
|
||||
listedOn = if (exchangesAmount != null && exchangesAmount > 0) {
|
||||
ListedOnUM.Content(onClick = { onListedOnClick(exchangesAmount) }, amount = exchangesAmount)
|
||||
} else {
|
||||
ListedOnUM.Empty
|
||||
},
|
||||
links = value.links?.let { linksConverter.convert(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.features.feed.model.market.details.formatter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.getFiatPriceAmountWithScale
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String {
|
||||
val percent = when (interval) {
|
||||
PriceChangeInterval.H24 -> h24ChangePercent
|
||||
PriceChangeInterval.WEEK -> weekChangePercent
|
||||
PriceChangeInterval.MONTH -> monthChangePercent
|
||||
PriceChangeInterval.MONTH3 -> m3ChangePercent
|
||||
PriceChangeInterval.MONTH6 -> m6ChangePercent
|
||||
PriceChangeInterval.YEAR -> yearChangePercent
|
||||
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
|
||||
}
|
||||
|
||||
return percent?.format { percent() }.orEmpty()
|
||||
}
|
||||
|
||||
internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> h24ChangePercent
|
||||
PriceChangeInterval.WEEK -> weekChangePercent
|
||||
PriceChangeInterval.MONTH -> monthChangePercent
|
||||
PriceChangeInterval.MONTH3 -> m3ChangePercent
|
||||
PriceChangeInterval.MONTH6 -> m6ChangePercent
|
||||
PriceChangeInterval.YEAR -> yearChangePercent
|
||||
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal fun BigDecimal?.percentChangeType(): PriceChangeType {
|
||||
val scaled = this?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: BigDecimal): BigDecimal {
|
||||
return if (previousPrice == BigDecimal.ZERO) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
currentPrice.subtract(previousPrice).divide(previousPrice, 4, RoundingMode.HALF_UP)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType {
|
||||
val current = getFiatPriceAmountWithScale(value = currentPrice).first
|
||||
val updated = getFiatPriceAmountWithScale(value = updatedPrice).first
|
||||
|
||||
return when {
|
||||
updated > current -> PriceChangeType.UP
|
||||
updated < current -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
|
||||
return when (this) {
|
||||
PriceChangeType.UP -> MarketChartLook.Type.Growing
|
||||
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
|
||||
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package com.tangem.features.feed.model.market.details.formatter
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.formatAsDateTime
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.H24_MILLIS
|
||||
import com.tangem.utils.WEEK_MILLIS
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal object MarketsDateTimeFormatters {
|
||||
|
||||
private val dateTimeMMMFormatter by lazy {
|
||||
DateTimeFormatters.getBestFormatterBySkeleton("dd MMM Hm")
|
||||
}
|
||||
|
||||
private val dateFormatter = DateTimeFormatters.dateDDMMYYYY
|
||||
|
||||
fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> { value: BigDecimal ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter)
|
||||
}
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
PriceChangeInterval.MONTH6,
|
||||
-> { value ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd)
|
||||
}
|
||||
PriceChangeInterval.YEAR -> { value ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd)
|
||||
}
|
||||
PriceChangeInterval.ALL_TIME -> { value ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.dateYYYY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> resourceReference(R.string.common_today)
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
-> {
|
||||
resourceReference(
|
||||
R.string.common_range_with_space,
|
||||
wrappedList(
|
||||
stringReference(
|
||||
startTimestamp.formatAsDateTime(dateTimeMMMFormatter),
|
||||
),
|
||||
resourceReference(R.string.common_now),
|
||||
),
|
||||
)
|
||||
}
|
||||
PriceChangeInterval.MONTH6,
|
||||
PriceChangeInterval.YEAR,
|
||||
-> {
|
||||
resourceReference(
|
||||
R.string.common_range_with_space,
|
||||
wrappedList(
|
||||
stringReference(
|
||||
startTimestamp.formatAsDateTime(dateFormatter),
|
||||
),
|
||||
resourceReference(R.string.common_now),
|
||||
),
|
||||
)
|
||||
}
|
||||
PriceChangeInterval.ALL_TIME -> resourceReference(R.string.common_all)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
-> {
|
||||
resourceReference(
|
||||
R.string.common_range_with_space,
|
||||
wrappedList(
|
||||
stringReference(
|
||||
markerTimestamp.toLong().formatAsDateTime(dateTimeMMMFormatter),
|
||||
),
|
||||
resourceReference(R.string.common_now),
|
||||
),
|
||||
)
|
||||
}
|
||||
PriceChangeInterval.MONTH6,
|
||||
PriceChangeInterval.YEAR,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
-> {
|
||||
resourceReference(
|
||||
R.string.common_range_with_space,
|
||||
wrappedList(
|
||||
stringReference(
|
||||
markerTimestamp.toLong().formatAsDateTime(dateFormatter),
|
||||
),
|
||||
resourceReference(R.string.common_now),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getStartTimestampByInterval(interval: PriceChangeInterval, currentTimestamp: Long): Long {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> currentTimestamp - H24_MILLIS
|
||||
PriceChangeInterval.WEEK -> currentTimestamp - WEEK_MILLIS
|
||||
PriceChangeInterval.MONTH -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(1).millis
|
||||
PriceChangeInterval.MONTH3 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(3).millis
|
||||
PriceChangeInterval.MONTH6 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(6).millis
|
||||
PriceChangeInterval.YEAR -> DateTime(currentTimestamp, DateTimeZone.UTC).minusYears(1).millis
|
||||
PriceChangeInterval.ALL_TIME -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference {
|
||||
return formatDateByInterval(
|
||||
interval = interval,
|
||||
startTimestamp = getStartTimestampByInterval(
|
||||
interval = interval,
|
||||
currentTimestamp = currentTimestamp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun formatAsDate(timestamp: Long): String {
|
||||
return timestamp.formatAsDateTime(dateFormatter)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.features.feed.model.market.details.state
|
||||
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import com.tangem.features.feed.model.market.details.converter.PricePerformanceConverter
|
||||
import com.tangem.features.feed.model.market.details.formatter.*
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class QuotesStateUpdater(
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val state: MutableStateFlow<MarketsTokenDetailsUM>,
|
||||
private val currentQuotes: MutableStateFlow<TokenQuotes>,
|
||||
private val lastUpdatedTimestamp: MutableStateFlow<Long>,
|
||||
private val currentTokenInfo: MutableStateFlow<TokenMarketInfo?>,
|
||||
private val onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) {
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(
|
||||
currentAppCurrency,
|
||||
onIntervalChanged = onPricePerformanceIntervalChanged,
|
||||
)
|
||||
|
||||
suspend fun updateQuotes(newQuotes: TokenQuotes) {
|
||||
val triggerPriceChangeType = getFormattedPriceChange(
|
||||
currentPrice = currentQuotes.value.currentPrice,
|
||||
updatedPrice = newQuotes.currentPrice,
|
||||
)
|
||||
val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) {
|
||||
triggeredEvent(
|
||||
data = triggerPriceChangeType,
|
||||
onConsume = {
|
||||
state.update { it.copy(triggerPriceChange = consumedEvent()) }
|
||||
},
|
||||
)
|
||||
} else {
|
||||
consumedEvent()
|
||||
}
|
||||
|
||||
val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
val priceChangeType = percent.percentChangeType()
|
||||
|
||||
// wait until marker is removed
|
||||
state.first { it.isMarkerSet.not() }
|
||||
|
||||
currentQuotes.value = newQuotes
|
||||
lastUpdatedTimestamp.value = DateTime.now().millis
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
priceText = newQuotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency().symbol,
|
||||
fiatCurrencyCode = currentAppCurrency().code,
|
||||
).price()
|
||||
},
|
||||
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
|
||||
interval = stateToUpdate.selectedInterval,
|
||||
),
|
||||
priceChangeType = priceChangeType,
|
||||
triggerPriceChange = trigger,
|
||||
dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString(
|
||||
stateToUpdate.selectedInterval,
|
||||
currentTimestamp = lastUpdatedTimestamp.value,
|
||||
),
|
||||
body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body {
|
||||
val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this
|
||||
|
||||
return if (this is MarketsTokenDetailsUM.Body.Content) {
|
||||
copy(
|
||||
infoBlocks = infoBlocks.copy(
|
||||
pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.feed.model.market.details.state
|
||||
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
|
||||
internal sealed class TokenNetworksState {
|
||||
|
||||
data object Loading : TokenNetworksState()
|
||||
|
||||
data object NoNetworksAvailable : TokenNetworksState()
|
||||
|
||||
data class NetworksAvailable(val networks: List<TokenMarketInfo.Network>) : TokenNetworksState()
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
|
|
@ -19,10 +20,10 @@ import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListCo
|
|||
import com.tangem.features.feed.model.market.list.analytics.MarketsListAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager
|
||||
import com.tangem.features.feed.model.market.list.statemanager.MarketsListUMStateManager
|
||||
import com.tangem.features.feed.ui.market.list.state.ListUM
|
||||
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.ui.market.list.state.MarketsNotificationUM
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.ListUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsNotificationUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -105,6 +106,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
|
||||
private var activeListManager: MarketsListBatchFlowManager = mainMarketsListManager
|
||||
|
||||
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
|
||||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
|
||||
val state = marketsListUMStateManager.state.asStateFlow()
|
||||
|
|
@ -289,6 +291,12 @@ internal class MarketsListModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun initAnalytics() {
|
||||
containerBottomSheetState.onEach { bottomSheetState ->
|
||||
if (bottomSheetState == BottomSheetState.EXPANDED) {
|
||||
analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened())
|
||||
}
|
||||
}.launchIn(modelScope)
|
||||
|
||||
state.filter { it.isInSearchMode.not() }
|
||||
.map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) }.distinctUntilChanged()
|
||||
.onEach {
|
||||
|
|
@ -308,6 +316,9 @@ internal class MarketsListModel @Inject constructor(
|
|||
launch {
|
||||
while (true) {
|
||||
delay(timeMillis)
|
||||
// Update quotes only when the container bottom sheet is in the expanded state
|
||||
containerBottomSheetState.first { it == BottomSheetState.EXPANDED }
|
||||
// and is visible on the screen
|
||||
isVisibleOnScreen.first { it }
|
||||
activeListManager.updateQuotes()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
package com.tangem.features.feed.model.market.list.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
|
||||
internal sealed class MarketsListAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = "Markets", event = event, params = params) {
|
||||
|
||||
class BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened")
|
||||
|
||||
data class SortBy(
|
||||
val sortByTypeUM: SortByTypeUM,
|
||||
val interval: MarketsListUM.TrendInterval,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.feed.ui.market.list.state
|
||||
package com.tangem.features.feed.model.market.list.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.feed.ui.market.list.state
|
||||
package com.tangem.features.feed.model.market.list.state
|
||||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.feed.ui.market.list.state
|
||||
package com.tangem.features.feed.model.market.list.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
|
|
@ -8,8 +8,8 @@ import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
|
|||
import com.tangem.features.feed.model.market.list.utils.logAction
|
||||
import com.tangem.features.feed.model.market.list.utils.logStatus
|
||||
import com.tangem.features.feed.model.market.list.utils.logUpdateResults
|
||||
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ import com.tangem.core.ui.event.triggeredEvent
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.list.state.*
|
||||
import com.tangem.features.feed.model.market.list.state.ListUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsNotificationUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsSearchBar
|
||||
import com.tangem.features.feed.model.market.list.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
package com.tangem.features.feed.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
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.router.stack.ChildStack
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.features.feed.components.FeedEntryChildFactory
|
||||
|
||||
@Composable
|
||||
internal fun EntryBottomSheetContent(
|
||||
stackState: ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
||||
Scaffold(
|
||||
containerColor = background,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
topBar = {
|
||||
AnimatedContent(
|
||||
targetState = stackState.active.instance,
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentState ->
|
||||
currentState.Title()
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
AnimatedContent(
|
||||
targetState = stackState.active.instance,
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentState ->
|
||||
currentState.Content(modifier = Modifier.padding(contentPadding))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.features.feed.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.features.feed.components.FeedEntryChildFactory
|
||||
|
||||
@Composable
|
||||
internal fun EntryContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
isOpenedInBottomSheet: Boolean,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
||||
Surface(contentColor = background) {
|
||||
Scaffold(
|
||||
containerColor = background,
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
topBar = {
|
||||
AnimatedContent(
|
||||
targetState = stackState.value.active.instance,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (!isOpenedInBottomSheet) {
|
||||
Modifier.statusBarsPadding()
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentState -> currentState.Title(bottomSheetState) }
|
||||
},
|
||||
content = { contentPadding ->
|
||||
Children(
|
||||
stack = stackState.value,
|
||||
animation = stackAnimation(fade()),
|
||||
) { child ->
|
||||
child.instance.Content(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
bottomSheetState = bottomSheetState,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
internal fun FeedListHeader(feedListSearchBar: FeedListSearchBar, modifier: Modifier = Modifier) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.*
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
|
|||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
|
||||
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
|
|
|
|||
|
|
@ -0,0 +1,330 @@
|
|||
package com.tangem.features.feed.ui.market.detailed
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.Animatable
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.components.*
|
||||
import com.tangem.features.feed.ui.market.detailed.preview.MarketsTokenDetailsPreview
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
|
||||
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.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun MarketsTokenDetailsContent(
|
||||
state: MarketsTokenDetailsUM,
|
||||
backgroundColor: Color,
|
||||
isAccountEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
Content(
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
state = state,
|
||||
portfolioBlock = portfolioBlock,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
)
|
||||
|
||||
when (state.bottomSheetConfig.content) {
|
||||
is InfoBottomSheetContent -> InfoBottomSheet(config = state.bottomSheetConfig)
|
||||
is SecurityScoreBottomSheetContent -> SecurityScoreBottomSheet(config = state.bottomSheetConfig)
|
||||
is ExchangesBottomSheetContent -> ExchangesBottomSheet(config = state.bottomSheetConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun Content(
|
||||
state: MarketsTokenDetailsUM,
|
||||
backgroundColor: Color,
|
||||
isAccountEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val lazyListState = rememberLazyListState()
|
||||
ShowPriceSubtitleEffect(
|
||||
lazyListState = lazyListState,
|
||||
onShouldShowPriceSubtitleChange = state.onShouldShowPriceSubtitleChange,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
SpacerH4()
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
item("intervalSelector") {
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH32() }
|
||||
item("chart") {
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = backgroundColor,
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
portfolioBlock = portfolioBlock,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun MarketsTokenDetailsTopBar(
|
||||
backgroundColor: Color,
|
||||
shouldShowPriceSubtitle: Boolean,
|
||||
tokenName: String,
|
||||
tokenPrice: String,
|
||||
isBackButtonEnabled: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.drawBehind { drawRect(backgroundColor) },
|
||||
title = tokenName,
|
||||
subtitle = if (shouldShowPriceSubtitle) tokenPrice else null,
|
||||
startButton = TopAppBarButtonUM.Back(
|
||||
onBackClicked = onBackClick,
|
||||
enabled = isBackButtonEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
TokenPriceText(
|
||||
price = state.priceText,
|
||||
triggerPriceChange = state.triggerPriceChange,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
Text(
|
||||
text = state.dateTimeText.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
if (state.priceChangePercentText != null) {
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = state.priceChangePercentText,
|
||||
type = state.priceChangeType,
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SpacerW4()
|
||||
CoinIcon(
|
||||
modifier = Modifier.requiredSize(TangemTheme.dimens.size48),
|
||||
url = state.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceText(
|
||||
price: String,
|
||||
triggerPriceChange: StateEvent<PriceChangeType>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val growColor = TangemTheme.colors.text.accent
|
||||
val fallColor = TangemTheme.colors.text.warning
|
||||
val generalColor = TangemTheme.colors.text.primary1
|
||||
|
||||
val color = remember(generalColor) { Animatable(generalColor) }
|
||||
|
||||
EventEffect(triggerPriceChange) { 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 = price,
|
||||
modifier = modifier,
|
||||
color = color.value,
|
||||
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.head.fontSize),
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.head,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IntervalSelector(
|
||||
trendInterval: PriceChangeInterval,
|
||||
isEnabled: Boolean,
|
||||
onIntervalClick: (PriceChangeInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
PriceChangeInterval.MONTH6,
|
||||
PriceChangeInterval.YEAR,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
),
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
initialSelectedItem = trendInterval,
|
||||
onClick = onIntervalClick,
|
||||
isEnabled = isEnabled,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = if (isEnabled) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.disabled
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowPriceSubtitleEffect(lazyListState: LazyListState, onShouldShowPriceSubtitleChange: (Boolean) -> Unit) {
|
||||
val showPriceSubtitleFlow = remember(lazyListState) {
|
||||
snapshotFlow { lazyListState.firstVisibleItemIndex > 1 }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
LaunchedEffect(showPriceSubtitleFlow) {
|
||||
showPriceSubtitleFlow.collect { isVisible ->
|
||||
onShouldShowPriceSubtitleChange(isVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PriceChangeInterval.getText(): TextReference {
|
||||
return when (this) {
|
||||
PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title)
|
||||
PriceChangeInterval.WEEK -> resourceReference(R.string.markets_selector_interval_7d_title)
|
||||
PriceChangeInterval.MONTH -> resourceReference(R.string.markets_selector_interval_1m_title)
|
||||
PriceChangeInterval.MONTH3 -> resourceReference(R.string.markets_selector_interval_3m_title)
|
||||
PriceChangeInterval.MONTH6 -> resourceReference(R.string.markets_selector_interval_6m_title)
|
||||
PriceChangeInterval.YEAR -> resourceReference(R.string.markets_selector_interval_1y_title)
|
||||
PriceChangeInterval.ALL_TIME -> resourceReference(R.string.markets_selector_interval_all_title)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun MarketsTokenDetailsContent_Preview(
|
||||
@PreviewParameter(MarketsTokenDetailsContentPreviewProvider::class) params: MarketsTokenDetailsUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
MarketsTokenDetailsContent(
|
||||
state = params,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
portfolioBlock = {},
|
||||
isAccountEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class MarketsTokenDetailsContentPreviewProvider : PreviewParameterProvider<MarketsTokenDetailsUM> {
|
||||
override val values: Sequence<MarketsTokenDetailsUM>
|
||||
get() = sequenceOf(
|
||||
MarketsTokenDetailsPreview.loadingState,
|
||||
MarketsTokenDetailsPreview.contentState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Exchanges bottom sheet
|
||||
*
|
||||
* @param config bottom sheet config
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<ExchangesBottomSheetContent>(
|
||||
config = config,
|
||||
addBottomInsets = false,
|
||||
title = { Title(textResId = it.titleResId, onBackClick = config.onDismissRequest) },
|
||||
content = { content ->
|
||||
Box {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
) {
|
||||
item(key = "subtitle") {
|
||||
Subtitle(
|
||||
subtitleRes = content.subtitleResId,
|
||||
volumeReference = content.volumeReference,
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = content.exchangeItems,
|
||||
key = TokenItemState::id,
|
||||
itemContent = { TokenItem(state = it, isBalanceHidden = false) },
|
||||
)
|
||||
}
|
||||
|
||||
if (content is ExchangesBottomSheetContent.Error) {
|
||||
Error(
|
||||
content = content,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(@StringRes textResId: Int, onBackClick: () -> Unit) {
|
||||
TangemTopAppBar(
|
||||
title = stringResourceSafe(id = textResId),
|
||||
startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Subtitle(@StringRes subtitleRes: Int, volumeReference: TextReference, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
SubtitleText(textReference = resourceReference(id = subtitleRes))
|
||||
|
||||
SubtitleText(textReference = volumeReference)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubtitleText(textReference: TextReference) {
|
||||
Text(
|
||||
text = textReference.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = content.message),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(id = R.string.alert_button_try_again),
|
||||
onClick = content.onRetryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ExchangesBottomSheet(
|
||||
@PreviewParameter(ExchangesBottomSheetContentProvider::class) content: ExchangesBottomSheetContent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ExchangesBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
onDismissRequest = {},
|
||||
content = content,
|
||||
isShown = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterProvider<ExchangesBottomSheetContent>(
|
||||
listOf(
|
||||
ExchangesBottomSheetContent.Loading(exchangesCount = 13),
|
||||
ExchangesBottomSheetContent.Error(onRetryClick = {}),
|
||||
ExchangesBottomSheetContent.Content(
|
||||
exchangeItems = List(size = 13) { index ->
|
||||
TokenItemState.Content(
|
||||
id = index.toString(),
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.ic_facebook_24,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "OKX")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference(value = "CEX")),
|
||||
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
|
||||
auditLabelUM = AuditLabelUM(
|
||||
text = stringReference("Caution"),
|
||||
type = AuditLabelUM.Type.Warning,
|
||||
),
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
|
||||
@Composable
|
||||
internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<InfoBottomSheetContent>(
|
||||
config = config,
|
||||
addBottomInsets = false,
|
||||
title = { TangemBottomSheetTitle(title = it.title) },
|
||||
content = { content ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
MarkdownText(
|
||||
markdown = content.body.resolveReference(),
|
||||
disableLinkMovementMethod = true,
|
||||
linkifyMask = 0,
|
||||
syntaxHighlightColor = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2.copy(
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
),
|
||||
)
|
||||
|
||||
if (content.generatedAINotificationUM != null) {
|
||||
AdditionalInfoNotification(
|
||||
onClick = content.generatedAINotificationUM.onClick,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Notification(
|
||||
config = NotificationConfig(
|
||||
subtitle = TextReference.Res(id = R.string.information_generated_with_ai),
|
||||
iconResId = R.drawable.ic_magic_28,
|
||||
onClick = onClick,
|
||||
shouldShowArrowIcon = false,
|
||||
),
|
||||
modifier = modifier,
|
||||
subtitleColor = TangemTheme.colors.text.primary1,
|
||||
containerColor = TangemTheme.colors.button.disabled,
|
||||
iconTint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.text.TooltipText
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
|
||||
|
||||
@Composable
|
||||
internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
if (infoPointUM.onInfoClick != null) {
|
||||
TooltipText(
|
||||
text = infoPointUM.title,
|
||||
onInfoClick = infoPointUM.onInfoClick,
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = infoPointUM.title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Text(
|
||||
text = infoPointUM.value,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (infoPointUM.change != null) {
|
||||
SpacerW4()
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size8)
|
||||
.align(Alignment.CenterVertically),
|
||||
imageVector = ImageVector.vectorResource(
|
||||
id = when (infoPointUM.change) {
|
||||
InfoPointUM.ChangeType.UP -> R.drawable.ic_arrow_up_8
|
||||
InfoPointUM.ChangeType.DOWN -> R.drawable.ic_arrow_down_8
|
||||
},
|
||||
),
|
||||
tint = when (infoPointUM.change) {
|
||||
InfoPointUM.ChangeType.UP -> TangemTheme.colors.icon.accent
|
||||
InfoPointUM.ChangeType.DOWN -> TangemTheme.colors.icon.warning
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) {
|
||||
Column(
|
||||
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
if (withTooltip) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredHeight(TangemTheme.dimens.size16)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
textSizeHeight = false,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
style = TangemTheme.typography.body1,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(150.dp)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000,000",
|
||||
),
|
||||
)
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000,000",
|
||||
onInfoClick = { },
|
||||
),
|
||||
)
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000",
|
||||
change = InfoPointUM.ChangeType.UP,
|
||||
onInfoClick = { },
|
||||
),
|
||||
)
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000",
|
||||
change = InfoPointUM.ChangeType.DOWN,
|
||||
onInfoClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewShimmer() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(150.dp)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
InfoPointShimmer(modifier = Modifier.fillMaxWidth())
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.block.information.GridItems
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.text.TooltipText
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.getText
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InsightsUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
|
||||
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
TooltipText(
|
||||
text = resourceReference(R.string.markets_token_details_insights),
|
||||
textStyle = TangemTheme.typography.subtitle2,
|
||||
onInfoClick = state.onInfoClick,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
),
|
||||
initialSelectedItem = PriceChangeInterval.H24,
|
||||
onClick = { interval ->
|
||||
currentInterval = interval
|
||||
state.onIntervalChanged(interval)
|
||||
},
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = 14.dp,
|
||||
vertical = 4.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val infoPoints = when (currentInterval) {
|
||||
PriceChangeInterval.H24 -> state.h24Info
|
||||
PriceChangeInterval.WEEK -> state.weekInfo
|
||||
PriceChangeInterval.MONTH -> state.monthInfo
|
||||
else -> state.h24Info
|
||||
}
|
||||
|
||||
GridItems(
|
||||
items = infoPoints,
|
||||
itemContent = { infoPointUM ->
|
||||
InfoPoint(
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
infoPointUM = infoPointUM,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
|
||||
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
|
||||
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(headerHeight)
|
||||
.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
GridItems(
|
||||
items = List(size = 4) { it }.toImmutableList(),
|
||||
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
itemContent = {
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
InsightsBlock(
|
||||
state = InsightsUM(
|
||||
h24Info = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
),
|
||||
weekInfo = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000 000",
|
||||
),
|
||||
),
|
||||
monthInfo = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000",
|
||||
),
|
||||
),
|
||||
onInfoClick = {},
|
||||
onIntervalChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
actualContent = { ContentPreview() },
|
||||
shimmerContent = { InsightsBlockPlaceholder() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.ChipShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.chip.Chip
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.LinksUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
contentHorizontalPadding = 0.dp,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.markets_token_details_links),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column {
|
||||
SubBlock(
|
||||
title = stringResourceSafe(id = R.string.markets_token_details_official_links),
|
||||
links = state.officialLinks,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResourceSafe(id = R.string.markets_token_details_social),
|
||||
links = state.social,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResourceSafe(id = R.string.markets_token_details_repository),
|
||||
links = state.repository,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResourceSafe(id = R.string.markets_token_details_blockchain_site),
|
||||
links = state.blockchainSite,
|
||||
onLinkClick = state.onLinkClick,
|
||||
lastBlock = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun SubBlock(
|
||||
links: ImmutableList<LinksUM.Link>,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
lastBlock: Boolean = false,
|
||||
title: String = "Official links",
|
||||
) {
|
||||
if (links.isEmpty()) return
|
||||
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = !lastBlock,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
links.fastForEach { link ->
|
||||
Chip(
|
||||
text = stringReference(link.title),
|
||||
iconResId = link.iconRes,
|
||||
onClick = { onLinkClick(link) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LinksBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
contentHorizontalPadding = 0.dp,
|
||||
title = {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column {
|
||||
SubBlockPlaceholder()
|
||||
SubBlockPlaceholder()
|
||||
SubBlockPlaceholder(lastBlock = true)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = !lastBlock,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(78.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
repeat(times = 3) {
|
||||
ChipShimmer(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
LinksBlock(
|
||||
state = LinksUM(
|
||||
officialLinks = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = "Website",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = "Website",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = "Website",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
social = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = "Twitter",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = "Facebook",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
repository = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = "Github",
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
blockchainSite = persistentListOf(),
|
||||
onLinkClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PlaceholderPreview() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = { LinksBlockPlaceholder() },
|
||||
actualContent = { ContentPreview() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ListedOnUM
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* "Listed on" block
|
||||
*
|
||||
* @param state block state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun ListedOnBlock(state: ListedOnUM, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
InformationBlock(
|
||||
title = {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(enabled = state is ListedOnUM.Content) {
|
||||
(state as? ListedOnUM.Content)?.onClick?.invoke()
|
||||
},
|
||||
) {
|
||||
Description(
|
||||
state = state,
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
|
||||
if (state is ListedOnUM.Content) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ListedOnBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
title = {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction = 0.3f)
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Description(state: ListedOnUM, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
modifier = modifier,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 328, heightDp = 68)
|
||||
@Preview(name = "Dark Theme", widthDp = 328, heightDp = 68, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ListedOnBlock(@PreviewParameter(ListenOnUMProvider::class) state: ListedOnUM?) {
|
||||
TangemThemePreview {
|
||||
if (state == null) {
|
||||
ListedOnBlockPlaceholder()
|
||||
} else {
|
||||
ListedOnBlock(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ListedOnBlock_StateChanging() {
|
||||
var state by remember { mutableStateOf<ListedOnUM?>(value = null) }
|
||||
|
||||
Preview_ListedOnBlock(state = state)
|
||||
|
||||
LaunchedEffect(key1 = null) {
|
||||
delay(timeMillis = 3000)
|
||||
|
||||
state = ListedOnUM.Empty
|
||||
}
|
||||
}
|
||||
|
||||
private class ListenOnUMProvider : CollectionPreviewParameterProvider<ListedOnUM?>(
|
||||
collection = listOf(
|
||||
ListedOnUM.Empty,
|
||||
ListedOnUM.Content(onClick = {}, amount = 5),
|
||||
null,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.common.ui.charts.MarketChart
|
||||
import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.rememberMarketChartState
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
|
||||
@Composable
|
||||
internal fun MarketTokenDetailsChart(
|
||||
state: MarketsTokenDetailsUM.ChartState,
|
||||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val growingColor = TangemTheme.colors.icon.accent
|
||||
val fallingColor = TangemTheme.colors.icon.warning
|
||||
val neutralColor = TangemTheme.colors.icon.informative
|
||||
|
||||
val chartState = rememberMarketChartState(
|
||||
dataProducer = state.dataProducer,
|
||||
colorMapper = { type ->
|
||||
when (type) {
|
||||
MarketChartLook.Type.Growing -> growingColor
|
||||
MarketChartLook.Type.Falling -> fallingColor
|
||||
MarketChartLook.Type.Neutral -> neutralColor
|
||||
}
|
||||
},
|
||||
onMarkerShown = state.onMarkerPointSelected,
|
||||
)
|
||||
|
||||
val bottomChartAxisHeight = getMarketChartBottomAxisHeight()
|
||||
|
||||
Box(modifier) {
|
||||
MarketChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = chartState,
|
||||
)
|
||||
|
||||
if (state.status != MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
Box(
|
||||
Modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.matchParentSize()
|
||||
.padding(bottom = bottomChartAxisHeight),
|
||||
) {
|
||||
when (state.status) {
|
||||
MarketsTokenDetailsUM.ChartState.Status.LOADING -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.align(Alignment.Center),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
MarketsTokenDetailsUM.ChartState.Status.ERROR -> {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.align(Alignment.Center),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.TextButton
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.GridItems
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MetricsUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
const val MAX_METRICS_COUNT = 6
|
||||
|
||||
@Composable
|
||||
internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.markets_token_details_metrics),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
if (state.metrics.size > MAX_METRICS_COUNT) {
|
||||
ShowLessMoreButton(expanded = isExpanded, onClick = { isExpanded = !isExpanded })
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val metrics = if (isExpanded) {
|
||||
state.metrics
|
||||
} else {
|
||||
state.metrics.take(MAX_METRICS_COUNT).toImmutableList()
|
||||
}
|
||||
|
||||
GridItems(
|
||||
items = metrics,
|
||||
itemContent = {
|
||||
InfoPoint(infoPointUM = it)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock
|
||||
@Composable
|
||||
private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) {
|
||||
// FIXME add string resources
|
||||
val text = if (expanded) {
|
||||
"See less"
|
||||
} else {
|
||||
"See more"
|
||||
}
|
||||
|
||||
TextButton(
|
||||
text = text,
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.positiveButtonColors,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
Box(Modifier)
|
||||
},
|
||||
content = {
|
||||
GridItems(
|
||||
items = List(size = 6) { it }.toImmutableList(),
|
||||
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
itemContent = {
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun BlockPreview() {
|
||||
TangemThemePreview {
|
||||
MetricsBlock(
|
||||
state = MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
value = "A",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
actualContent = { BlockPreview() },
|
||||
shimmerContent = { MetricsBlockPlaceholder() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW8
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemAnimations
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.getText
|
||||
import com.tangem.features.feed.ui.market.detailed.state.PricePerformanceUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) {
|
||||
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.markets_token_details_price_performance),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
),
|
||||
initialSelectedItem = PriceChangeInterval.H24,
|
||||
onClick = { interval ->
|
||||
currentInterval = interval
|
||||
state.onIntervalChanged(interval)
|
||||
},
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = 14.dp,
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val value = when (currentInterval) {
|
||||
PriceChangeInterval.H24 -> state.h24
|
||||
PriceChangeInterval.MONTH -> state.month
|
||||
PriceChangeInterval.ALL_TIME -> state.all
|
||||
else -> error("")
|
||||
}
|
||||
|
||||
Content(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = value,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) {
|
||||
val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(
|
||||
targetFraction = state.indicatorFraction,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_token_details_low),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SpacerW8()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_token_details_high),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
TangemLinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size6)
|
||||
.fillMaxWidth(),
|
||||
progress = { animatedIndicatorFraction },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = state.low,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = state.high,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
|
||||
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
|
||||
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(headerHeight)
|
||||
.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(35.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
SpacerW8()
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(35.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size6)
|
||||
.fillMaxWidth(),
|
||||
radius = 27.dp,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size56),
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
SpacerW8()
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size56),
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
PricePerformanceBlock(
|
||||
modifier = Modifier,
|
||||
state = PricePerformanceUM(
|
||||
h24 = PricePerformanceUM.Value(
|
||||
low = "\$38,5K",
|
||||
high = "\$58,5K",
|
||||
indicatorFraction = 0.5f,
|
||||
),
|
||||
month = PricePerformanceUM.Value(
|
||||
low = "\$500,5K",
|
||||
high = "\$5800,5K",
|
||||
indicatorFraction = 0.8f,
|
||||
),
|
||||
all = PricePerformanceUM.Value(
|
||||
low = "\$58,52",
|
||||
high = "\$580,5M",
|
||||
indicatorFraction = 0.2f,
|
||||
),
|
||||
onIntervalChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PlaceholderPreview() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
PricePerformanceBlockPlaceholder()
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlin.math.round
|
||||
|
||||
private const val STARS_COUNT = 5
|
||||
|
||||
@Composable
|
||||
internal fun ScoreStarsBlock(
|
||||
score: Float,
|
||||
horizontalSpacing: Dp,
|
||||
scoreTextStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val rounded = score.roundTo1decimal()
|
||||
val percentage = rounded / STARS_COUNT
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(horizontalSpacing),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = rounded.toString(),
|
||||
style = scoreTextStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Stars(fraction = percentage)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
|
||||
val grayColor = TangemTheme.colors.icon.inactive
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
repeat(times = 5) { i ->
|
||||
Box(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.requiredSize(16.dp)
|
||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||
.drawWithCache {
|
||||
onDrawWithContent {
|
||||
val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0)
|
||||
val starFractionFloat = starFraction
|
||||
.toFloat()
|
||||
.roundTo1decimal()
|
||||
|
||||
drawContent()
|
||||
drawRect(
|
||||
color = grayColor,
|
||||
topLeft = Offset(x = size.width * starFractionFloat, y = 0f),
|
||||
size = Size(size.width * (1 - starFractionFloat), size.height),
|
||||
blendMode = BlendMode.SrcIn,
|
||||
)
|
||||
}
|
||||
},
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_star_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun Float.roundTo1decimal(): Float {
|
||||
return round(this * 10) / 10
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.text.TooltipText
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreUM
|
||||
|
||||
@Composable
|
||||
internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = TangemTheme.dimens.size72)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TooltipText(
|
||||
text = resourceReference(R.string.markets_token_details_security_score),
|
||||
onInfoClick = state.onInfoClick,
|
||||
textStyle = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
ScoreStarsBlock(
|
||||
score = state.score,
|
||||
scoreTextStyle = TangemTheme.typography.body1,
|
||||
horizontalSpacing = TangemTheme.dimens.spacing8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = TangemTheme.dimens.size72)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction = 0.4f)
|
||||
.padding(vertical = TangemTheme.dimens.spacing2)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 328, showBackground = true)
|
||||
@Preview(widthDp = 328, showBackground = true, locale = "ru")
|
||||
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
SecurityScoreBlock(
|
||||
state = SecurityScoreUM(
|
||||
score = 3.5f,
|
||||
description = stringReference("Based on 3 ratings"),
|
||||
onInfoClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 328, showBackground = true)
|
||||
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
SecurityScoreBlockPlaceholder(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.preview.SecurityScorePreviewData
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent
|
||||
|
||||
@Composable
|
||||
internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<SecurityScoreBottomSheetContent>(
|
||||
config = config,
|
||||
addBottomInsets = false,
|
||||
title = { TangemBottomSheetTitle(title = it.title) },
|
||||
content = { content ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
Text(
|
||||
text = content.description.resolveReference(),
|
||||
style = TangemTheme.typography.body2.copy(
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
),
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
content.providers.fastForEachIndexed { index, provider ->
|
||||
DividerContainer(
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = content.providers.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
showDivider = index != content.providers.lastIndex,
|
||||
) {
|
||||
SecurityScoreProviderRow(
|
||||
providerUM = provider,
|
||||
onLinkClick = { content.onProviderLinkClick(provider) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH16()
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecurityScoreProviderRow(
|
||||
providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM,
|
||||
onLinkClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.heightIn(min = TangemTheme.dimens.size68),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size40)
|
||||
.clip(TangemTheme.shapes.roundedCorners8),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(providerUM.iconUrl)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.build(),
|
||||
loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
|
||||
error = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
Text(
|
||||
text = providerUM.name,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
providerUM.lastAuditDate?.let { auditDate ->
|
||||
Text(
|
||||
text = auditDate,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerWMax()
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
modifier = Modifier.clickable(
|
||||
enabled = providerUM.urlData != null,
|
||||
indication = ripple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onLinkClick,
|
||||
),
|
||||
) {
|
||||
ScoreStarsBlock(
|
||||
score = providerUM.score,
|
||||
scoreTextStyle = TangemTheme.typography.body2,
|
||||
horizontalSpacing = TangemTheme.dimens.spacing3,
|
||||
)
|
||||
|
||||
UrlBlock(providerUM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM) {
|
||||
val urlData = providerUM.urlData
|
||||
val rootHost = urlData?.rootHost
|
||||
if (urlData != null && rootHost != null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
Text(
|
||||
text = urlData.rootHost,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SecurityScoreBottomSheetPreview() {
|
||||
TangemThemePreview {
|
||||
SecurityScoreBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = SecurityScorePreviewData.bottomSheetContent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.items.DescriptionItem
|
||||
import com.tangem.core.ui.components.items.DescriptionPlaceholder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
|
||||
@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA]
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(
|
||||
state: MarketsTokenDetailsUM.Body,
|
||||
isAccountEnabled: Boolean,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
when (state) {
|
||||
MarketsTokenDetailsUM.Body.Loading -> {
|
||||
item("description-loading") {
|
||||
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
if (portfolioBlock != null) {
|
||||
item(key = "portfolio") {
|
||||
portfolioBlock(Modifier.blockPaddings())
|
||||
}
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
|
||||
loadingInfoBlocks()
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Content -> {
|
||||
if (state.description != null) {
|
||||
description(state.description)
|
||||
}
|
||||
|
||||
if (portfolioBlock != null) {
|
||||
item(key = "portfolio") {
|
||||
portfolioBlock(Modifier.blockPaddings())
|
||||
}
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
|
||||
infoBlocksList(state.infoBlocks)
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Error -> {
|
||||
error(state)
|
||||
}
|
||||
MarketsTokenDetailsUM.Body.Nothing -> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
|
||||
item("body-error") {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing40,
|
||||
),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.aboutCoinHeader() {
|
||||
item("aboutCoinHeader") {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
),
|
||||
text = stringResourceSafe(R.string.markets_about_coin_header),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
|
||||
item("description") {
|
||||
DescriptionItem(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
description = description.shortDescription,
|
||||
hasFullDescription = description.fullDescription != null,
|
||||
onReadMoreClick = description.onReadMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) {
|
||||
if (state.insights != null) {
|
||||
item("insights") {
|
||||
InsightsBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.insights,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.securityScore != null) {
|
||||
item("securityScore") {
|
||||
SecurityScoreBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.securityScore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.metrics != null) {
|
||||
item("metrics") {
|
||||
MetricsBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.metrics,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.pricePerformance != null) {
|
||||
item("pricePerformance") {
|
||||
PricePerformanceBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.pricePerformance,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "listedOn") {
|
||||
ListedOnBlock(
|
||||
state = state.listedOn,
|
||||
modifier = Modifier.blockPaddings(),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.links != null) {
|
||||
item("links") {
|
||||
LinksBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.links,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.loadingInfoBlocks() {
|
||||
item("insights-loading") {
|
||||
InsightsBlockPlaceholder(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
)
|
||||
}
|
||||
|
||||
item("securityScore-loading") {
|
||||
SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("metrics-loading") {
|
||||
MetricsBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("pricePerformance-loading") {
|
||||
PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item(key = "listedOn-loading") {
|
||||
ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("links-loading") {
|
||||
LinksBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.blockPaddings(): Modifier {
|
||||
return this.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.preview
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InsightsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ListedOnUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MetricsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.PricePerformanceUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object MarketsTokenDetailsPreview {
|
||||
private val infoPoint = InfoPointUM(
|
||||
title = stringReference("1"),
|
||||
value = "2",
|
||||
change = InfoPointUM.ChangeType.DOWN,
|
||||
onInfoClick = {},
|
||||
)
|
||||
|
||||
val loadingState = MarketsTokenDetailsUM(
|
||||
tokenName = "Token Name",
|
||||
priceText = "$0.00000000324",
|
||||
dateTimeText = stringReference("Today"),
|
||||
priceChangePercentText = "52.00%",
|
||||
iconUrl = "",
|
||||
priceChangeType = PriceChangeType.UP,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = MarketChartDataProducer.build { },
|
||||
onLoadRetryClick = {},
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = { _, _ -> },
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = { },
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
isMarkerSet = false,
|
||||
triggerPriceChange = consumedEvent(),
|
||||
onShouldShowPriceSubtitleChange = {},
|
||||
shouldShowPriceSubtitle = false,
|
||||
)
|
||||
|
||||
val contentState = MarketsTokenDetailsUM(
|
||||
tokenName = "Token Name",
|
||||
priceText = "$0.00000000324",
|
||||
dateTimeText = stringReference("Today"),
|
||||
priceChangePercentText = "52.00%",
|
||||
iconUrl = "",
|
||||
priceChangeType = PriceChangeType.UP,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = MarketChartDataProducer.build { },
|
||||
onLoadRetryClick = {},
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = { _, _ -> },
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = { },
|
||||
body = MarketsTokenDetailsUM.Body.Content(
|
||||
description = MarketsTokenDetailsUM.Description(
|
||||
shortDescription = stringReference("markets_token_details_description_short"),
|
||||
fullDescription = stringReference("markets_token_details_description_full"),
|
||||
onReadMoreClick = {},
|
||||
),
|
||||
infoBlocks = MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = InsightsUM(
|
||||
h24Info = persistentListOf(
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
),
|
||||
weekInfo = persistentListOf(
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
),
|
||||
monthInfo = persistentListOf(
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
),
|
||||
onInfoClick = {},
|
||||
onIntervalChanged = {},
|
||||
),
|
||||
securityScore = SecurityScoreUM(
|
||||
score = 2.3f,
|
||||
description = stringReference("markets_token_details_security_score_description"),
|
||||
onInfoClick = {},
|
||||
),
|
||||
metrics = MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
infoPoint,
|
||||
),
|
||||
),
|
||||
pricePerformance = PricePerformanceUM(
|
||||
h24 = PricePerformanceUM.Value(
|
||||
low = "1",
|
||||
high = "2",
|
||||
indicatorFraction = 0.3f,
|
||||
),
|
||||
month = PricePerformanceUM.Value(
|
||||
low = "1",
|
||||
high = "2",
|
||||
indicatorFraction = 0.3f,
|
||||
),
|
||||
all = PricePerformanceUM.Value(
|
||||
low = "1",
|
||||
high = "2",
|
||||
indicatorFraction = 0.3f,
|
||||
),
|
||||
onIntervalChanged = {},
|
||||
),
|
||||
listedOn = ListedOnUM.Empty,
|
||||
links = null,
|
||||
),
|
||||
),
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
isMarkerSet = true,
|
||||
triggerPriceChange = consumedEvent(),
|
||||
onShouldShowPriceSubtitleChange = {},
|
||||
shouldShowPriceSubtitle = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.preview
|
||||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent
|
||||
|
||||
internal object SecurityScorePreviewData {
|
||||
|
||||
val bottomSheetContent = SecurityScoreBottomSheetContent(
|
||||
title = stringReference("Security score"),
|
||||
description = stringReference(
|
||||
"Security score of a token is a metric that assesses the " +
|
||||
"security level of a blockchain or token based on various factors and is compiled from " +
|
||||
"the sources listed below.",
|
||||
),
|
||||
providers = listOf(
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = "Moralis",
|
||||
lastAuditDate = "21.10.2024",
|
||||
score = 4.9F,
|
||||
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = "https://moralis.com/",
|
||||
rootHost = "moralis.com",
|
||||
),
|
||||
iconUrl = "",
|
||||
),
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = "Certik",
|
||||
lastAuditDate = "10.07.2024",
|
||||
score = 4.6F,
|
||||
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = "https://certik.com/",
|
||||
rootHost = "certik.com",
|
||||
),
|
||||
iconUrl = "",
|
||||
),
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = "Cyberscope",
|
||||
lastAuditDate = "25.06.2023",
|
||||
score = 4.5F,
|
||||
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = "https://cyberscope.com/",
|
||||
rootHost = "cyberscope.com",
|
||||
),
|
||||
iconUrl = "",
|
||||
),
|
||||
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
|
||||
name = "TokenInsight",
|
||||
lastAuditDate = "17.01.2022",
|
||||
score = 4.0F,
|
||||
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData(
|
||||
fullUrl = "https://tokeninsight.com/",
|
||||
rootHost = "tokeninsight.com",
|
||||
),
|
||||
iconUrl = "",
|
||||
),
|
||||
|
||||
),
|
||||
onProviderLinkClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.plus
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.StringsSigns.DOT
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Exchanges bottom sheet content
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigContent {
|
||||
|
||||
/** Title of bottom sheet. Like, app bar. */
|
||||
@get:StringRes
|
||||
val titleResId: Int
|
||||
get() = R.string.markets_token_details_exchanges_title
|
||||
|
||||
/** Subtitle */
|
||||
@get:StringRes
|
||||
val subtitleResId: Int
|
||||
get() = R.string.markets_token_details_exchange
|
||||
|
||||
/** Volume info */
|
||||
val volumeReference: TextReference
|
||||
get() = resourceReference(id = R.string.markets_token_details_volume) +
|
||||
stringReference(value = " $DOT ") +
|
||||
resourceReference(id = R.string.markets_selector_interval_24h_title)
|
||||
|
||||
/** Exchange items */
|
||||
val exchangeItems: ImmutableList<TokenItemState>
|
||||
|
||||
/**
|
||||
* Loading state
|
||||
*
|
||||
* @property exchangesCount count of exchanges
|
||||
*/
|
||||
data class Loading(val exchangesCount: Int) : ExchangesBottomSheetContent {
|
||||
|
||||
override val exchangeItems: ImmutableList<TokenItemState>
|
||||
get() = List(size = exchangesCount) { index -> TokenItemState.Loading(id = "loading#$index") }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Content state
|
||||
*
|
||||
* @property exchangeItems exchanges
|
||||
*/
|
||||
data class Content(
|
||||
override val exchangeItems: ImmutableList<TokenItemState>,
|
||||
) : ExchangesBottomSheetContent
|
||||
|
||||
/** Error state */
|
||||
data class Error(
|
||||
val onRetryClick: () -> Unit,
|
||||
) : ExchangesBottomSheetContent {
|
||||
override val exchangeItems: ImmutableList<TokenItemState> = persistentListOf()
|
||||
|
||||
@StringRes
|
||||
val message: Int = R.string.markets_loading_error_title
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class InfoBottomSheetContent(
|
||||
val title: TextReference,
|
||||
val body: TextReference,
|
||||
val generatedAINotificationUM: GeneratedAINotificationUM? = null,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
data class GeneratedAINotificationUM(val onClick: () -> Unit)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class InfoPointUM(
|
||||
val title: TextReference,
|
||||
val value: String,
|
||||
val change: ChangeType? = null,
|
||||
val onInfoClick: (() -> Unit)? = null,
|
||||
) {
|
||||
enum class ChangeType {
|
||||
UP, DOWN
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class InsightsUM(
|
||||
val h24Info: ImmutableList<InfoPointUM>,
|
||||
val weekInfo: ImmutableList<InfoPointUM>,
|
||||
val monthInfo: ImmutableList<InfoPointUM>,
|
||||
val onInfoClick: () -> Unit,
|
||||
val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class LinksUM(
|
||||
val officialLinks: ImmutableList<Link>,
|
||||
val social: ImmutableList<Link>,
|
||||
val repository: ImmutableList<Link>,
|
||||
val blockchainSite: ImmutableList<Link>,
|
||||
val onLinkClick: (Link) -> Unit,
|
||||
) {
|
||||
data class Link(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val title: String,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
||||
/**
|
||||
* "Listed on" block UI model
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal sealed interface ListedOnUM {
|
||||
|
||||
/** Title */
|
||||
val title: TextReference
|
||||
get() = resourceReference(id = R.string.markets_token_details_listed_on)
|
||||
|
||||
/** Description */
|
||||
val description: TextReference
|
||||
|
||||
/** Empty state. No exchanges found */
|
||||
data object Empty : ListedOnUM {
|
||||
override val description = resourceReference(id = R.string.markets_token_details_empty_exchanges)
|
||||
}
|
||||
|
||||
/**
|
||||
* Content with number of exchanges
|
||||
*
|
||||
* @property onClick lambda be invoked when button is clicked
|
||||
* @property amount amount of exchanges
|
||||
*/
|
||||
data class Content(
|
||||
val onClick: () -> Unit,
|
||||
private val amount: Int,
|
||||
) : ListedOnUM {
|
||||
override val description: TextReference = pluralReference(
|
||||
id = R.plurals.markets_token_details_amount_exchanges,
|
||||
count = amount,
|
||||
formatArgs = wrappedList(amount),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
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.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 java.math.BigDecimal
|
||||
|
||||
internal data class MarketsTokenDetailsUM(
|
||||
val tokenName: String,
|
||||
val priceText: String,
|
||||
val iconUrl: String?,
|
||||
val dateTimeText: TextReference,
|
||||
val priceChangePercentText: String?,
|
||||
val priceChangeType: PriceChangeType,
|
||||
val selectedInterval: PriceChangeInterval,
|
||||
val isMarkerSet: Boolean,
|
||||
val chartState: ChartState,
|
||||
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
|
||||
val bottomSheetConfig: TangemBottomSheetConfig,
|
||||
val triggerPriceChange: StateEvent<PriceChangeType>,
|
||||
val body: Body,
|
||||
val shouldShowPriceSubtitle: Boolean,
|
||||
val onShouldShowPriceSubtitleChange: (Boolean) -> Unit,
|
||||
) {
|
||||
|
||||
data class ChartState(
|
||||
val status: Status,
|
||||
val dataProducer: MarketChartDataProducer,
|
||||
val onLoadRetryClick: () -> Unit,
|
||||
val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit,
|
||||
) {
|
||||
enum class Status {
|
||||
LOADING, ERROR, DATA
|
||||
}
|
||||
}
|
||||
|
||||
data class InformationBlocks(
|
||||
val insights: InsightsUM?,
|
||||
val securityScore: SecurityScoreUM?,
|
||||
val metrics: MetricsUM?,
|
||||
val pricePerformance: PricePerformanceUM?,
|
||||
val listedOn: ListedOnUM,
|
||||
val links: LinksUM?,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed interface Body {
|
||||
|
||||
data class Error(
|
||||
val onLoadRetryClick: () -> Unit,
|
||||
) : Body
|
||||
|
||||
data object Loading : Body
|
||||
|
||||
data class Content(
|
||||
val description: Description?,
|
||||
val infoBlocks: InformationBlocks,
|
||||
) : Body
|
||||
|
||||
data object Nothing : Body
|
||||
}
|
||||
|
||||
data class Description(
|
||||
val shortDescription: TextReference,
|
||||
val fullDescription: TextReference?,
|
||||
val onReadMoreClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class MetricsUM(
|
||||
val metrics: ImmutableList<InfoPointUM>,
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
|
||||
internal data class PricePerformanceUM(
|
||||
val h24: Value,
|
||||
val month: Value,
|
||||
val all: Value,
|
||||
val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) {
|
||||
data class Value(
|
||||
val low: String,
|
||||
val high: String,
|
||||
@FloatRange(from = 0.0, to = 1.0) val indicatorFraction: Float,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class SecurityScoreBottomSheetContent(
|
||||
val title: TextReference,
|
||||
val description: TextReference,
|
||||
val providers: List<SecurityScoreProviderUM>,
|
||||
val onProviderLinkClick: (SecurityScoreProviderUM) -> Unit,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
data class SecurityScoreProviderUM(
|
||||
val name: String,
|
||||
val lastAuditDate: String?,
|
||||
val score: Float,
|
||||
val urlData: UrlData?,
|
||||
val iconUrl: String?,
|
||||
) {
|
||||
data class UrlData(
|
||||
val fullUrl: String,
|
||||
val rootHost: String?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.state
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class SecurityScoreUM(
|
||||
@FloatRange(from = 0.0, to = 5.0) val score: Float,
|
||||
val description: TextReference,
|
||||
val onInfoClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -38,10 +38,10 @@ 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.market.list.components.MarketsListLazyColumn
|
||||
import com.tangem.features.feed.ui.market.list.components.MarketsListSortByBottomSheet
|
||||
import com.tangem.features.feed.ui.market.list.components.YieldSupplyInMarketsPromoNotification
|
||||
import com.tangem.features.feed.ui.market.list.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -49,7 +49,12 @@ import kotlinx.coroutines.delay
|
|||
private const val SHOW_MORE_KEY = "privacyPolicy"
|
||||
|
||||
@Composable
|
||||
internal fun TopBarWithSearch(onBackClick: () -> Unit, onSearchClick: () -> Unit, marketsSearchBar: MarketsSearchBar) {
|
||||
internal fun TopBarWithSearch(
|
||||
buttonsEnabled: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
marketsSearchBar: MarketsSearchBar,
|
||||
) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val focusRequester: FocusRequester = remember { FocusRequester() }
|
||||
|
||||
|
|
@ -59,6 +64,8 @@ internal fun TopBarWithSearch(onBackClick: () -> Unit, onSearchClick: () -> Unit
|
|||
if (showAppBarWithBackIcon) {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
onBackClick = onBackClick,
|
||||
backButtonEnabled = buttonsEnabled,
|
||||
endButtonEnabled = buttonsEnabled,
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
iconRes = R.drawable.ic_search_24,
|
||||
onIconClick = onSearchClick,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.list.state.ListUM
|
||||
import com.tangem.features.feed.model.market.list.state.ListUM
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
fun MarketsListSortByBottomSheet(config: TangemBottomSheetConfig) {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
private val model: WalletModel = getOrCreateModel()
|
||||
|
||||
private val feedEntryComponent by lazy {
|
||||
feedEntryComponentFactory.create(child("feedEntryComponent"))
|
||||
feedEntryComponentFactory.create(
|
||||
context = child("feedEntryComponent"),
|
||||
entryRoute = null,
|
||||
)
|
||||
}
|
||||
private val marketsEntryComponent by lazy {
|
||||
marketsEntryComponentFactory.create(child("marketsEntryComponent"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue