Updated on 2026-08-14
This commit is contained in:
parent
2396225311
commit
23ddf1a665
34 changed files with 1301 additions and 226 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
18
features/news/news-details/api/build.gradle.kts
Normal file
18
features/news/news-details/api/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
43
features/news/news-details/impl/build.gradle.kts
Normal file
43
features/news/news-details/impl/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -14,4 +14,6 @@ internal enum class TesterScreen {
|
|||
BLOCKCHAIN_PROVIDERS,
|
||||
TEST_PUSHES,
|
||||
ACCOUNTS,
|
||||
NEWS,
|
||||
NEWS_DETAILS,
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue