Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-05 08:39:53 +01:00
parent 0d4fd5874e
commit 8cd088188f
20 changed files with 1119 additions and 489 deletions

View file

@ -1,12 +1,12 @@
package com.tangem.features.feed.model.converter
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
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.ui.feed.components.articles.ArticleConfigUM
import com.tangem.features.feed.ui.utils.mapFormattedDate
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
@ -16,7 +16,7 @@ import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
internal class ShortArticleToArticleConfigUMConverter(
private val isTrending: Provider<Boolean>,
private val isTrending: Provider<Boolean>?,
) : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
override fun convert(value: List<ShortArticle>): ImmutableList<ArticleConfigUM> {
@ -25,7 +25,7 @@ internal class ShortArticleToArticleConfigUMConverter(
id = shortArticle.id,
title = shortArticle.title,
score = shortArticle.score,
isTrending = isTrending(),
isTrending = isTrending?.invoke() ?: shortArticle.isTrending,
tags = buildArticleTags(shortArticle),
createdAt = mapFormattedDate(shortArticle.createdAt),
isViewed = shortArticle.viewed,

View file

@ -40,6 +40,7 @@ internal class NewsDetailsConverter(
newsUrl = value.newsUrl,
relatedTokens = value.relatedTokens.toImmutableList(),
isLiked = value.isLiked,
isTrending = value.isTrending,
)
}

View file

@ -1,12 +1,12 @@
package com.tangem.features.feed.model.news.list.statemanager
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.news.model.NewsListBatchingContext
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMConverter
import com.tangem.features.feed.model.converter.distinctBatchesContent
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.PaginationStatus
@ -30,7 +30,7 @@ internal open class NewsListBatchFlowManager(
) {
private val actionsFlow = MutableSharedFlow<BatchAction<Int, NewsListConfig, Nothing>>()
private val converter by lazy {
ShortArticleToArticleConfigUMConverter(isTrending = Provider { false })
ShortArticleToArticleConfigUMConverter(null)
}
private val batchFlow = getNewsListBatchFlowUseCase(

View file

@ -10,12 +10,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onFirstVisible
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.HorizontalFadeWithBlur
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard
@ -25,72 +21,50 @@ import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
@Composable
internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) {
val background = LocalMainBottomSheetColor.current.value
val isRedesignEnabled = LocalRedesignEnabled.current
Box(
modifier = Modifier.fillMaxWidth(),
LazyRow(
modifier = Modifier.background(color = background),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = rememberLazyListState(),
) {
LazyRow(
modifier = Modifier
.conditionalCompose(
condition = isRedesignEnabled,
modifier = {
hazeSourceTangem(zIndex = -1f)
},
itemsIndexed(
items = newsSliderConfig.content,
key = { index, _ -> index },
contentType = { _, _ -> "article" },
) { index, article ->
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
Modifier.onFirstVisible(
minFractionVisible = 0.5f,
callback = newsSliderConfig.callbacks.onSliderScroll,
)
.background(color = background),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = rememberLazyListState(),
) {
itemsIndexed(
items = newsSliderConfig.content,
key = { index, _ -> index },
contentType = { _, _ -> "article" },
) { index, article ->
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
Modifier.onFirstVisible(
minFractionVisible = 0.5f,
callback = newsSliderConfig.callbacks.onSliderScroll,
)
} else {
Modifier
}
ArticleCard(
articleConfigUM = article,
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
modifier = articleModifier
} else {
Modifier
}
ArticleCard(
articleConfigUM = article,
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
modifier = articleModifier
.width(228.dp)
.heightIn(min = 172.dp)
.fillMaxHeight(),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
)
}
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
item(contentType = "show_more") {
ShowMoreArticlesCard(
modifier = Modifier
.width(228.dp)
.heightIn(min = 172.dp)
.fillMaxHeight(),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
.onFirstVisible(
minFractionVisible = 0.5f,
callback = newsSliderConfig.callbacks.onSliderEndReached,
),
onClick = newsSliderConfig.callbacks.onOpenAllNews,
)
}
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
item(contentType = "show_more") {
ShowMoreArticlesCard(
modifier = Modifier
.width(228.dp)
.heightIn(min = 172.dp)
.onFirstVisible(
minFractionVisible = 0.5f,
callback = newsSliderConfig.callbacks.onSliderEndReached,
),
onClick = newsSliderConfig.callbacks.onOpenAllNews,
)
}
}
}
if (isRedesignEnabled) {
HorizontalFadeWithBlur(
modifier = Modifier
.align(Alignment.TopEnd)
.heightIn(min = 172.dp)
.fillMaxHeight()
.width(100.dp),
backgroundColor = background,
)
}
}
}

View file

@ -1,23 +1,69 @@
package com.tangem.features.feed.ui.feed.components.articles
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.ds.badge.TangemBadge
import com.tangem.core.ui.ds.badge.TangemBadgeColor
import com.tangem.core.ui.ds.badge.TangemBadgeIconPosition
import com.tangem.core.ui.ds.badge.TangemBadgeShape
import com.tangem.core.ui.ds.badge.TangemBadgeSize
import com.tangem.core.ui.ds.badge.TangemBadgeType
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ArticleHeader(
isTrending: Boolean,
title: String,
createdAt: String,
score: Float,
tags: ImmutableList<LabelUM>,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
ArticleHeaderV2(
isTrending = isTrending,
title = title,
createdAt = createdAt,
score = score,
tags = tags,
modifier = modifier,
)
} else {
ArticleHeaderV1(
title = title,
createdAt = createdAt,
score = score,
tags = tags,
modifier = modifier,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ArticleHeaderV1(
title: String,
createdAt: String,
score: Float,
@ -52,4 +98,161 @@ fun ArticleHeader(
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ArticleHeaderV2(
isTrending: Boolean,
title: String,
createdAt: String,
score: Float,
tags: ImmutableList<LabelUM>,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Row(
modifier = Modifier
.heightIn(min = 66.dp)
.padding(top = 16.dp),
verticalAlignment = Alignment.Bottom,
) {
DateBlock(
modifier = Modifier.weight(1f),
createdAt = createdAt,
)
SpacerW(30.dp)
VerticalDivider(
modifier = Modifier
.height(46.dp)
.padding(bottom = 4.dp),
color = TangemTheme.colors2.border.neutral.primary,
)
SpacerW(30.dp)
ScoreBlock(
modifier = Modifier.weight(1f),
score = score,
isTrending = isTrending,
)
}
Text(
modifier = Modifier.padding(vertical = 36.dp),
text = title,
style = TangemTheme.typography2.headingBold34,
color = TangemTheme.colors2.text.neutral.primary,
)
if (tags.isNotEmpty()) {
Spacer(modifier = Modifier.height(20.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
tags.forEach { tag ->
TangemBadge(
text = tag.text,
tangemIconUM = when (val content = tag.leadingContent) {
LabelLeadingContentUM.None -> null
is LabelLeadingContentUM.Token -> TangemIconUM.Url(content.iconUrl)
},
shape = TangemBadgeShape.Rounded,
size = TangemBadgeSize.X9,
type = TangemBadgeType.Tinted,
color = TangemBadgeColor.Gray,
iconPosition = when (tag.leadingContent) {
LabelLeadingContentUM.None -> TangemBadgeIconPosition.None
is LabelLeadingContentUM.Token -> TangemBadgeIconPosition.Start
},
)
}
}
}
}
}
@Composable
private fun ScoreBlock(score: Float, isTrending: Boolean, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16),
tint = if (isTrending) {
TangemTheme.colors2.fill.status.attention
} else {
TangemTheme.colors2.graphic.neutral.primary
},
contentDescription = null,
)
Text(
text = score.toString(),
style = TangemTheme.typography2.bodyRegular16,
color = if (isTrending) {
TangemTheme.colors2.text.status.attention
} else {
TangemTheme.colors2.text.neutral.primary
},
)
}
Text(
text = stringResourceSafe(R.string.news_trending_score),
style = TangemTheme.typography2.captionSemibold13,
color = TangemTheme.colors2.text.neutral.tertiary,
)
}
}
@Composable
private fun DateBlock(createdAt: String, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_calendar_20),
tint = TangemTheme.colors2.fill.neutral.primary,
contentDescription = null,
)
Text(
text = createdAt,
style = TangemTheme.typography2.captionSemibold13,
color = TangemTheme.colors2.text.neutral.tertiary,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ArticleHeaderPreviewV1() {
TangemThemePreview {
ArticleHeader(
title = "Something going good!",
createdAt = "1 hour ago",
score = 5.5f,
tags = persistentListOf(),
isTrending = true,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ArticleHeaderPreviewV2() {
TangemThemePreviewRedesign {
ArticleHeader(
title = "Something going good!",
createdAt = "1 hour ago",
score = 5.5f,
tags = persistentListOf(),
isTrending = true,
)
}
}

View file

@ -3,53 +3,25 @@ package com.tangem.features.feed.ui.news.details
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.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.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
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 coil.compose.SubcomposeAsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.pager.PagerIndicator
import com.tangem.core.ui.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.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.*
import com.tangem.features.feed.ui.news.details.components.ArticleDetail
import com.tangem.features.feed.ui.news.details.components.NewsDetailsPlaceholder
import com.tangem.features.feed.ui.news.details.components.RelatedTokensBlock
import com.tangem.features.feed.ui.news.details.state.*
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
import com.tangem.features.feed.ui.news.details.state.MockArticlesFactory
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
@Composable
internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
@ -86,6 +58,7 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif
@Composable
private fun Content(state: NewsDetailsUM, background: Color) {
val isRedesignEnabled = LocalRedesignEnabled.current
val pagerState = rememberPagerState(
initialPage = state.selectedArticleIndex,
pageCount = { state.articles.size },
@ -109,6 +82,12 @@ private fun Content(state: NewsDetailsUM, background: Color) {
Column(
modifier = Modifier
.fillMaxSize()
.conditionalCompose(
condition = isRedesignEnabled,
modifier = {
hazeSourceTangem(zIndex = 1f)
},
)
.background(background),
) {
Box(modifier = Modifier.fillMaxSize()) {
@ -138,242 +117,6 @@ private fun Content(state: NewsDetailsUM, background: Color) {
}
}
@Suppress("LongMethod")
@Composable
private fun ArticleDetail(
article: ArticleUM,
onLikeClick: () -> Unit,
relatedTokensUM: RelatedTokensUM,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val pagerHeight = 32.dp
val contentPadding = pagerHeight + 56.dp + with(density) {
WindowInsets.navigationBars.getBottom(this).div(this.density)
}.dp
Box(modifier = modifier) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = contentPadding),
) {
item("content") {
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 = if (article.isLiked) {
R.drawable.ic_heart_filled_20
} else {
R.drawable.ic_heart_20
},
iconTint = Color.Unspecified,
text = stringResourceSafe(R.string.news_like),
size = TangemButtonSize.RoundedAction,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLikeClick()
},
)
RelatedTokensBlock(
relatedTokensUM = relatedTokensUM,
onItemClick = when (relatedTokensUM) {
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
else -> null
},
modifier = Modifier.padding(horizontal = 16.dp),
)
if (article.relatedArticles.isNotEmpty()) {
SpacerH(24.dp)
Row(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResourceSafe(R.string.news_sources),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
Text(
text = "${article.relatedArticles.size}",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
if (article.relatedArticles.isNotEmpty()) {
item("relatedArticles") {
LazyRow(
modifier = Modifier.padding(vertical = 12.dp),
state = rememberLazyListState(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(
items = article.relatedArticles,
key = RelatedArticleUM::id,
) { article ->
RelatedNewsItem(
relatedArticle = article,
modifier = Modifier.fillParentMaxHeight(),
)
}
}
}
}
}
BottomFade(
modifier = Modifier
.align(Alignment.BottomCenter),
backgroundColor = background,
)
}
}
@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 RelatedNewsItem(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.sizeIn(maxWidth = 256.dp, minHeight = 132.dp)
.background(color = TangemTheme.colors.background.action, shape = RoundedCornerShape(12.dp))
.clickable(onClick = relatedArticle.onClick)
.padding(12.dp),
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Column(modifier = Modifier.weight(1f)) {
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 = relatedArticle.media.name,
style = TangemTheme.typography.caption1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors.text.tertiary,
)
}
if (relatedArticle.title.isNotEmpty()) {
SpacerH(4.dp)
Text(
text = relatedArticle.title,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
}
if (relatedArticle.imageUrl != null) {
SubcomposeAsyncImage(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(4.dp)),
contentScale = ContentScale.Crop,
model = ImageRequest.Builder(context = LocalContext.current)
.data(relatedArticle.imageUrl)
.crossfade(enable = false)
.allowHardware(true)
.memoryCachePolicy(CachePolicy.DISABLED)
.build(),
loading = {
RectangleShimmer(
modifier = Modifier.size(40.dp),
radius = 4.dp,
)
},
error = {},
contentDescription = relatedArticle.media.name,
)
}
}
SpacerHMax()
Text(
text = relatedArticle.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
@ -396,4 +139,28 @@ private fun PreviewNewsDetailsContent() {
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewNewsDetailsContentV2() {
TangemThemePreviewRedesign {
val background = TangemTheme.colors.background.tertiary
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
NewsDetailsContent(
state = NewsDetailsUM(
articlesStateUM = ArticlesStateUM.Content,
articles = MockArticlesFactory.createMockArticles(),
selectedArticleIndex = 0,
onShareClick = {},
onLikeClick = {},
onBackClick = {},
onArticleIndexChanged = {},
),
)
}
}
}

View file

@ -0,0 +1,324 @@
package com.tangem.features.feed.ui.news.details.components
import androidx.compose.foundation.background
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.material3.HorizontalDivider
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.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.BottomFadeWithBlur
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
import com.tangem.features.feed.ui.news.details.state.ArticleUM
import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
@Composable
internal fun ArticleDetail(
article: ArticleUM,
onLikeClick: () -> Unit,
relatedTokensUM: RelatedTokensUM,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
ArticleDetailV2(
article = article,
onLikeClick = onLikeClick,
relatedTokensUM = relatedTokensUM,
modifier = modifier,
)
} else {
ArticleDetailV1(
article = article,
onLikeClick = onLikeClick,
relatedTokensUM = relatedTokensUM,
modifier = modifier,
)
}
}
@Suppress("LongMethod")
@Composable
private fun ArticleDetailV1(
article: ArticleUM,
onLikeClick: () -> Unit,
relatedTokensUM: RelatedTokensUM,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val pagerHeight = 32.dp
val contentPadding = pagerHeight + 56.dp + with(density) {
WindowInsets.navigationBars.getBottom(this).div(this.density)
}.dp
Box(modifier = modifier) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = contentPadding),
) {
item("content") {
ArticleHeader(
title = article.title,
createdAt = article.createdAt.resolveReference(),
score = article.score,
tags = article.tags,
isTrending = article.isTrending,
modifier = Modifier
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
)
if (article.shortContent.isNotEmpty()) {
QuickRecap(
content = article.shortContent,
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 = if (article.isLiked) {
R.drawable.ic_heart_filled_20
} else {
R.drawable.ic_heart_20
},
iconTint = Color.Unspecified,
text = stringResourceSafe(R.string.news_like),
size = TangemButtonSize.RoundedAction,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLikeClick()
},
)
RelatedTokensBlock(
relatedTokensUM = relatedTokensUM,
onItemClick = when (relatedTokensUM) {
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
else -> null
},
modifier = Modifier.padding(horizontal = 16.dp),
)
if (article.relatedArticles.isNotEmpty()) {
SpacerH(24.dp)
Row(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResourceSafe(R.string.news_sources),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
Text(
text = "${article.relatedArticles.size}",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
if (article.relatedArticles.isNotEmpty()) {
item("relatedArticles") {
LazyRow(
modifier = Modifier.padding(vertical = 12.dp),
state = rememberLazyListState(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(
items = article.relatedArticles,
key = RelatedArticleUM::id,
) { article ->
RelatedNewsItem(
relatedArticle = article,
modifier = Modifier.fillParentMaxHeight(),
)
}
}
}
}
}
BottomFade(
modifier = Modifier
.align(Alignment.BottomCenter),
backgroundColor = background,
)
}
}
@Suppress("LongMethod")
@Composable
internal fun ArticleDetailV2(
article: ArticleUM,
onLikeClick: () -> Unit,
relatedTokensUM: RelatedTokensUM,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val pagerHeight = 32.dp
val contentPadding = pagerHeight + 56.dp + with(density) {
WindowInsets.navigationBars.getBottom(this).div(this.density)
}.dp
Box(modifier = modifier) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = 0f)
.background(background),
contentPadding = PaddingValues(bottom = contentPadding),
) {
item("content") {
ArticleHeader(
title = article.title,
createdAt = article.createdAt.resolveReference(),
score = article.score,
tags = article.tags,
isTrending = article.isTrending,
modifier = Modifier
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
)
if (article.shortContent.isNotEmpty()) {
QuickRecap(
content = article.shortContent,
modifier = Modifier
.padding(top = 32.dp)
.padding(horizontal = 16.dp),
)
}
Text(
text = article.content,
style = TangemTheme.typography2.bodyRegular16,
color = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier
.padding(top = 12.dp)
.padding(horizontal = 16.dp),
)
SpacerH(24.dp)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = TangemTheme.colors2.border.neutral.primary,
)
SpacerH(20.dp)
SecondaryTangemButton(
modifier = Modifier.padding(horizontal = 24.dp),
text = resourceReference(R.string.news_like),
size = com.tangem.core.ui.ds.button.TangemButtonSize.X9,
iconRes = if (article.isLiked) {
R.drawable.ic_like_20
} else {
R.drawable.ic_heart_20
},
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLikeClick()
},
shape = TangemButtonShape.Rounded,
)
RelatedTokensBlock(
relatedTokensUM = relatedTokensUM,
onItemClick = when (relatedTokensUM) {
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
else -> null
},
modifier = Modifier.padding(horizontal = 16.dp),
)
if (article.relatedArticles.isNotEmpty()) {
SpacerH(24.dp)
Row(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResourceSafe(R.string.news_sources),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
)
}
}
}
if (article.relatedArticles.isNotEmpty()) {
item("relatedArticles") {
LazyRow(
modifier = Modifier
.padding(vertical = 12.dp),
state = rememberLazyListState(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(
items = article.relatedArticles,
key = RelatedArticleUM::id,
) { article ->
RelatedNewsItem(
relatedArticle = article,
modifier = Modifier.fillParentMaxHeight(),
)
}
}
}
}
}
BottomFadeWithBlur(
modifier = Modifier
.align(Alignment.BottomCenter)
.height(80.dp)
.fillMaxWidth(),
backgroundColor = background,
)
}
}

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
@ -24,6 +25,7 @@ import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun QuickRecap(content: String, modifier: Modifier = Modifier) {
@ -105,20 +107,36 @@ private fun QuickRecapV2(content: String, modifier: Modifier = Modifier) {
Box {
VerticalDivider(
modifier = Modifier.fillMaxHeight(),
modifier = Modifier
.fillMaxHeight()
.padding(start = 10.dp),
thickness = 2.dp,
color = Color(QUICK_RECAP_DIVIDER_COLOR),
)
Text(
modifier = Modifier.padding(start = 16.dp),
modifier = Modifier.padding(start = 20.dp),
text = content,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography2.bodyRegular16,
color = TangemTheme.colors2.text.neutral.primary,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun QuickRecapPreview() {
TangemThemePreviewRedesign {
QuickRecapV2(
content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut" +
" labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " +
"nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit " +
"esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt " +
"in culpa qui officia deserunt mollit anim id est laborum.",
)
}
}
private const val QUICK_RECAP_DIVIDER_COLOR = 0xFFA99FFF
private const val LINEAR_GRADIENT_FIRST_PART = 0xFFA3A0FF
private const val LINEAR_GRADIENT_SECOND_PART = 0xFFF79DFF

View file

@ -0,0 +1,188 @@
package com.tangem.features.feed.ui.news.details.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM
@Composable
internal fun RelatedNewsItem(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
RelatedNewsItemV2(relatedArticle, modifier)
} else {
RelatedNewsItemV1(relatedArticle, modifier)
}
}
@Composable
private fun RelatedNewsItemV1(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.sizeIn(maxWidth = 256.dp, minHeight = 132.dp)
.background(color = TangemTheme.colors.background.action, shape = RoundedCornerShape(12.dp))
.clickable(onClick = relatedArticle.onClick)
.padding(12.dp),
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Column(modifier = Modifier.weight(1f)) {
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 = relatedArticle.media.name,
style = TangemTheme.typography.caption1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors.text.tertiary,
)
}
if (relatedArticle.title.isNotEmpty()) {
SpacerH(4.dp)
Text(
text = relatedArticle.title,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
}
if (relatedArticle.imageUrl != null) {
SubcomposeAsyncImage(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(4.dp)),
contentScale = ContentScale.Crop,
model = ImageRequest.Builder(context = LocalContext.current)
.data(relatedArticle.imageUrl)
.crossfade(enable = false)
.allowHardware(true)
.memoryCachePolicy(CachePolicy.DISABLED)
.build(),
loading = {
RectangleShimmer(
modifier = Modifier.size(40.dp),
radius = 4.dp,
)
},
error = {},
contentDescription = relatedArticle.media.name,
)
}
}
SpacerHMax()
Text(
text = relatedArticle.publishedAt.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Suppress("LongMethod")
@Composable
private fun RelatedNewsItemV2(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.sizeIn(maxWidth = 228.dp, minHeight = 160.dp)
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.clickable(onClick = relatedArticle.onClick)
.border(
width = 1.dp,
color = TangemTheme.colors2.border.neutral.primary,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.padding(16.dp),
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 4.dp)) {
Icon(
painter = painterResource(id = R.drawable.ic_explore_16),
contentDescription = null,
tint = TangemTheme.colors2.markers.iconGray,
modifier = Modifier.size(16.dp),
)
SpacerW(2.dp)
Text(
text = relatedArticle.media.name,
style = TangemTheme.typography2.captionSemibold12,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors2.text.neutral.secondary,
)
}
if (relatedArticle.title.isNotEmpty()) {
SpacerH(8.dp)
Text(
text = relatedArticle.title,
style = TangemTheme.typography2.bodyRegular16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
}
if (relatedArticle.imageUrl != null) {
SubcomposeAsyncImage(
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop,
model = ImageRequest.Builder(context = LocalContext.current)
.data(relatedArticle.imageUrl)
.crossfade(enable = false)
.allowHardware(true)
.memoryCachePolicy(CachePolicy.DISABLED)
.build(),
loading = {
RectangleShimmer(
modifier = Modifier.size(44.dp),
radius = 12.dp,
)
},
error = {},
contentDescription = relatedArticle.media.name,
)
}
}
SpacerHMax()
Text(
text = relatedArticle.publishedAt.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.feed.ui.news.details.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -16,6 +17,7 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.model.news.details.NewsDetailsModel.Companion.RELATED_TOKEN_MAX_COUNT
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
@ -38,11 +40,20 @@ internal fun RelatedTokensBlock(
Column(modifier = modifier) {
SpacerH(40.dp)
Text(
text = stringResourceSafe(R.string.news_related_tokens),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
if (LocalRedesignEnabled.current) {
Text(
modifier = Modifier.padding(horizontal = 8.dp),
text = stringResourceSafe(R.string.news_related_tokens),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
)
} else {
Text(
text = stringResourceSafe(R.string.news_related_tokens),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
}
SpacerH(12.dp)
BlockCard(

View file

@ -11,6 +11,7 @@ import kotlinx.collections.immutable.toPersistentList
internal object MockArticlesFactory {
fun createMockArticles(): ImmutableList<ArticleUM> = listOf(
ArticleUM(
isTrending = false,
id = 1,
title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds",
createdAt = TextReference.Str("20 Jun, 21:45"),
@ -78,6 +79,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
ArticleUM(
id = 3,
@ -107,6 +109,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
ArticleUM(
id = 4,
@ -136,6 +139,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
ArticleUM(
id = 5,
@ -151,6 +155,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
ArticleUM(
id = 6,
@ -167,6 +172,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
ArticleUM(
id = 7,
@ -182,6 +188,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
ArticleUM(
id = 8,
@ -197,6 +204,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
ArticleUM(
id = 9,
@ -213,6 +221,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
ArticleUM(
id = 10,
@ -229,6 +238,7 @@ internal object MockArticlesFactory {
newsUrl = "",
relatedTokens = persistentListOf(),
isLiked = false,
isTrending = false,
),
).toPersistentList()
}

View file

@ -25,6 +25,7 @@ internal sealed interface ArticlesStateUM {
data class LoadingError(val onRetryClicked: () -> Unit) : ArticlesStateUM
}
@Immutable
internal data class ArticleUM(
val id: Int,
val title: String,
@ -37,6 +38,7 @@ internal data class ArticleUM(
val newsUrl: String,
val relatedTokens: ImmutableList<RelatedToken>,
val isLiked: Boolean,
val isTrending: Boolean,
)
internal data class RelatedArticleUM(