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

@ -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>