Updated on 2026-08-14
This commit is contained in:
parent
636ae3ee50
commit
a71b7ed4ba
43 changed files with 590 additions and 681 deletions
|
|
@ -15,10 +15,13 @@ 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.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryRoute
|
||||
import com.tangem.features.feed.model.feed.FeedModelClickIntents
|
||||
|
|
@ -55,6 +58,9 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
source = "Market",
|
||||
),
|
||||
onBackClicked = { onChildBack() },
|
||||
onArticleClick = { articleId, preselectedArticlesId ->
|
||||
onArticleClick(articleId, preselectedArticlesId)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -73,8 +79,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onArticleClick(articleId: Int) {
|
||||
innerRouter.push(FeedEntryChildFactory.Child.NewsDetails)
|
||||
override fun onArticleClick(articleId: Int, preselectedArticlesId: List<Int>) {
|
||||
innerRouter.push(
|
||||
FeedEntryChildFactory.Child.NewsDetails(
|
||||
params = DefaultNewsDetailsComponent.Params(
|
||||
articleId = articleId,
|
||||
onBackClicked = { onChildBack() },
|
||||
preselectedArticlesId = preselectedArticlesId,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onOpenAllNews() {
|
||||
|
|
@ -121,20 +135,25 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val bottomSheetState = remember {
|
||||
derivedStateOf { BottomSheetState.EXPANDED }
|
||||
}
|
||||
val background = TangemTheme.colors.background.tertiary
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
val bottomSheetState = remember {
|
||||
derivedStateOf { BottomSheetState.EXPANDED }
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
router.pop()
|
||||
}
|
||||
BackHandler {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
EntryContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
stackState = stack.subscribeAsState(),
|
||||
onHeaderSizeChange = {},
|
||||
isOpenedInBottomSheet = false,
|
||||
)
|
||||
EntryContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
stackState = stack.subscribeAsState(),
|
||||
onHeaderSizeChange = {},
|
||||
isOpenedInBottomSheet = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
|
|
@ -157,6 +176,9 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
)
|
||||
},
|
||||
onBackClicked = { router.pop() },
|
||||
onArticleClick = { articleId, preselectedArticlesId ->
|
||||
clickIntents.onArticleClick(articleId, preselectedArticlesId)
|
||||
},
|
||||
),
|
||||
)
|
||||
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
|
||||
|
|
@ -167,6 +189,13 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
shouldAlwaysShowSearchBar = false,
|
||||
),
|
||||
)
|
||||
is FeedEntryRoute.NewsDetail -> FeedEntryChildFactory.Child.NewsDetails(
|
||||
DefaultNewsDetailsComponent.Params(
|
||||
articleId = entryRoute.articleId,
|
||||
onBackClicked = { router.pop() },
|
||||
preselectedArticlesId = entryRoute.preselectedArticlesId,
|
||||
),
|
||||
)
|
||||
null -> FeedEntryChildFactory.Child.Feed
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
|
||||
@Serializable
|
||||
@Immutable
|
||||
data object NewsDetails : Child
|
||||
data class NewsDetails(val params: DefaultNewsDetailsComponent.Params) : Child
|
||||
}
|
||||
|
||||
fun createChild(
|
||||
|
|
@ -67,9 +67,10 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
params = child.params,
|
||||
)
|
||||
}
|
||||
Child.NewsDetails -> {
|
||||
is Child.NewsDetails -> {
|
||||
DefaultNewsDetailsComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = child.params,
|
||||
)
|
||||
}
|
||||
Child.NewsList -> {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ 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
|
||||
|
|
@ -87,13 +86,14 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
MarketsTokenDetailsTopBar(
|
||||
onBackClick = { params.onBackClicked() },
|
||||
isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
shouldShowPriceSubtitle = state.shouldShowPriceSubtitle,
|
||||
tokenName = state.tokenName,
|
||||
tokenPrice = state.priceText,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
backgroundColor = background,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +131,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
val shouldShowPortfolio: Boolean,
|
||||
val analyticsParams: AnalyticsParams?,
|
||||
val onBackClicked: () -> Unit,
|
||||
val onArticleClick: (articleId: Int, preselectedArticlesId: List<Int>) -> Unit,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import com.tangem.domain.onramp.model.OnrampSource
|
|||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.tokens.model.details.NavigationAction
|
||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
|
@ -41,6 +43,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val shareManager: ShareManager,
|
||||
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
|
||||
) {
|
||||
|
||||
private val disabledActionsInDemoMode = buildSet {
|
||||
|
|
@ -186,22 +189,37 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData ->
|
||||
currencyData.userWallet.walletId to currencyData.status
|
||||
}
|
||||
if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) {
|
||||
router.push(
|
||||
AppRoute.YieldSupplyActive(
|
||||
val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull()
|
||||
val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
|
||||
|
||||
when {
|
||||
tokenEnterStatus != null -> router.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
apy = yieldSupplyApy,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router.push(
|
||||
AppRoute.YieldSupplyPromo(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
apy = yieldSupplyApy,
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
navigationAction = NavigationAction.YieldSupply(
|
||||
isActive = isActiveYield,
|
||||
),
|
||||
),
|
||||
)
|
||||
isActiveYield -> {
|
||||
router.push(
|
||||
AppRoute.YieldSupplyActive(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
apy = yieldSupplyApy,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
router.push(
|
||||
AppRoute.YieldSupplyPromo(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
apy = yieldSupplyApy,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ internal sealed class QuickActionUM(
|
|||
data class YieldMode(
|
||||
private val apy: String,
|
||||
) : QuickActionUM(
|
||||
title = resourceReference(R.string.yield_module_start_earning),
|
||||
title = resourceReference(R.string.common_yield_mode),
|
||||
description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)),
|
||||
icon = R.drawable.ic_analytics_up_mini_24,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ internal data class TokenActionsBSContentUM(
|
|||
iconRes = R.drawable.ic_staking_24,
|
||||
),
|
||||
YieldMode(
|
||||
text = resourceReference(R.string.yield_module_start_earning),
|
||||
text = resourceReference(R.string.common_yield_mode),
|
||||
iconRes = R.drawable.ic_analytics_up_mini_24,
|
||||
),
|
||||
;
|
||||
|
|
|
|||
|
|
@ -2,18 +2,63 @@ package com.tangem.features.feed.components.news.details
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.features.feed.model.news.details.NewsDetailsModel
|
||||
import com.tangem.features.feed.ui.news.details.NewsDetailsContent
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal class DefaultNewsDetailsComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {}
|
||||
private val newsDetailsModel = getOrCreateModel<NewsDetailsModel, Params>(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {}
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
|
||||
TangemTopAppBar(
|
||||
containerColor = background,
|
||||
title = null,
|
||||
startButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onClicked = state.onBackClick,
|
||||
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
),
|
||||
endButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClicked = state.onShareClick,
|
||||
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
|
||||
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
|
||||
NewsDetailsContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val articleId: Int,
|
||||
val onBackClicked: () -> Unit,
|
||||
val preselectedArticlesId: List<Int> = emptyList(),
|
||||
val tokenIds: List<String> = emptyList(),
|
||||
val categoryIds: List<Int> = emptyList(),
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ 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 com.tangem.features.feed.model.news.details.NewsDetailsModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -29,4 +30,9 @@ internal interface ModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(MarketsTokenDetailsModel::class)
|
||||
fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(NewsDetailsModel::class)
|
||||
fun provideNewsDetailsModel(model: NewsDetailsModel): Model
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import org.joda.time.DateTime
|
|||
import org.joda.time.DateTimeZone
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.map
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
|
|
@ -311,7 +312,17 @@ internal class FeedComponentModel @Inject constructor(
|
|||
params.feedClickIntents.onMarketOpenClick(sortBy)
|
||||
},
|
||||
onArticleClick = { articleId ->
|
||||
params.feedClickIntents.onArticleClick(articleId)
|
||||
val trendingArticleId = state.value.trendingArticle?.id
|
||||
params.feedClickIntents.onArticleClick(
|
||||
articleId = articleId,
|
||||
preselectedArticlesId = listOfNotNull(trendingArticleId) +
|
||||
when (state.value.news.newsUMState) {
|
||||
NewsUMState.CONTENT -> state.value.news.content.map { it.id }
|
||||
NewsUMState.LOADING,
|
||||
NewsUMState.ERROR,
|
||||
-> emptyList()
|
||||
},
|
||||
)
|
||||
},
|
||||
onOpenAllNews = {
|
||||
params.feedClickIntents.onOpenAllNews()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@ import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
|||
internal interface FeedModelClickIntents {
|
||||
fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency)
|
||||
fun onMarketOpenClick(sortBy: SortByTypeUM?)
|
||||
fun onArticleClick(articleId: Int)
|
||||
fun onArticleClick(articleId: Int, preselectedArticlesId: List<Int> = emptyList())
|
||||
fun onOpenAllNews()
|
||||
}
|
||||
|
|
@ -241,9 +241,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
onShouldShowPriceSubtitleChange = ::onShouldShowPriceSubtitleChange,
|
||||
relatedNews = MarketsTokenDetailsUM.RelatedNews(
|
||||
articles = persistentListOf(),
|
||||
onArticledClicked = {
|
||||
// TODO in [REDACTED_JIRA]
|
||||
},
|
||||
onArticledClicked = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -313,9 +311,16 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
),
|
||||
).onRight { articles ->
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
val relatedNews = relatedNewsConverter.convert(articles)
|
||||
marketsTokenDetailsUM.copy(
|
||||
relatedNews = marketsTokenDetailsUM.relatedNews.copy(
|
||||
articles = relatedNewsConverter.convert(articles),
|
||||
articles = relatedNews,
|
||||
onArticledClicked = { articledId ->
|
||||
params.onArticleClick(
|
||||
/* articledId */ articledId,
|
||||
/* preselectedIds */ relatedNews.map { it.id },
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,14 @@ import com.tangem.common.ui.news.ArticleConfigUM
|
|||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.FormattedDate
|
||||
import com.tangem.core.ui.utils.getFormattedDate
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.features.feed.ui.utils.mapFormattedDate
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
|
||||
|
||||
|
|
@ -51,34 +45,4 @@ class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<Article
|
|||
}
|
||||
return (categoryLabels + tokenLabels).toPersistentSet()
|
||||
}
|
||||
|
||||
private fun mapFormattedDate(createdAt: String): TextReference {
|
||||
val formattedDate = getFormattedDate(
|
||||
createdAt = createdAt,
|
||||
now = DateTime.now(),
|
||||
)
|
||||
return when (formattedDate) {
|
||||
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
|
||||
is FormattedDate.HoursAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_hours_ago,
|
||||
count = formattedDate.hours,
|
||||
formatArgs = wrappedList(formattedDate.hours),
|
||||
)
|
||||
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_minutes_ago,
|
||||
count = formattedDate.minutes,
|
||||
formatArgs = wrappedList(formattedDate.minutes),
|
||||
)
|
||||
is FormattedDate.Today -> TextReference.Combined(
|
||||
refs = WrappedList(
|
||||
data = listOf(
|
||||
TextReference.Res(R.string.common_today),
|
||||
TextReference.Str(StringsSigns.COMA_SIGN),
|
||||
TextReference.Str(StringsSigns.WHITE_SPACE),
|
||||
TextReference.Str(formattedDate.time),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
flow4 = shouldShowYieldModeMarketPromoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
|
||||
),
|
||||
).conflate(),
|
||||
) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo ->
|
||||
MarketsItemsData(
|
||||
items = uiItems,
|
||||
|
|
@ -138,7 +138,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
flow3 = shouldShowYieldModeMarketPromoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
|
||||
),
|
||||
).conflate(),
|
||||
) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo ->
|
||||
MarketsItemsData(
|
||||
items = uiItems,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.features.feed.model.news.details
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
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.domain.news.usecase.ObserveNewsDetailsUseCase
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
import com.tangem.features.feed.model.news.details.converter.NewsDetailsConverter
|
||||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.indexOfFirstOrNull
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class NewsDetailsModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val observeNewsDetailsUseCase: ObserveNewsDetailsUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<DefaultNewsDetailsComponent.Params>()
|
||||
|
||||
private val currentLanguage = Locale.getDefault().language
|
||||
|
||||
private val converter = NewsDetailsConverter(
|
||||
onSourceClick = { url ->
|
||||
urlOpener.openUrl(url)
|
||||
},
|
||||
)
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
NewsDetailsUM(
|
||||
articles = persistentListOf(),
|
||||
selectedArticleIndex = 0,
|
||||
onShareClick = { /* [REDACTED_TODO_COMMENT] */ },
|
||||
onLikeClick = { /* [REDACTED_TODO_COMMENT] */ },
|
||||
onBackClick = params.onBackClicked,
|
||||
onArticleIndexChanged = { /* [REDACTED_TODO_COMMENT] */ },
|
||||
),
|
||||
)
|
||||
|
||||
val state: StateFlow<NewsDetailsUM> = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
if (params.preselectedArticlesId.isNotEmpty()) {
|
||||
handlePreselectedArticles()
|
||||
} else {
|
||||
// TODO handle pagination [REDACTED_TASK_KEY]
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePreselectedArticles() {
|
||||
modelScope.launch {
|
||||
observeNewsDetailsUseCase.prefetch(
|
||||
newsIds = params.preselectedArticlesId,
|
||||
language = currentLanguage,
|
||||
)
|
||||
|
||||
observeNewsDetailsUseCase
|
||||
.invoke()
|
||||
.map { articlesMap ->
|
||||
params.preselectedArticlesId.mapNotNull { articleId ->
|
||||
articlesMap[articleId]?.let { detailedArticle ->
|
||||
converter.convert(detailedArticle)
|
||||
}
|
||||
}
|
||||
}
|
||||
.map { articles ->
|
||||
articles.toImmutableList()
|
||||
}
|
||||
.onEach { articles ->
|
||||
val selectedIndex = articles.indexOfFirstOrNull { it.id == params.articleId } ?: 0
|
||||
_state.update { newsDetailsUM ->
|
||||
newsDetailsUM.copy(
|
||||
articles = articles,
|
||||
selectedArticleIndex = selectedIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.tangem.features.feed.model.news.details.converter
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.FormattedDate
|
||||
import com.tangem.core.ui.utils.getFormattedDate
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.DetailedArticle
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||
import com.tangem.features.feed.ui.news.details.state.Source
|
||||
import com.tangem.features.feed.ui.news.details.state.SourceUM
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.joda.time.DateTime
|
||||
|
||||
@Stable
|
||||
internal class NewsDetailsConverter(
|
||||
private val onSourceClick: (String) -> Unit,
|
||||
) : Converter<DetailedArticle, ArticleUM> {
|
||||
|
||||
override fun convert(value: DetailedArticle): ArticleUM {
|
||||
return ArticleUM(
|
||||
id = value.id,
|
||||
title = value.title,
|
||||
createdAt = mapFormattedDate(value.createdAt),
|
||||
score = value.score,
|
||||
tags = buildTags(value),
|
||||
shortContent = value.shortContent,
|
||||
content = value.content,
|
||||
sources = buildSources(value),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildTags(article: DetailedArticle): ImmutableList<LabelUM> {
|
||||
val categoryLabels = article.categories.map { category ->
|
||||
LabelUM(text = TextReference.Str(category.name))
|
||||
}
|
||||
val tokenLabels = article.relatedTokens.map { token ->
|
||||
LabelUM(
|
||||
text = TextReference.Str(token.symbol),
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
iconUrl = getTokenIconUrlFromDefaultHost(
|
||||
tokenId = CryptoCurrency.RawID(token.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
return (categoryLabels + tokenLabels).toImmutableList()
|
||||
}
|
||||
|
||||
private fun buildSources(article: DetailedArticle): ImmutableList<SourceUM> {
|
||||
return article.originalArticles.map { originalArticle ->
|
||||
SourceUM(
|
||||
id = originalArticle.id,
|
||||
title = originalArticle.title,
|
||||
source = Source(id = originalArticle.source.id, name = originalArticle.source.name),
|
||||
publishedAt = mapFormattedDate(originalArticle.publishedAt),
|
||||
url = originalArticle.url,
|
||||
onClick = { onSourceClick(originalArticle.url) },
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun mapFormattedDate(createdAt: String): TextReference {
|
||||
val formattedDate = getFormattedDate(
|
||||
createdAt = createdAt,
|
||||
now = DateTime.now(),
|
||||
)
|
||||
return when (formattedDate) {
|
||||
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
|
||||
is FormattedDate.HoursAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_hours_ago,
|
||||
count = formattedDate.hours,
|
||||
formatArgs = wrappedList(formattedDate.hours),
|
||||
)
|
||||
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_minutes_ago,
|
||||
count = formattedDate.minutes,
|
||||
formatArgs = wrappedList(formattedDate.minutes),
|
||||
)
|
||||
is FormattedDate.Today -> TextReference.Combined(
|
||||
refs = WrappedList(
|
||||
data = listOf(
|
||||
TextReference.Res(R.string.common_today),
|
||||
TextReference.Str(StringsSigns.COMA_SIGN),
|
||||
TextReference.Str(StringsSigns.WHITE_SPACE),
|
||||
TextReference.Str(formattedDate.time),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -357,7 +357,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = Modifier
|
||||
.height(164.dp)
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
|
|
@ -365,7 +365,9 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
|
||||
item {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier.size(width = 216.dp, height = 164.dp),
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp),
|
||||
onClick = feedListCallbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,22 +4,16 @@ import com.tangem.common.ui.news.ArticleConfigUM
|
|||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.FormattedDate
|
||||
import com.tangem.core.ui.utils.getFormattedDate
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.models.news.TrendingNews
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.utils.mapFormattedDate
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal class TrendingNewsStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
|
|
@ -111,34 +105,4 @@ internal class TrendingNewsStateFactory(
|
|||
}
|
||||
return (categoryLabels + tokenLabels).toPersistentSet()
|
||||
}
|
||||
|
||||
private fun mapFormattedDate(createdAt: String): TextReference {
|
||||
val formattedDate = getFormattedDate(
|
||||
createdAt = createdAt,
|
||||
now = DateTime.now(),
|
||||
)
|
||||
return when (formattedDate) {
|
||||
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
|
||||
is FormattedDate.HoursAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_hours_ago,
|
||||
count = formattedDate.hours,
|
||||
formatArgs = wrappedList(formattedDate.hours),
|
||||
)
|
||||
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_minutes_ago,
|
||||
count = formattedDate.minutes,
|
||||
formatArgs = wrappedList(formattedDate.minutes),
|
||||
)
|
||||
is FormattedDate.Today -> TextReference.Combined(
|
||||
refs = WrappedList(
|
||||
data = listOf(
|
||||
TextReference.Res(R.string.common_today),
|
||||
TextReference.Str(StringsSigns.COMA_SIGN),
|
||||
TextReference.Str(StringsSigns.WHITE_SPACE),
|
||||
TextReference.Str(formattedDate.time),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,14 +142,14 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
|
|||
)
|
||||
}
|
||||
|
||||
val marketsNotification = state.marketsNotificationUM
|
||||
AnimatedVisibility(
|
||||
state.isInSearchMode.not() &&
|
||||
state.list !is ListUM.LoadingError && state.isInSearchMode.not() &&
|
||||
state.selectedSortBy != SortByTypeUM.YieldSupply,
|
||||
) {
|
||||
val wrappedNotification = remember(this) { state.marketsNotificationUM }
|
||||
val showMore = stringResourceSafe(R.string.common_show_more)
|
||||
|
||||
when (marketsNotification) {
|
||||
when (wrappedNotification) {
|
||||
is MarketsNotificationUM.YieldSupplyPromo -> {
|
||||
val description = stringResourceSafe(
|
||||
R.string.markets_yield_supply_banner_description,
|
||||
|
|
@ -165,7 +165,7 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
|
|||
}
|
||||
|
||||
YieldSupplyInMarketsPromoNotification(
|
||||
config = marketsNotification.config.copy(
|
||||
config = wrappedNotification.config.copy(
|
||||
subtitle = clickableDescription,
|
||||
),
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
package com.tangem.features.feed.ui.news.details
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleHeader
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SecondaryButtonIconStart
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.pager.PagerIndicator
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||
import com.tangem.features.feed.ui.news.details.state.MockArticlesFactory
|
||||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
||||
import com.tangem.features.feed.ui.news.details.state.SourceUM
|
||||
|
||||
@Composable
|
||||
internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = state.selectedArticleIndex,
|
||||
pageCount = { state.articles.size },
|
||||
)
|
||||
|
||||
LaunchedEffect(state.selectedArticleIndex) {
|
||||
if (pagerState.currentPage != state.selectedArticleIndex) {
|
||||
pagerState.scrollToPage(state.selectedArticleIndex)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
if (pagerState.currentPage != state.selectedArticleIndex) {
|
||||
state.onArticleIndexChanged(pagerState.currentPage)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(background),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (state.articles.isNotEmpty()) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { page ->
|
||||
ArticleDetail(
|
||||
article = state.articles[page],
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onLikeClick = state.onLikeClick,
|
||||
)
|
||||
}
|
||||
if (state.articles.size > 1) {
|
||||
PagerIndicator(
|
||||
pagerState = pagerState,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onLikeClick: () -> Unit) {
|
||||
val density = LocalDensity.current
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
contentPadding = PaddingValues(bottom = 56.dp + WindowInsets.navigationBars.getBottom(density).dp),
|
||||
) {
|
||||
item {
|
||||
ArticleHeader(
|
||||
title = article.title,
|
||||
createdAt = article.createdAt.resolveReference(),
|
||||
score = article.score,
|
||||
tags = article.tags,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.shortContent.isNotEmpty()) {
|
||||
QuickRecap(
|
||||
content = article.shortContent,
|
||||
modifier = Modifier
|
||||
.padding(top = 32.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = article.content,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
SpacerH(24.dp)
|
||||
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
iconResId = R.drawable.ic_heart_20,
|
||||
text = stringResourceSafe(R.string.news_like),
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
onClick = onLikeClick,
|
||||
)
|
||||
|
||||
// TODO [REDACTED_TASK_KEY] add related tokens block
|
||||
|
||||
if (article.sources.isNotEmpty()) {
|
||||
SpacerH(24.dp)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_sources),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = "${article.sources.size}",
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (article.sources.isNotEmpty()) {
|
||||
item {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(
|
||||
items = article.sources,
|
||||
key = SourceUM::id,
|
||||
) { source ->
|
||||
SourceItem(source = source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuickRecap(content: String, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.height(IntrinsicSize.Min),
|
||||
) {
|
||||
VerticalDivider(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
thickness = 2.dp,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_quick_recap_16),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_quick_recap),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = content,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.widthIn(max = 216.dp)
|
||||
.heightIn(min = 132.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
.clickable(onClick = source.onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_explore_16),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = source.source.name,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
if (source.title.isNotEmpty()) {
|
||||
SpacerH(4.dp)
|
||||
Text(
|
||||
text = source.title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = source.publishedAt.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewNewsDetailsContent() {
|
||||
TangemThemePreview {
|
||||
NewsDetailsContent(
|
||||
state = NewsDetailsUM(
|
||||
articles = MockArticlesFactory.createMockArticles(),
|
||||
selectedArticleIndex = 0,
|
||||
onShareClick = {},
|
||||
onLikeClick = {},
|
||||
onBackClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package com.tangem.features.feed.ui.news.details.state
|
||||
|
||||
import com.tangem.core.ui.components.label.entity.LabelSize
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Suppress("MaximumLineLength", "LongMethod")
|
||||
internal object MockArticlesFactory {
|
||||
fun createMockArticles(): ImmutableList<ArticleUM> = listOf(
|
||||
ArticleUM(
|
||||
id = 1,
|
||||
title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds",
|
||||
createdAt = TextReference.Str("20 Jun, 21:45"),
|
||||
score = 6.5f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "Bitwise has updated its Solana ETF, adding staking and setting a low management fee of 0.20%.",
|
||||
content = "Bitwise Asset Management has revised its filing to launch a Solana ETF, renaming it \"Bitwise Solana Staking ETF\" and setting an exceptionally low management fee of just 0.20%.\n\nThe update comes as the SEC prepares to review several Solana ETF applications.",
|
||||
sources = listOf(
|
||||
SourceUM(
|
||||
id = 1,
|
||||
title = "Deeper liquidity could drive crypto market beyond \$6T",
|
||||
source = Source(
|
||||
id = 11,
|
||||
name = "Coin-telegraph",
|
||||
),
|
||||
publishedAt = TextReference.Str("1h ago"),
|
||||
url = "https://cointelegraph.com",
|
||||
onClick = {},
|
||||
),
|
||||
SourceUM(
|
||||
id = 2,
|
||||
title = "Top gainers and losers in crypto this week",
|
||||
source = Source(
|
||||
id = 10,
|
||||
name = "Investing",
|
||||
),
|
||||
publishedAt = TextReference.Str("2h ago"),
|
||||
url = "https://investing.com",
|
||||
onClick = {},
|
||||
),
|
||||
).toPersistentList(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 2,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
createdAt = TextReference.Str("20 Jun, 20:15"),
|
||||
score = 8.2f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("BTC"), size = LabelSize.BIG),
|
||||
LabelUM(text = TextReference.Str("ETF"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "Bitcoin spot ETFs recorded fourth consecutive day of positive inflows.",
|
||||
content = "Bitcoin spot ETFs continue their impressive streak with \$550 million in net positive inflows.\n\nBlackRock's IBIT led with \$250M.",
|
||||
sources = listOf(
|
||||
SourceUM(
|
||||
id = 3,
|
||||
title = "Bitcoin ETFs see massive inflows",
|
||||
source = Source(
|
||||
id = 12,
|
||||
name = "Bloomberg",
|
||||
),
|
||||
publishedAt = TextReference.Str("3h ago"),
|
||||
url = "https://bloomberg.com",
|
||||
onClick = {},
|
||||
),
|
||||
).toPersistentList(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 3,
|
||||
title = "Ethereum network upgrade scheduled for Q2 2025",
|
||||
createdAt = TextReference.Str("20 Jun, 18:30"),
|
||||
score = 7.8f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("ETH"), size = LabelSize.BIG),
|
||||
LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "Ethereum developers announced major network upgrade.",
|
||||
content = "The Ethereum Foundation announced a significant upgrade for Q2 2025.\n\nKey improvements include EVM enhancements.",
|
||||
sources = listOf(
|
||||
SourceUM(
|
||||
id = 4,
|
||||
title = "Ethereum core devs announce upgrade",
|
||||
source = Source(
|
||||
id = 15,
|
||||
name = "CoinDesk",
|
||||
),
|
||||
publishedAt = TextReference.Str("5h ago"),
|
||||
url = "https://coindesk.com",
|
||||
onClick = {},
|
||||
),
|
||||
).toPersistentList(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 4,
|
||||
title = "Solana surpasses Ethereum in daily transaction volume",
|
||||
createdAt = TextReference.Str("20 Jun, 16:00"),
|
||||
score = 9.1f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("SOL"), size = LabelSize.BIG),
|
||||
LabelUM(text = TextReference.Str("Market"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "Solana achieved new milestone processing more daily transactions than Ethereum.",
|
||||
content = "Solana processed over 50 million transactions in a single day.\n\nDriven by DeFi and NFT activity.",
|
||||
sources = listOf(
|
||||
SourceUM(
|
||||
id = 5,
|
||||
title = "Solana transactions hit record",
|
||||
source = Source(
|
||||
id = 18,
|
||||
name = "Times",
|
||||
),
|
||||
publishedAt = TextReference.Str("7h ago"),
|
||||
url = "https://theblock.co",
|
||||
onClick = {},
|
||||
),
|
||||
).toPersistentList(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 5,
|
||||
title = "DeFi protocol launches innovative yield farming strategy",
|
||||
createdAt = TextReference.Str("20 Jun, 14:20"),
|
||||
score = 6.9f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("DeFi"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "New DeFi protocol introduced innovative yield farming approach.",
|
||||
content = "A newly launched protocol unveiled innovative yield farming mechanism.\n\nAPY rates range from 15% to 30%.",
|
||||
sources = persistentListOf(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 6,
|
||||
title = "Crypto regulation bill advances in US Senate",
|
||||
createdAt = TextReference.Str("20 Jun, 12:45"),
|
||||
score = 8.7f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG),
|
||||
LabelUM(text = TextReference.Str("USA"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "Comprehensive cryptocurrency regulation bill passed Senate Banking Committee.",
|
||||
content = "The US Senate Banking Committee advanced landmark crypto regulation bill.\n\nKey provisions include asset definitions.",
|
||||
sources = persistentListOf(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 7,
|
||||
title = "Major bank announces crypto custody services",
|
||||
createdAt = TextReference.Str("20 Jun, 10:30"),
|
||||
score = 7.3f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Adoption"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "World's largest bank announced cryptocurrency custody services.",
|
||||
content = "Major financial institution announced comprehensive crypto custody services.\n\nSupporting Bitcoin and Ethereum initially.",
|
||||
sources = persistentListOf(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 8,
|
||||
title = "NFT marketplace reports 300% increase in trading volume",
|
||||
createdAt = TextReference.Str("20 Jun, 08:15"),
|
||||
score = 5.8f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("NFT"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "Leading NFT marketplace experienced dramatic surge in trading activity.",
|
||||
content = "Prominent NFT marketplace reported 300% increase in trading volume.\n\nNew features include lower fees.",
|
||||
sources = persistentListOf(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 9,
|
||||
title = "Layer 2 solution achieves 100,000 TPS milestone",
|
||||
createdAt = TextReference.Str("19 Jun, 22:00"),
|
||||
score = 8.5f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG),
|
||||
LabelUM(text = TextReference.Str("L2"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "New Layer 2 scaling solution achieved 100,000 TPS in testing.",
|
||||
content = "Layer 2 solution processed 100,000 transactions per second.\n\nUsing zero-knowledge proof technology.",
|
||||
sources = persistentListOf(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 10,
|
||||
title = "Stablecoin market cap reaches new all-time high",
|
||||
createdAt = TextReference.Str("19 Jun, 19:30"),
|
||||
score = 7.6f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Stablecoins"), size = LabelSize.BIG),
|
||||
LabelUM(text = TextReference.Str("Market"), size = LabelSize.BIG),
|
||||
).toPersistentList(),
|
||||
shortContent = "Total stablecoin market capitalization surpassed \$180 billion.",
|
||||
content = "Stablecoin market cap reached \$180 billion all-time high.\n\nDriven by DeFi activity and institutional adoption.",
|
||||
sources = persistentListOf(),
|
||||
),
|
||||
).toPersistentList()
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.feed.ui.news.details.state
|
||||
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class NewsDetailsUM(
|
||||
val articles: ImmutableList<ArticleUM>,
|
||||
val selectedArticleIndex: Int,
|
||||
val onShareClick: () -> Unit,
|
||||
val onLikeClick: () -> Unit,
|
||||
val onBackClick: () -> Unit,
|
||||
val onArticleIndexChanged: (Int) -> Unit = {},
|
||||
)
|
||||
|
||||
internal data class ArticleUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val createdAt: TextReference,
|
||||
val score: Float,
|
||||
val tags: ImmutableList<LabelUM>,
|
||||
val shortContent: String,
|
||||
val content: String,
|
||||
val sources: ImmutableList<SourceUM>,
|
||||
)
|
||||
|
||||
internal data class SourceUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val source: Source,
|
||||
val publishedAt: TextReference,
|
||||
val url: String,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
internal data class Source(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.feed.ui.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.FormattedDate
|
||||
import com.tangem.core.ui.utils.getFormattedDate
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal fun mapFormattedDate(createdAt: String): TextReference {
|
||||
val formattedDate = getFormattedDate(
|
||||
createdAt = createdAt,
|
||||
now = DateTime.now(),
|
||||
)
|
||||
return when (formattedDate) {
|
||||
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
|
||||
is FormattedDate.HoursAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_hours_ago,
|
||||
count = formattedDate.hours,
|
||||
formatArgs = wrappedList(formattedDate.hours),
|
||||
)
|
||||
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_minutes_ago,
|
||||
count = formattedDate.minutes,
|
||||
formatArgs = wrappedList(formattedDate.minutes),
|
||||
)
|
||||
is FormattedDate.Today -> TextReference.Combined(
|
||||
refs = WrappedList(
|
||||
data = listOf(
|
||||
TextReference.Res(R.string.common_today),
|
||||
TextReference.Str(StringsSigns.COMA_SIGN),
|
||||
TextReference.Str(StringsSigns.WHITE_SPACE),
|
||||
TextReference.Str(formattedDate.time),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue