Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-09 17:38:17 +05:00
parent 2396225311
commit 23ddf1a665
34 changed files with 1301 additions and 226 deletions

View file

@ -1,76 +0,0 @@
package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun ArticleBadge(articleTagUM: ArticleTagUM, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier
.heightIn(min = 24.dp)
.background(
color = TangemTheme.colors.icon.informative.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
when (articleTagUM) {
is ArticleTagUM.Category -> Unit
is ArticleTagUM.Token -> {
CurrencyIcon(
state = articleTagUM.iconState,
shouldDisplayNetwork = false,
iconSize = 16.dp,
)
}
}
Text(
text = articleTagUM.title.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.secondary,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ArticleBadgePreview() {
TangemThemePreview {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ArticleBadge(
articleTagUM = ArticleTagUM.Token(
TextReference.Str("BTC"),
iconState = CurrencyIconState.CoinIcon(
url = "",
fallbackResId = 0,
isGrayscale = false,
shouldShowCustomBadge = false,
),
),
)
ArticleBadge(
articleTagUM = ArticleTagUM.Category(TextReference.Str("Regulation")),
)
}
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -12,24 +11,23 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.collections.immutable.toPersistentList
@Composable
fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) {
@ -121,54 +119,13 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
}
}
@Composable
private fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
val dotColor = TangemTheme.colors.text.secondary
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
contentDescription = null,
)
Text(
text = score.toString(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Spacer(
modifier = Modifier
.size(4.dp)
.drawWithCache {
val radius = size.minDimension / 2f
onDrawBehind {
drawCircle(
color = dotColor,
radius = radius,
)
}
},
)
Text(
text = createdAt,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifier) {
private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
val expandIndicator = remember {
ContextualFlowRowOverflow.expandIndicator {
val remainingItems = tags.size - shownItemCount
ArticleBadge(articleTagUM = ArticleTagUM.Category(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
}
}
ContextualFlowRow(
@ -179,7 +136,7 @@ private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifie
maxLines = 1,
overflow = expandIndicator,
) { index ->
ArticleBadge(articleTagUM = tags[index])
Label(state = tags[index])
}
}
@ -189,14 +146,14 @@ private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifie
private fun TagsPreview() {
TangemThemePreview {
Tags(
tags = listOf(
ArticleTagUM.Category(TextReference.Str("Hype")),
ArticleTagUM.Category(TextReference.Str("BTC")),
ArticleTagUM.Category(TextReference.Str("Supply")),
ArticleTagUM.Category(TextReference.Str("Demand")),
ArticleTagUM.Category(TextReference.Str("Best rate")),
ArticleTagUM.Category(TextReference.Str("Breaking news")),
).toPersistentList(),
tags = persistentListOf(
LabelUM(TextReference.Str("Hype")),
LabelUM(TextReference.Str("BTC")),
LabelUM(TextReference.Str("Supply")),
LabelUM(TextReference.Str("Demand")),
LabelUM(TextReference.Str("Best rate")),
LabelUM(TextReference.Str("Breaking news")),
),
)
}
}
@ -206,12 +163,12 @@ private fun TagsPreview() {
@Composable
private fun ArticleCardsPreview() {
val tags = listOf(
ArticleTagUM.Category(TextReference.Str("Hype")),
ArticleTagUM.Category(TextReference.Str("BTC")),
ArticleTagUM.Category(TextReference.Str("Supply")),
ArticleTagUM.Category(TextReference.Str("Demand")),
ArticleTagUM.Category(TextReference.Str("Best rate")),
ArticleTagUM.Category(TextReference.Str("Breaking news")),
LabelUM(TextReference.Str("Hype")),
LabelUM(TextReference.Str("BTC")),
LabelUM(TextReference.Str("Supply")),
LabelUM(TextReference.Str("Demand")),
LabelUM(TextReference.Str("Best rate")),
LabelUM(TextReference.Str("Breaking news")),
).toImmutableSet()
val config = ArticleConfigUM(

View file

@ -1,5 +1,6 @@
package com.tangem.common.ui.news
import com.tangem.core.ui.components.label.entity.LabelUM
import kotlinx.collections.immutable.ImmutableSet
data class ArticleConfigUM(
@ -8,6 +9,6 @@ data class ArticleConfigUM(
val score: Float,
val createdAt: String,
val isTrending: Boolean,
val tags: ImmutableSet<ArticleTagUM>,
val tags: ImmutableSet<LabelUM>,
val isViewed: Boolean,
)

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.news
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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ArticleHeader(
title: String,
createdAt: String,
score: Float,
tags: ImmutableList<LabelUM>,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
ArticleInfo(
score = score,
createdAt = createdAt,
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = title,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
if (tags.isNotEmpty()) {
Spacer(modifier = Modifier.height(20.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
tags.forEach { tag ->
Label(
state = tag,
)
}
}
}
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
val dotColor = TangemTheme.colors.text.secondary
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
contentDescription = null,
)
Text(
text = score.toString(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Spacer(
modifier = Modifier
.size(4.dp)
.drawWithCache {
val radius = size.minDimension / 2f
onDrawBehind {
drawCircle(
color = dotColor,
radius = radius,
)
}
},
)
Text(
text = createdAt,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.common.ui.news
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed interface ArticleTagUM {
val title: TextReference
data class Category(override val title: TextReference) : ArticleTagUM
data class Token(
override val title: TextReference,
val iconState: CurrencyIconState,
) : ArticleTagUM
}

View file

@ -220,6 +220,8 @@ fun SecondaryButtonIconEnd(
modifier: Modifier = Modifier,
showProgress: Boolean = false,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.Default,
shape: Shape = size.toShape(),
) {
TangemButton(
modifier = modifier,
@ -230,6 +232,8 @@ fun SecondaryButtonIconEnd(
enabled = enabled,
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
size = size,
shape = shape,
)
}
@ -244,6 +248,8 @@ fun SecondaryButtonIconStart(
modifier: Modifier = Modifier,
showProgress: Boolean = false,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.Default,
shape: Shape = size.toShape(),
) {
TangemButton(
modifier = modifier,
@ -254,6 +260,8 @@ fun SecondaryButtonIconStart(
enabled = enabled,
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
size = size,
shape = shape,
)
}
// endregion SecondaryButton

View file

@ -17,11 +17,15 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
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.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelSize
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
@ -37,6 +41,7 @@ import com.tangem.core.ui.res.TangemThemePreview
*
* @see <a href="https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=4480-1459&t=2QTpi1G7FeTexTFS-4">Figma</a>
*/
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun Label(state: LabelUM, modifier: Modifier = Modifier) {
val backgroundColor by animateColorAsState(
@ -63,12 +68,28 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
},
)
AnimatedContent(targetState = state.text) { text ->
val horizontalArrangementSize = remember {
when (state.size) {
LabelSize.REGULAR -> 4.dp
LabelSize.BIG -> 8.dp
}
}
val paddings = remember {
when (state.size) {
LabelSize.REGULAR -> PaddingValues(horizontal = 8.dp, vertical = 4.dp)
LabelSize.BIG -> PaddingValues(horizontal = 16.dp, vertical = 8.dp)
}
}
AnimatedContent(
modifier = modifier,
targetState = state.text,
) { text ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier
.padding(horizontal = 4.dp)
horizontalArrangement = Arrangement.spacedBy(horizontalArrangementSize),
modifier = Modifier
.clip(TangemTheme.shapes.roundedCorners8)
.background(color = backgroundColor)
.then(
@ -82,8 +103,22 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
Modifier
},
)
.padding(horizontal = 8.dp, vertical = 4.dp),
.padding(paddings),
) {
state.leadingContent.let { leadingContentUM ->
when (leadingContentUM) {
is LabelLeadingContentUM.Token -> {
Icon(
modifier = Modifier
.size(16.dp),
painter = painterResource(leadingContentUM.icon),
tint = Color.Unspecified,
contentDescription = null,
)
}
LabelLeadingContentUM.None -> Unit
}
}
Text(
modifier = Modifier.weight(1.0f, fill = false),
text = text.resolveReference(),
@ -109,6 +144,8 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -118,47 +155,93 @@ private fun LabelPreview() {
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp),
) {
Label(
state = LabelUM(
text = TextReference.Str("Regular Label"),
style = LabelStyle.REGULAR,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
),
)
Label(
state = LabelUM(
text = TextReference.Str(
"Regular long long long long long long long long long long long long Label",
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Label(
state = LabelUM(
text = TextReference.Str("Regular Label"),
style = LabelStyle.REGULAR,
),
style = LabelStyle.REGULAR,
icon = R.drawable.ic_information_24,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
icon = R.drawable.ic_information_24,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
icon = R.drawable.ic_information_24,
),
)
)
Label(
state = LabelUM(
leadingContent = LabelLeadingContentUM.Token(
icon = R.drawable.img_bsc_22,
),
text = TextReference.Str("Regular Label"),
style = LabelStyle.REGULAR,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
),
)
}
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Label(
state = LabelUM(
text = TextReference.Str(
"Regular long long long long long long long long long long long long Label",
),
style = LabelStyle.REGULAR,
icon = R.drawable.ic_information_24,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
icon = R.drawable.ic_information_24,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
icon = R.drawable.ic_information_24,
),
)
}
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Label(
state = LabelUM(
text = TextReference.Str("Regular Label"),
style = LabelStyle.REGULAR,
size = LabelSize.BIG,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
size = LabelSize.BIG,
icon = R.drawable.ic_information_24,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
size = LabelSize.BIG,
),
)
}
}
}
}

View file

@ -1,16 +1,29 @@
package com.tangem.core.ui.components.label.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
data class LabelUM(
val text: TextReference,
val style: LabelStyle,
val style: LabelStyle = LabelStyle.REGULAR,
val size: LabelSize = LabelSize.REGULAR,
val leadingContent: LabelLeadingContentUM = LabelLeadingContentUM.None,
@DrawableRes val icon: Int? = null,
val onIconClick: (() -> Unit)? = null,
val onClick: (() -> Unit)? = null,
)
@Immutable
sealed class LabelLeadingContentUM {
data object None : LabelLeadingContentUM()
data class Token(val icon: Int) : LabelLeadingContentUM()
}
enum class LabelStyle {
REGULAR, ACCENT, WARNING,
}
enum class LabelSize {
REGULAR, BIG,
}

View file

@ -0,0 +1,130 @@
package com.tangem.core.ui.components.pager
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
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.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Horizontal pager indicator
*
* @param pagerState state of pager
* @param indicatorCount counter of visible indicator items
*/
@Composable
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) {
val listState = rememberLazyListState()
val indicatorColor = TangemTheme.colors.control.key
val overlayColor = TangemTheme.colors.overlay.secondary
val indicatorSize = 8.dp
val spacing = 4.dp
val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1)
val widthInPx = LocalDensity.current.run { indicatorSize.toPx() }
val currentItem by remember {
derivedStateOf {
pagerState.currentPage
}
}
val itemCount = pagerState.pageCount
LaunchedEffect(key1 = currentItem) {
val viewportSize = listState.layoutInfo.viewportSize
listState.animateScrollToItem(
currentItem,
(widthInPx / 2 - viewportSize.width / 2).toInt(),
)
}
Box(
modifier = modifier
.height(32.dp)
.background(
color = overlayColor,
shape = CircleShape,
)
.padding(horizontal = 16.dp, vertical = 12.dp),
contentAlignment = Alignment.Center,
) {
LazyRow(
modifier = Modifier
.width(totalWidth),
state = listState,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
userScrollEnabled = false,
) {
indicatorItems(
itemCount = itemCount,
currentItem = currentItem,
indicatorShape = CircleShape,
activeColor = indicatorColor,
inActiveColor = indicatorColor.copy(alpha = 0.5f),
indicatorSize = indicatorSize,
)
}
}
}
@Suppress("LongParameterList")
private fun LazyListScope.indicatorItems(
itemCount: Int,
currentItem: Int,
indicatorShape: Shape,
activeColor: Color,
inActiveColor: Color,
indicatorSize: Dp,
) {
items(itemCount) { index ->
val isSelected = index == currentItem
Box(
modifier = Modifier
.clip(indicatorShape)
.size(indicatorSize)
.background(
if (isSelected) activeColor else inActiveColor,
indicatorShape,
),
)
}
}
@Preview(showBackground = true)
@Composable
private fun PagerIndicatorPreviewFirstPage() {
TangemThemePreview {
Box(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.padding(),
contentAlignment = Alignment.Center,
) {
val pagerState = rememberPagerState(
initialPage = 0,
pageCount = { 10 },
)
PagerIndicator(pagerState = pagerState)
}
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M2.667,8C2.667,5.055 5.055,2.667 8,2.667C10.946,2.667 13.333,5.055 13.333,8C13.333,10.946 10.946,13.333 8,13.333C5.055,13.333 2.667,10.946 2.667,8ZM1.333,8C1.333,11.682 4.318,14.667 8,14.667C11.682,14.667 14.667,11.682 14.667,8C14.667,4.318 11.682,1.334 8,1.334C4.318,1.334 1.333,4.318 1.333,8Z"
android:fillColor="#919191"/>
<path
android:pathData="M5.032,11.297C4.984,11.287 4.94,11.263 4.906,11.229C4.871,11.194 4.847,11.15 4.837,11.102C4.827,11.054 4.832,11.005 4.85,10.959L6.315,7.297C6.416,7.045 6.567,6.816 6.758,6.625C6.95,6.433 7.178,6.282 7.429,6.182L11.092,4.717C11.137,4.699 11.188,4.695 11.235,4.704C11.283,4.714 11.328,4.738 11.362,4.772C11.396,4.807 11.42,4.851 11.43,4.899C11.44,4.947 11.435,4.997 11.417,5.042L9.952,8.704C9.851,8.956 9.7,9.185 9.509,9.376C9.317,9.568 9.089,9.718 8.838,9.819L5.175,11.284C5.13,11.302 5.08,11.306 5.032,11.297ZM8.28,8.736C8.425,8.707 8.559,8.636 8.664,8.531C8.768,8.426 8.84,8.292 8.869,8.147C8.898,8.001 8.884,7.85 8.827,7.713C8.77,7.576 8.673,7.46 8.55,7.377C8.427,7.295 8.282,7.251 8.134,7.251C7.935,7.251 7.744,7.33 7.603,7.47C7.463,7.611 7.384,7.802 7.384,8.001C7.384,8.149 7.427,8.294 7.51,8.417C7.592,8.54 7.709,8.637 7.846,8.694C7.983,8.75 8.134,8.765 8.28,8.736Z"
android:fillColor="#919191"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<group>
<clip-path
android:pathData="M0,0h20v20h-20z"/>
<path
android:pathData="M13.75,1.75C16.73,1.75 19.083,4.093 19.084,7.083C19.084,8.9 18.257,10.495 16.961,12.081C15.673,13.657 13.843,15.315 11.713,17.247L10.505,18.347L10,18.806L9.495,18.347L8.288,17.247C6.157,15.315 4.327,13.657 3.039,12.081C1.742,10.495 0.917,8.9 0.917,7.083C0.917,4.093 3.27,1.75 6.25,1.75C7.646,1.75 8.986,2.29 10,3.174C11.013,2.29 12.353,1.75 13.75,1.75Z"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#1E1E1E"/>
</group>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M1.873,2.461C2.205,2.461 2.473,2.73 2.473,3.061V8.464C2.473,9.9 3.638,11.064 5.073,11.064H11.855V10.146C11.855,9.831 12.203,9.639 12.469,9.809L14.858,11.326C15.105,11.483 15.105,11.844 14.858,12.002L12.469,13.519C12.203,13.688 11.855,13.497 11.855,13.181V12.264H5.073C2.975,12.264 1.273,10.562 1.273,8.464V3.061C1.273,2.73 1.542,2.461 1.873,2.461Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M9.473,6.413C9.805,6.413 10.073,6.682 10.073,7.013C10.073,7.345 9.805,7.613 9.473,7.613H5.673C5.342,7.613 5.074,7.345 5.073,7.013C5.073,6.682 5.342,6.413 5.673,6.413H9.473Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M13.273,2.696C13.605,2.696 13.873,2.964 13.873,3.296C13.873,3.627 13.605,3.896 13.273,3.896H5.673C5.342,3.896 5.074,3.627 5.073,3.296C5.073,2.964 5.342,2.696 5.673,2.696H13.273Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -137,11 +138,12 @@ private fun WalletBlock(
vertical = 12.dp,
),
) {
Row {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier
.weight(1f, fill = false)
.padding(end = 8.dp),
.weight(1f, fill = false),
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,

View file

@ -4,12 +4,13 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.common.ui.news.ArticleTagUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.feed.state.*
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
import com.tangem.features.feed.ui.market.state.SortByTypeUM
@ -159,21 +160,18 @@ internal object FeedListPreviewDataProvider {
),
)
private fun createArticleTags(): ImmutableSet<ArticleTagUM> {
private fun createArticleTags(): ImmutableSet<LabelUM> {
return persistentSetOf(
ArticleTagUM.Token(
title = TextReference.Str("BTC"),
iconState = CurrencyIconState.CoinIcon(
url = "",
fallbackResId = 0,
isGrayscale = false,
shouldShowCustomBadge = false,
LabelUM(
text = TextReference.Str("BTC"),
leadingContent = LabelLeadingContentUM.Token(
icon = R.drawable.img_btc_22,
),
),
ArticleTagUM.Category(TextReference.Str("Regulation")),
ArticleTagUM.Category(TextReference.Str("BTC")),
ArticleTagUM.Category(TextReference.Str("Supply")),
ArticleTagUM.Category(TextReference.Str("Demand")),
LabelUM(TextReference.Str("Regulation")),
LabelUM(TextReference.Str("BTC")),
LabelUM(TextReference.Str("Supply")),
LabelUM(TextReference.Str("Demand")),
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.hotwallet.common.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@ -43,11 +44,12 @@ internal fun OptionBlock(
}
.padding(16.dp),
) {
Row {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier
.weight(1f, fill = false)
.padding(end = 4.dp),
.weight(1f, fill = false),
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,

View file

@ -0,0 +1,18 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.news.details.api"
}
dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.news.details.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface NewsDetailsComponent : ComposableContentComponent {
data class Params(val selectedArticleId: Int = 0)
interface Factory : ComponentFactory<Params, NewsDetailsComponent>
}

View file

@ -0,0 +1,43 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.news.details.impl"
}
dependencies {
/* AndroidX */
implementation(deps.lifecycle.compose)
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.material3)
/** Core modules */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.decompose)
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Feature modules */
implementation(projects.features.news.newsDetails.api)
implementation(projects.domain.models)
/** Other dependencies */
implementation(deps.kotlin.immutable.collections)
implementation(deps.arrow.core)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.news.details.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.news.details.api.NewsDetailsComponent
import com.tangem.features.news.details.impl.ui.NewsDetailsContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNewsDetailsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: NewsDetailsComponent.Params,
) : NewsDetailsComponent, AppComponentContext by context {
private val model: NewsDetailsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsState()
NewsDetailsContent(
state = uiState,
onBackClick = model::onBackClick,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : NewsDetailsComponent.Factory {
override fun create(
context: AppComponentContext,
params: NewsDetailsComponent.Params,
): DefaultNewsDetailsComponent
}
}

View file

@ -0,0 +1,182 @@
package com.tangem.features.news.details.impl
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 com.tangem.features.news.details.impl.ui.ArticleUM
import com.tangem.features.news.details.impl.ui.SourceUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
// TODO [REDACTED_TASK_KEY] remove mock data
@Suppress("MaximumLineLength", "LongMethod")
object MockArticlesFactory {
fun createMockArticles(): ImmutableList<ArticleUM> = listOf(
ArticleUM(
id = 1,
title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds",
createdAt = "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",
sourceName = "cointelegraph",
publishedAt = "1h ago",
url = "https://cointelegraph.com",
),
SourceUM(
id = 2,
title = "Top gainers and losers in crypto this week",
sourceName = "Investing",
publishedAt = "2h ago",
url = "https://investing.com",
),
).toPersistentList(),
),
ArticleUM(
id = 2,
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
createdAt = "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",
sourceName = "Bloomberg",
publishedAt = "3h ago",
url = "https://bloomberg.com",
),
).toPersistentList(),
),
ArticleUM(
id = 3,
title = "Ethereum network upgrade scheduled for Q2 2025",
createdAt = "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",
sourceName = "CoinDesk",
publishedAt = "5h ago",
url = "https://coindesk.com",
),
).toPersistentList(),
),
ArticleUM(
id = 4,
title = "Solana surpasses Ethereum in daily transaction volume",
createdAt = "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",
sourceName = "The Block",
publishedAt = "7h ago",
url = "https://theblock.co",
),
).toPersistentList(),
),
ArticleUM(
id = 5,
title = "DeFi protocol launches innovative yield farming strategy",
createdAt = "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 = "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 = "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 = "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 = "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 = "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()
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.news.details.impl
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.features.news.details.api.NewsDetailsComponent
import com.tangem.features.news.details.impl.ui.NewsDetailsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@ModelScoped
internal class NewsDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
) : Model() {
private val mockedArticles = MockArticlesFactory.createMockArticles()
private val params = paramsContainer.require<NewsDetailsComponent.Params>()
private val _uiState = MutableStateFlow(
NewsDetailsUM(
/* [REDACTED_TODO_COMMENT] */
articles = mockedArticles,
selectedArticleIndex = mockedArticles.indexOfFirstOrNull { it.id == params.selectedArticleId } ?: 0,
onShareClick = { /* [REDACTED_TODO_COMMENT] */ },
onLikeClick = { /* [REDACTED_TODO_COMMENT] */ },
),
)
val uiState: StateFlow<NewsDetailsUM> = _uiState.asStateFlow()
fun onBackClick() {
router.pop()
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.news.details.impl.di
import com.tangem.features.news.details.api.NewsDetailsComponent
import com.tangem.features.news.details.impl.DefaultNewsDetailsComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface NewsDetailsModule {
@Binds
@Singleton
fun bindNewsDetailsComponentFactory(factory: DefaultNewsDetailsComponent.Factory): NewsDetailsComponent.Factory
}

View file

@ -0,0 +1,278 @@
package com.tangem.features.news.details.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
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.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.pager.PagerIndicator
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.news.details.impl.MockArticlesFactory
// TODO [REDACTED_TASK_KEY] make internal
@Composable
fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
val pagerState = rememberPagerState(
initialPage = state.selectedArticleIndex,
pageCount = { state.articles.size },
)
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.secondary)
.systemBarsPadding(),
) {
Column {
TangemTopAppBar(
title = null,
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = onBackClick,
),
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_share_24,
onClicked = state.onShareClick,
),
)
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) {
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
) {
PagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp),
)
}
}
}
}
}
}
}
@Suppress("LongMethod")
@Composable
private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onLikeClick: () -> Unit) {
val density = LocalDensity.current
val pagerHeight = 48.dp
val contentPadding = 56.dp
LazyColumn(
modifier = modifier.padding(horizontal = 16.dp),
contentPadding = PaddingValues(
bottom = contentPadding + pagerHeight + WindowInsets.navigationBars.getBottom(density).dp,
),
) {
item {
ArticleHeader(
title = article.title,
createdAt = article.createdAt,
score = article.score,
tags = article.tags,
modifier = Modifier.padding(top = 16.dp),
)
if (article.shortContent.isNotEmpty()) {
QuickRecap(
content = article.shortContent,
modifier = Modifier.padding(top = 32.dp),
)
}
Text(
text = article.content,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(top = 16.dp),
)
Spacer(modifier = Modifier.height(24.dp))
SecondaryButtonIconStart(
iconResId = R.drawable.ic_heart_20,
text = "Like", // TODO [REDACTED_TASK_KEY] export to strings
size = TangemButtonSize.RoundedAction,
onClick = onLikeClick,
)
// TODO [REDACTED_TASK_KEY] add related tokens block
if (article.sources.isNotEmpty()) {
Spacer(modifier = Modifier.height(24.dp))
Row {
Text(
text = "Sources", // TODO [REDACTED_TASK_KEY] export to strings
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "${article.sources.size}",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
if (article.sources.isNotEmpty()) {
item {
val sourcesPagerState = rememberPagerState(
pageCount = { article.sources.size },
)
Spacer(modifier = Modifier.height(12.dp))
HorizontalPager(
state = sourcesPagerState,
modifier = Modifier
.fillMaxWidth(),
pageSpacing = 12.dp,
contentPadding = PaddingValues(horizontal = 0.dp),
) { page ->
SourceItem(
source = article.sources[page],
modifier = Modifier.fillMaxWidth(),
)
}
Spacer(modifier = Modifier.height(12.dp))
}
}
}
}
@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 = "Quick recap", // TODO [REDACTED_TASK_KEY] export to strings
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
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(12.dp),
)
.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(20.dp),
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = source.sourceName,
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
)
}
if (source.title.isNotEmpty()) {
Text(
text = source.title,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(bottom = 12.dp),
)
}
Text(
text = source.publishedAt,
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 = { },
)
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.news.details.impl.ui
import com.tangem.core.ui.components.label.entity.LabelUM
import kotlinx.collections.immutable.ImmutableList
// TODO [REDACTED_TASK_KEY] make internal
data class NewsDetailsUM(
val articles: ImmutableList<ArticleUM>,
val selectedArticleIndex: Int,
val onShareClick: () -> Unit,
val onLikeClick: () -> Unit,
)
// TODO [REDACTED_TASK_KEY] make internal
data class ArticleUM(
val id: Int,
val title: String,
val createdAt: String,
val score: Float,
val tags: ImmutableList<LabelUM>,
val shortContent: String,
val content: String,
val sources: ImmutableList<SourceUM>,
)
// TODO [REDACTED_TASK_KEY] make internal
data class SourceUM(
val id: Int,
val title: String,
val sourceName: String,
val publishedAt: String,
val url: String,
)

View file

@ -59,6 +59,8 @@ dependencies {
/** Feature Apis */
implementation(projects.features.tester.api)
implementation(projects.features.pushNotifications.api)
implementation(projects.features.news.newsDetails.api)
implementation(projects.features.news.newsDetails.impl)
/* SDK */
implementation(tangemDeps.blockchain)

View file

@ -35,6 +35,11 @@ import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersSc
import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel
import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen
import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel
import com.tangem.feature.tester.presentation.news.ui.NewsScreen
import com.tangem.feature.tester.presentation.news.viewmodel.NewsViewModel
import com.tangem.features.news.details.impl.MockArticlesFactory
import com.tangem.features.news.details.impl.ui.NewsDetailsContent
import com.tangem.features.news.details.impl.ui.NewsDetailsUM
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.collections.immutable.persistentSetOf
import javax.inject.Inject
@ -82,9 +87,10 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.TESTER_ACTIONS,
ButtonUM.TEST_PUSHES,
ButtonUM.ACCOUNTS,
ButtonUM.NEWS,
),
onButtonClick = {
val route = when (it) {
onButtonClick = { buttonUM ->
val route = when (buttonUM) {
ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES
ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS
ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES
@ -92,6 +98,7 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS
ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES
ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS
ButtonUM.NEWS -> TesterScreen.NEWS
}
innerTesterRouter.open(route)
@ -162,6 +169,27 @@ internal class TesterActivity : ComposeActivity() {
AccountsScreen(state)
}
composable(route = TesterScreen.NEWS.name) {
val viewModel = hiltViewModel<NewsViewModel>().apply {
setupNavigation(innerTesterRouter)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
NewsScreen(state)
}
composable(route = TesterScreen.NEWS_DETAILS.name) {
NewsDetailsContent(
state = NewsDetailsUM(
articles = MockArticlesFactory.createMockArticles(),
selectedArticleIndex = 0,
onLikeClick = { },
onShareClick = { },
),
onBackClick = { innerTesterRouter.back() },
)
}
}
}
}

View file

@ -25,5 +25,6 @@ data class TesterMenuUM(
TESTER_ACTIONS(R.string.tester_actions),
TEST_PUSHES(R.string.test_push),
ACCOUNTS(R.string.accounts),
NEWS(R.string.news),
}
}

View file

@ -14,4 +14,6 @@ internal enum class TesterScreen {
BLOCKCHAIN_PROVIDERS,
TEST_PUSHES,
ACCOUNTS,
NEWS,
NEWS_DETAILS,
}

View file

@ -0,0 +1,16 @@
package com.tangem.feature.tester.presentation.news.state
import androidx.annotation.StringRes
import com.tangem.feature.tester.impl.R
import kotlinx.collections.immutable.ImmutableSet
data class NewsUM(
val onBackClick: () -> Unit,
val buttons: ImmutableSet<ButtonUM>,
val onButtonClick: (ButtonUM) -> Unit,
) {
enum class ButtonUM(@StringRes val textResId: Int) {
NEWS_DETAILS(R.string.news_details),
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.feature.tester.presentation.news.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.news.state.NewsUM
import kotlinx.collections.immutable.ImmutableSet
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun NewsScreen(state: NewsUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
) {
stickyHeader { AppBar(onBackClick = state.onBackClick) }
NewsButtons(buttons = state.buttons, onButtonClick = state.onButtonClick)
}
}
@Composable
private fun AppBar(onBackClick: () -> Unit) {
AppBarWithBackButton(
onBackClick = onBackClick,
text = stringResourceSafe(id = R.string.news),
containerColor = TangemTheme.colors.background.primary,
)
}
@Suppress("FunctionNaming")
private fun LazyListScope.NewsButtons(
buttons: ImmutableSet<NewsUM.ButtonUM>,
onButtonClick: (NewsUM.ButtonUM) -> Unit,
) {
items(buttons.toList()) { button ->
PrimaryButton(
text = stringResourceSafe(button.textResId),
onClick = { onButtonClick(button) },
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.feature.tester.presentation.news.viewmodel
import androidx.lifecycle.ViewModel
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.feature.tester.presentation.navigation.TesterScreen
import com.tangem.feature.tester.presentation.news.state.NewsUM
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@HiltViewModel
internal class NewsViewModel @Inject constructor() : ViewModel() {
private val _uiState = MutableStateFlow(createInitialState())
val uiState: StateFlow<NewsUM> = _uiState
private var router: InnerTesterRouter? = null
fun setupNavigation(router: InnerTesterRouter) {
this.router = router
}
private fun createInitialState(): NewsUM {
return NewsUM(
onBackClick = ::onBackClick,
buttons = persistentSetOf(
NewsUM.ButtonUM.NEWS_DETAILS,
),
onButtonClick = ::onButtonClick,
)
}
private fun onBackClick() {
router?.back()
}
private fun onButtonClick(button: NewsUM.ButtonUM) {
when (button) {
NewsUM.ButtonUM.NEWS_DETAILS -> router?.open(TesterScreen.NEWS_DETAILS)
}
}
}

View file

@ -18,4 +18,6 @@
<string name="share_logs" translatable="false">Share logs</string>
<string name="test_push" translatable="false">Test push</string>
<string name="accounts" translatable="false">Accounts</string>
<string name="news" translatable="false">News</string>
<string name="news_details" translatable="false">News details</string>
</resources>

View file

@ -300,6 +300,9 @@ include(":features:yield-supply:impl")
include(":features:feed:api")
include(":features:feed:impl")
include(":features:news:news-details:api")
include(":features:news:news-details:impl")
// endregion Feature modules
// region Domain modules