Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-26 11:05:04 +01:00
parent 636ae3ee50
commit a71b7ed4ba
43 changed files with 590 additions and 681 deletions

View file

@ -1,7 +1,10 @@
package com.tangem.common.ui.news package com.tangem.common.ui.news
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -9,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@ -49,6 +53,8 @@ internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = M
Text( Text(
text = createdAt, text = createdAt,
style = TangemTheme.typography.subtitle2, style = TangemTheme.typography.subtitle2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors.text.secondary, color = TangemTheme.colors.text.secondary,
) )
} }

View file

@ -27,15 +27,4 @@ data class NewsRelatedTokenDto(
@Json(name = "id") val id: String, @Json(name = "id") val id: String,
@Json(name = "symbol") val symbol: String, @Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
)
@JsonClass(generateAdapter = true)
data class NewsOriginalArticleDto(
@Json(name = "id") val id: Int,
@Json(name = "title") val title: String,
@Json(name = "sourceName") val sourceName: String,
@Json(name = "language") val language: String,
@Json(name = "publishedAt") val publishedAt: String,
@Json(name = "url") val url: String,
@Json(name = "imageUrl") val imageUrl: String? = null,
) )

View file

@ -17,4 +17,21 @@ data class NewsDetailsResponse(
@Json(name = "shortContent") val shortContent: String, @Json(name = "shortContent") val shortContent: String,
@Json(name = "content") val content: String, @Json(name = "content") val content: String,
@Json(name = "originalArticles") val originalArticles: List<NewsOriginalArticleDto>, @Json(name = "originalArticles") val originalArticles: List<NewsOriginalArticleDto>,
)
@JsonClass(generateAdapter = true)
data class NewsOriginalArticleDto(
@Json(name = "id") val id: Int,
@Json(name = "title") val title: String,
@Json(name = "source") val source: Source,
@Json(name = "language") val language: String,
@Json(name = "publishedAt") val publishedAt: String,
@Json(name = "url") val url: String,
@Json(name = "imageUrl") val imageUrl: String? = null,
)
@JsonClass(generateAdapter = true)
data class Source(
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
) )

View file

@ -4,11 +4,7 @@ import com.tangem.datasource.api.news.models.response.NewsArticleDto
import com.tangem.datasource.api.news.models.response.NewsDetailsResponse import com.tangem.datasource.api.news.models.response.NewsDetailsResponse
import com.tangem.datasource.api.news.models.response.NewsOriginalArticleDto import com.tangem.datasource.api.news.models.response.NewsOriginalArticleDto
import com.tangem.datasource.api.news.models.response.NewsRelatedTokenDto import com.tangem.datasource.api.news.models.response.NewsRelatedTokenDto
import com.tangem.domain.models.news.ArticleCategory import com.tangem.domain.models.news.*
import com.tangem.domain.models.news.DetailedArticle
import com.tangem.domain.models.news.OriginalArticle
import com.tangem.domain.models.news.RelatedToken
import com.tangem.domain.models.news.ShortArticle
internal fun NewsDetailsResponse.toDomainDetailedArticle(): DetailedArticle { internal fun NewsDetailsResponse.toDomainDetailedArticle(): DetailedArticle {
return DetailedArticle( return DetailedArticle(
@ -54,7 +50,10 @@ internal fun NewsOriginalArticleDto.toDomainOriginalArticle(): OriginalArticle {
return OriginalArticle( return OriginalArticle(
id = id, id = id,
title = title, title = title,
sourceName = sourceName, source = Source(
id = source.id,
name = source.name,
),
locale = language, locale = language,
publishedAt = publishedAt, publishedAt = publishedAt,
url = url, url = url,

View file

@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
* @param id - unique identifier of the article * @param id - unique identifier of the article
* @param title - article title * @param title - article title
* @param sourceName - name of original article source * @param source - object of source name and identifier
* @param locale - language of the article * @param locale - language of the article
* @param publishedAt - date of article publishing * @param publishedAt - date of article publishing
* @param url - link to source of original article * @param url - link to source of original article
@ -18,9 +18,15 @@ import kotlinx.serialization.Serializable
data class OriginalArticle( data class OriginalArticle(
val id: Int, val id: Int,
val title: String, val title: String,
val sourceName: String, val source: Source,
val locale: String, val locale: String,
val publishedAt: String, val publishedAt: String,
val url: String, val url: String,
val imageUrl: String?, val imageUrl: String?,
)
@Serializable
data class Source(
val id: Int,
val name: String,
) )

View file

@ -1,5 +1,6 @@
package com.tangem.domain.news.usecase package com.tangem.domain.news.usecase
import arrow.core.Either
import com.tangem.domain.models.news.DetailedArticle import com.tangem.domain.models.news.DetailedArticle
import com.tangem.domain.news.repository.NewsRepository import com.tangem.domain.news.repository.NewsRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -23,7 +24,7 @@ class ObserveNewsDetailsUseCase(
/** /**
* Prefetches the given article ids (can be called with current + next ids for pager preloading). * Prefetches the given article ids (can be called with current + next ids for pager preloading).
*/ */
suspend fun prefetch(newsIds: Collection<Int>, language: String?) { suspend fun prefetch(newsIds: Collection<Int>, language: String?): Either<Throwable, Unit> = Either.catch {
repository.fetchDetailedArticles(newsIds, language) repository.fetchDetailedArticles(newsIds, language)
} }
} }

View file

@ -24,4 +24,7 @@ sealed interface FeedEntryRoute {
@Serializable @Serializable
data object MarketTokenList : FeedEntryRoute data object MarketTokenList : FeedEntryRoute
@Serializable
data class NewsDetail(val articleId: Int, val preselectedArticlesId: List<Int>) : FeedEntryRoute
} }

View file

@ -15,10 +15,13 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.feed.entry.components.FeedEntryRoute import com.tangem.features.feed.entry.components.FeedEntryRoute
import com.tangem.features.feed.model.feed.FeedModelClickIntents import com.tangem.features.feed.model.feed.FeedModelClickIntents
@ -55,6 +58,9 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
source = "Market", source = "Market",
), ),
onBackClicked = { onChildBack() }, onBackClicked = { onChildBack() },
onArticleClick = { articleId, preselectedArticlesId ->
onArticleClick(articleId, preselectedArticlesId)
},
), ),
), ),
) )
@ -73,8 +79,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
) )
} }
override fun onArticleClick(articleId: Int) { override fun onArticleClick(articleId: Int, preselectedArticlesId: List<Int>) {
innerRouter.push(FeedEntryChildFactory.Child.NewsDetails) innerRouter.push(
FeedEntryChildFactory.Child.NewsDetails(
params = DefaultNewsDetailsComponent.Params(
articleId = articleId,
onBackClicked = { onChildBack() },
preselectedArticlesId = preselectedArticlesId,
),
),
)
} }
override fun onOpenAllNews() { override fun onOpenAllNews() {
@ -121,20 +135,25 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
@Composable @Composable
override fun Content(modifier: Modifier) { override fun Content(modifier: Modifier) {
val bottomSheetState = remember { val background = TangemTheme.colors.background.tertiary
derivedStateOf { BottomSheetState.EXPANDED } CompositionLocalProvider(
} LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
val bottomSheetState = remember {
derivedStateOf { BottomSheetState.EXPANDED }
}
BackHandler { BackHandler {
router.pop() router.pop()
} }
EntryContent( EntryContent(
bottomSheetState = bottomSheetState, bottomSheetState = bottomSheetState,
stackState = stack.subscribeAsState(), stackState = stack.subscribeAsState(),
onHeaderSizeChange = {}, onHeaderSizeChange = {},
isOpenedInBottomSheet = false, isOpenedInBottomSheet = false,
) )
}
} }
private fun onChildBack() { private fun onChildBack() {
@ -157,6 +176,9 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
) )
}, },
onBackClicked = { router.pop() }, onBackClicked = { router.pop() },
onArticleClick = { articleId, preselectedArticlesId ->
clickIntents.onArticleClick(articleId, preselectedArticlesId)
},
), ),
) )
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList( FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
@ -167,6 +189,13 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
shouldAlwaysShowSearchBar = false, shouldAlwaysShowSearchBar = false,
), ),
) )
is FeedEntryRoute.NewsDetail -> FeedEntryChildFactory.Child.NewsDetails(
DefaultNewsDetailsComponent.Params(
articleId = entryRoute.articleId,
onBackClicked = { router.pop() },
preselectedArticlesId = entryRoute.preselectedArticlesId,
),
)
null -> FeedEntryChildFactory.Child.Feed null -> FeedEntryChildFactory.Child.Feed
} }
} }

View file

@ -43,7 +43,7 @@ internal class FeedEntryChildFactory @Inject constructor(
@Serializable @Serializable
@Immutable @Immutable
data object NewsDetails : Child data class NewsDetails(val params: DefaultNewsDetailsComponent.Params) : Child
} }
fun createChild( fun createChild(
@ -67,9 +67,10 @@ internal class FeedEntryChildFactory @Inject constructor(
params = child.params, params = child.params,
) )
} }
Child.NewsDetails -> { is Child.NewsDetails -> {
DefaultNewsDetailsComponent( DefaultNewsDetailsComponent(
appComponentContext = appComponentContext, appComponentContext = appComponentContext,
params = child.params,
) )
} }
Child.NewsList -> { Child.NewsList -> {

View file

@ -15,7 +15,6 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.markets.TokenMarketParams
@ -87,13 +86,14 @@ internal class DefaultMarketsTokenDetailsComponent(
@Composable @Composable
override fun Title(bottomSheetState: State<BottomSheetState>) { override fun Title(bottomSheetState: State<BottomSheetState>) {
val state by model.state.collectAsStateWithLifecycle() val state by model.state.collectAsStateWithLifecycle()
val background = LocalMainBottomSheetColor.current.value
MarketsTokenDetailsTopBar( MarketsTokenDetailsTopBar(
onBackClick = { params.onBackClicked() }, onBackClick = { params.onBackClicked() },
isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
shouldShowPriceSubtitle = state.shouldShowPriceSubtitle, shouldShowPriceSubtitle = state.shouldShowPriceSubtitle,
tokenName = state.tokenName, tokenName = state.tokenName,
tokenPrice = state.priceText, tokenPrice = state.priceText,
backgroundColor = TangemTheme.colors.background.tertiary, backgroundColor = background,
) )
} }
@ -131,6 +131,7 @@ internal class DefaultMarketsTokenDetailsComponent(
val shouldShowPortfolio: Boolean, val shouldShowPortfolio: Boolean,
val analyticsParams: AnalyticsParams?, val analyticsParams: AnalyticsParams?,
val onBackClicked: () -> Unit, val onBackClicked: () -> Unit,
val onArticleClick: (articleId: Int, preselectedArticlesId: List<Int>) -> Unit,
) )
@Serializable @Serializable

View file

@ -20,6 +20,8 @@ import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.feed.impl.R import com.tangem.features.feed.impl.R
@ -41,6 +43,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase, private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender, private val messageSender: UiMessageSender,
private val shareManager: ShareManager, private val shareManager: ShareManager,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
) { ) {
private val disabledActionsInDemoMode = buildSet { private val disabledActionsInDemoMode = buildSet {
@ -186,22 +189,37 @@ internal class TokenActionsHandler @AssistedInject constructor(
val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData -> val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData ->
currencyData.userWallet.walletId to currencyData.status currencyData.userWallet.walletId to currencyData.status
} }
if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) { val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull()
router.push( val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
AppRoute.YieldSupplyActive(
when {
tokenEnterStatus != null -> router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId, userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency, currency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy, navigationAction = NavigationAction.YieldSupply(
), isActive = isActiveYield,
) ),
} else {
router.push(
AppRoute.YieldSupplyPromo(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
), ),
) )
isActiveYield -> {
router.push(
AppRoute.YieldSupplyActive(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
),
)
}
else -> {
router.push(
AppRoute.YieldSupplyPromo(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
),
)
}
} }
} }

View file

@ -44,7 +44,7 @@ internal sealed class QuickActionUM(
data class YieldMode( data class YieldMode(
private val apy: String, private val apy: String,
) : QuickActionUM( ) : QuickActionUM(
title = resourceReference(R.string.yield_module_start_earning), title = resourceReference(R.string.common_yield_mode),
description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)),
icon = R.drawable.ic_analytics_up_mini_24, icon = R.drawable.ic_analytics_up_mini_24,
) )

View file

@ -48,7 +48,7 @@ internal data class TokenActionsBSContentUM(
iconRes = R.drawable.ic_staking_24, iconRes = R.drawable.ic_staking_24,
), ),
YieldMode( YieldMode(
text = resourceReference(R.string.yield_module_start_earning), text = resourceReference(R.string.common_yield_mode),
iconRes = R.drawable.ic_analytics_up_mini_24, iconRes = R.drawable.ic_analytics_up_mini_24,
), ),
; ;

View file

@ -2,18 +2,63 @@ package com.tangem.features.feed.components.news.details
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.features.feed.model.news.details.NewsDetailsModel
import com.tangem.features.feed.ui.news.details.NewsDetailsContent
import kotlinx.serialization.Serializable
internal class DefaultNewsDetailsComponent( internal class DefaultNewsDetailsComponent(
appComponentContext: AppComponentContext, appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
@Composable private val newsDetailsModel = getOrCreateModel<NewsDetailsModel, Params>(params = params)
override fun Title(bottomSheetState: State<BottomSheetState>) {}
@Composable @Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {} override fun Title(bottomSheetState: State<BottomSheetState>) {
val background = LocalMainBottomSheetColor.current.value
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
TangemTopAppBar(
containerColor = background,
title = null,
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_share_24,
onClicked = state.onShareClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
)
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
NewsDetailsContent(
state = state,
modifier = modifier,
)
}
@Serializable
data class Params(
val articleId: Int,
val onBackClicked: () -> Unit,
val preselectedArticlesId: List<Int> = emptyList(),
val tokenIds: List<String> = emptyList(),
val categoryIds: List<Int> = emptyList(),
)
} }

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.feed.FeedComponentModel
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
import com.tangem.features.feed.model.market.list.MarketsListModel import com.tangem.features.feed.model.market.list.MarketsListModel
import com.tangem.features.feed.model.news.details.NewsDetailsModel
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -29,4 +30,9 @@ internal interface ModelModule {
@IntoMap @IntoMap
@ClassKey(MarketsTokenDetailsModel::class) @ClassKey(MarketsTokenDetailsModel::class)
fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model
@Binds
@IntoMap
@ClassKey(NewsDetailsModel::class)
fun provideNewsDetailsModel(model: NewsDetailsModel): Model
} }

View file

@ -31,6 +31,7 @@ import org.joda.time.DateTime
import org.joda.time.DateTimeZone import org.joda.time.DateTimeZone
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.all import kotlin.collections.all
import kotlin.collections.map
@Stable @Stable
@ModelScoped @ModelScoped
@ -311,7 +312,17 @@ internal class FeedComponentModel @Inject constructor(
params.feedClickIntents.onMarketOpenClick(sortBy) params.feedClickIntents.onMarketOpenClick(sortBy)
}, },
onArticleClick = { articleId -> onArticleClick = { articleId ->
params.feedClickIntents.onArticleClick(articleId) val trendingArticleId = state.value.trendingArticle?.id
params.feedClickIntents.onArticleClick(
articleId = articleId,
preselectedArticlesId = listOfNotNull(trendingArticleId) +
when (state.value.news.newsUMState) {
NewsUMState.CONTENT -> state.value.news.content.map { it.id }
NewsUMState.LOADING,
NewsUMState.ERROR,
-> emptyList()
},
)
}, },
onOpenAllNews = { onOpenAllNews = {
params.feedClickIntents.onOpenAllNews() params.feedClickIntents.onOpenAllNews()

View file

@ -10,6 +10,6 @@ import com.tangem.features.feed.model.market.list.state.SortByTypeUM
internal interface FeedModelClickIntents { internal interface FeedModelClickIntents {
fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency) fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency)
fun onMarketOpenClick(sortBy: SortByTypeUM?) fun onMarketOpenClick(sortBy: SortByTypeUM?)
fun onArticleClick(articleId: Int) fun onArticleClick(articleId: Int, preselectedArticlesId: List<Int> = emptyList())
fun onOpenAllNews() fun onOpenAllNews()
} }

View file

@ -241,9 +241,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
onShouldShowPriceSubtitleChange = ::onShouldShowPriceSubtitleChange, onShouldShowPriceSubtitleChange = ::onShouldShowPriceSubtitleChange,
relatedNews = MarketsTokenDetailsUM.RelatedNews( relatedNews = MarketsTokenDetailsUM.RelatedNews(
articles = persistentListOf(), articles = persistentListOf(),
onArticledClicked = { onArticledClicked = {},
// TODO in [REDACTED_JIRA]
},
), ),
), ),
) )
@ -313,9 +311,16 @@ internal class MarketsTokenDetailsModel @Inject constructor(
), ),
).onRight { articles -> ).onRight { articles ->
state.update { marketsTokenDetailsUM -> state.update { marketsTokenDetailsUM ->
val relatedNews = relatedNewsConverter.convert(articles)
marketsTokenDetailsUM.copy( marketsTokenDetailsUM.copy(
relatedNews = marketsTokenDetailsUM.relatedNews.copy( relatedNews = marketsTokenDetailsUM.relatedNews.copy(
articles = relatedNewsConverter.convert(articles), articles = relatedNews,
onArticledClicked = { articledId ->
params.onArticleClick(
/* articledId */ articledId,
/* preselectedIds */ relatedNews.map { it.id },
)
},
), ),
) )
} }

View file

@ -4,20 +4,14 @@ import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.FormattedDate
import com.tangem.core.ui.utils.getFormattedDate
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.ShortArticle import com.tangem.domain.models.news.ShortArticle
import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.utils.mapFormattedDate
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet import kotlinx.collections.immutable.toPersistentSet
import org.joda.time.DateTime
class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> { class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
@ -51,34 +45,4 @@ class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<Article
} }
return (categoryLabels + tokenLabels).toPersistentSet() return (categoryLabels + tokenLabels).toPersistentSet()
} }
private fun mapFormattedDate(createdAt: String): TextReference {
val formattedDate = getFormattedDate(
createdAt = createdAt,
now = DateTime.now(),
)
return when (formattedDate) {
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
is FormattedDate.HoursAgo -> TextReference.PluralRes(
id = R.plurals.news_published_hours_ago,
count = formattedDate.hours,
formatArgs = wrappedList(formattedDate.hours),
)
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
id = R.plurals.news_published_minutes_ago,
count = formattedDate.minutes,
formatArgs = wrappedList(formattedDate.minutes),
)
is FormattedDate.Today -> TextReference.Combined(
refs = WrappedList(
data = listOf(
TextReference.Res(R.string.common_today),
TextReference.Str(StringsSigns.COMA_SIGN),
TextReference.Str(StringsSigns.WHITE_SPACE),
TextReference.Str(formattedDate.time),
),
),
)
}
}
} }

View file

@ -122,7 +122,7 @@ internal class MarketsListModel @Inject constructor(
flow4 = shouldShowYieldModeMarketPromoUseCase( flow4 = shouldShowYieldModeMarketPromoUseCase(
appCurrency = currentAppCurrency.value, appCurrency = currentAppCurrency.value,
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
), ).conflate(),
) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo -> ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo ->
MarketsItemsData( MarketsItemsData(
items = uiItems, items = uiItems,
@ -138,7 +138,7 @@ internal class MarketsListModel @Inject constructor(
flow3 = shouldShowYieldModeMarketPromoUseCase( flow3 = shouldShowYieldModeMarketPromoUseCase(
appCurrency = currentAppCurrency.value, appCurrency = currentAppCurrency.value,
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
), ).conflate(),
) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo -> ) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo ->
MarketsItemsData( MarketsItemsData(
items = uiItems, items = uiItems,

View file

@ -0,0 +1,93 @@
package com.tangem.features.feed.model.news.details
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.model.news.details.converter.NewsDetailsConverter
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.util.Locale
import javax.inject.Inject
@Stable
@ModelScoped
@Suppress("LongParameterList")
internal class NewsDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val observeNewsDetailsUseCase: ObserveNewsDetailsUseCase,
private val urlOpener: UrlOpener,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<DefaultNewsDetailsComponent.Params>()
private val currentLanguage = Locale.getDefault().language
private val converter = NewsDetailsConverter(
onSourceClick = { url ->
urlOpener.openUrl(url)
},
)
private val _state = MutableStateFlow(
NewsDetailsUM(
articles = persistentListOf(),
selectedArticleIndex = 0,
onShareClick = { /* [REDACTED_TODO_COMMENT] */ },
onLikeClick = { /* [REDACTED_TODO_COMMENT] */ },
onBackClick = params.onBackClicked,
onArticleIndexChanged = { /* [REDACTED_TODO_COMMENT] */ },
),
)
val state: StateFlow<NewsDetailsUM> = _state.asStateFlow()
init {
if (params.preselectedArticlesId.isNotEmpty()) {
handlePreselectedArticles()
} else {
// TODO handle pagination [REDACTED_TASK_KEY]
}
}
private fun handlePreselectedArticles() {
modelScope.launch {
observeNewsDetailsUseCase.prefetch(
newsIds = params.preselectedArticlesId,
language = currentLanguage,
)
observeNewsDetailsUseCase
.invoke()
.map { articlesMap ->
params.preselectedArticlesId.mapNotNull { articleId ->
articlesMap[articleId]?.let { detailedArticle ->
converter.convert(detailedArticle)
}
}
}
.map { articles ->
articles.toImmutableList()
}
.onEach { articles ->
val selectedIndex = articles.indexOfFirstOrNull { it.id == params.articleId } ?: 0
_state.update { newsDetailsUM ->
newsDetailsUM.copy(
articles = articles,
selectedArticleIndex = selectedIndex,
)
}
}
.launchIn(modelScope)
}
}
}

View file

@ -0,0 +1,101 @@
package com.tangem.features.feed.model.news.details.converter
import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.FormattedDate
import com.tangem.core.ui.utils.getFormattedDate
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.DetailedArticle
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.news.details.state.ArticleUM
import com.tangem.features.feed.ui.news.details.state.Source
import com.tangem.features.feed.ui.news.details.state.SourceUM
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.joda.time.DateTime
@Stable
internal class NewsDetailsConverter(
private val onSourceClick: (String) -> Unit,
) : Converter<DetailedArticle, ArticleUM> {
override fun convert(value: DetailedArticle): ArticleUM {
return ArticleUM(
id = value.id,
title = value.title,
createdAt = mapFormattedDate(value.createdAt),
score = value.score,
tags = buildTags(value),
shortContent = value.shortContent,
content = value.content,
sources = buildSources(value),
)
}
private fun buildTags(article: DetailedArticle): ImmutableList<LabelUM> {
val categoryLabels = article.categories.map { category ->
LabelUM(text = TextReference.Str(category.name))
}
val tokenLabels = article.relatedTokens.map { token ->
LabelUM(
text = TextReference.Str(token.symbol),
leadingContent = LabelLeadingContentUM.Token(
iconUrl = getTokenIconUrlFromDefaultHost(
tokenId = CryptoCurrency.RawID(token.id),
),
),
)
}
return (categoryLabels + tokenLabels).toImmutableList()
}
private fun buildSources(article: DetailedArticle): ImmutableList<SourceUM> {
return article.originalArticles.map { originalArticle ->
SourceUM(
id = originalArticle.id,
title = originalArticle.title,
source = Source(id = originalArticle.source.id, name = originalArticle.source.name),
publishedAt = mapFormattedDate(originalArticle.publishedAt),
url = originalArticle.url,
onClick = { onSourceClick(originalArticle.url) },
)
}.toImmutableList()
}
private fun mapFormattedDate(createdAt: String): TextReference {
val formattedDate = getFormattedDate(
createdAt = createdAt,
now = DateTime.now(),
)
return when (formattedDate) {
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
is FormattedDate.HoursAgo -> TextReference.PluralRes(
id = R.plurals.news_published_hours_ago,
count = formattedDate.hours,
formatArgs = wrappedList(formattedDate.hours),
)
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
id = R.plurals.news_published_minutes_ago,
count = formattedDate.minutes,
formatArgs = wrappedList(formattedDate.minutes),
)
is FormattedDate.Today -> TextReference.Combined(
refs = WrappedList(
data = listOf(
TextReference.Res(R.string.common_today),
TextReference.Str(StringsSigns.COMA_SIGN),
TextReference.Str(StringsSigns.WHITE_SPACE),
TextReference.Str(formattedDate.time),
),
),
)
}
}
}

View file

@ -357,7 +357,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
articleConfigUM = article, articleConfigUM = article,
onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
modifier = Modifier modifier = Modifier
.height(164.dp) .heightIn(min = 164.dp)
.width(216.dp), .width(216.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) )
@ -365,7 +365,9 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
item { item {
ShowMoreArticlesCard( ShowMoreArticlesCard(
modifier = Modifier.size(width = 216.dp, height = 164.dp), modifier = Modifier
.width(216.dp)
.heightIn(min = 164.dp),
onClick = feedListCallbacks.onOpenAllNews, onClick = feedListCallbacks.onOpenAllNews,
) )
} }

View file

@ -4,22 +4,16 @@ import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.FormattedDate
import com.tangem.core.ui.utils.getFormattedDate
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.ShortArticle import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.models.news.TrendingNews import com.tangem.domain.models.news.TrendingNews
import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.utils.mapFormattedDate
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet import kotlinx.collections.immutable.toPersistentSet
import org.joda.time.DateTime
internal class TrendingNewsStateFactory( internal class TrendingNewsStateFactory(
private val currentStateProvider: Provider<FeedListUM>, private val currentStateProvider: Provider<FeedListUM>,
@ -111,34 +105,4 @@ internal class TrendingNewsStateFactory(
} }
return (categoryLabels + tokenLabels).toPersistentSet() return (categoryLabels + tokenLabels).toPersistentSet()
} }
private fun mapFormattedDate(createdAt: String): TextReference {
val formattedDate = getFormattedDate(
createdAt = createdAt,
now = DateTime.now(),
)
return when (formattedDate) {
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
is FormattedDate.HoursAgo -> TextReference.PluralRes(
id = R.plurals.news_published_hours_ago,
count = formattedDate.hours,
formatArgs = wrappedList(formattedDate.hours),
)
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
id = R.plurals.news_published_minutes_ago,
count = formattedDate.minutes,
formatArgs = wrappedList(formattedDate.minutes),
)
is FormattedDate.Today -> TextReference.Combined(
refs = WrappedList(
data = listOf(
TextReference.Res(R.string.common_today),
TextReference.Str(StringsSigns.COMA_SIGN),
TextReference.Str(StringsSigns.WHITE_SPACE),
TextReference.Str(formattedDate.time),
),
),
)
}
}
} }

View file

@ -142,14 +142,14 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
) )
} }
val marketsNotification = state.marketsNotificationUM
AnimatedVisibility( AnimatedVisibility(
state.isInSearchMode.not() && state.list !is ListUM.LoadingError && state.isInSearchMode.not() &&
state.selectedSortBy != SortByTypeUM.YieldSupply, state.selectedSortBy != SortByTypeUM.YieldSupply,
) { ) {
val wrappedNotification = remember(this) { state.marketsNotificationUM }
val showMore = stringResourceSafe(R.string.common_show_more) val showMore = stringResourceSafe(R.string.common_show_more)
when (marketsNotification) { when (wrappedNotification) {
is MarketsNotificationUM.YieldSupplyPromo -> { is MarketsNotificationUM.YieldSupplyPromo -> {
val description = stringResourceSafe( val description = stringResourceSafe(
R.string.markets_yield_supply_banner_description, R.string.markets_yield_supply_banner_description,
@ -165,7 +165,7 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
} }
YieldSupplyInMarketsPromoNotification( YieldSupplyInMarketsPromoNotification(
config = marketsNotification.config.copy( config = wrappedNotification.config.copy(
subtitle = clickableDescription, subtitle = clickableDescription,
), ),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),

View file

@ -1,9 +1,13 @@
package com.tangem.features.news.details.impl.ui package com.tangem.features.feed.ui.news.details
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -11,57 +15,57 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.common.ui.news.ArticleHeader import com.tangem.common.ui.news.ArticleHeader
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.pager.PagerIndicator import com.tangem.core.ui.components.pager.PagerIndicator
import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.news.details.impl.MockArticlesFactory import com.tangem.features.feed.ui.news.details.state.ArticleUM
import com.tangem.features.feed.ui.news.details.state.MockArticlesFactory
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
import com.tangem.features.feed.ui.news.details.state.SourceUM
// TODO [REDACTED_TASK_KEY] make internal
@Composable @Composable
fun NewsDetailsContent( internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
state: NewsDetailsUM, val background = LocalMainBottomSheetColor.current.value
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
isBottomSheetMode: Boolean = false,
) {
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = state.selectedArticleIndex, initialPage = state.selectedArticleIndex,
pageCount = { state.articles.size }, pageCount = { state.articles.size },
) )
LaunchedEffect(state.selectedArticleIndex) {
if (pagerState.currentPage != state.selectedArticleIndex) {
pagerState.scrollToPage(state.selectedArticleIndex)
}
}
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != state.selectedArticleIndex) {
state.onArticleIndexChanged(pagerState.currentPage)
}
}
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(TangemTheme.colors.background.tertiary) .background(background),
.conditionalCompose(isBottomSheetMode) { systemBarsPadding() },
) { ) {
TangemTopAppBar( Box(modifier = Modifier.fillMaxSize()) {
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()) { if (state.articles.isNotEmpty()) {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
@ -73,20 +77,13 @@ fun NewsDetailsContent(
onLikeClick = state.onLikeClick, onLikeClick = state.onLikeClick,
) )
} }
if (state.articles.size > 1) { if (state.articles.size > 1) {
Column( PagerIndicator(
pagerState = pagerState,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.fillMaxWidth(), .windowInsetsPadding(WindowInsets.navigationBars),
) { )
PagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp),
)
}
} }
} }
} }
@ -97,27 +94,27 @@ fun NewsDetailsContent(
@Composable @Composable
private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onLikeClick: () -> Unit) { private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onLikeClick: () -> Unit) {
val density = LocalDensity.current val density = LocalDensity.current
val pagerHeight = 48.dp
val contentPadding = 56.dp
LazyColumn( LazyColumn(
modifier = modifier.padding(horizontal = 16.dp), modifier = modifier,
contentPadding = PaddingValues( contentPadding = PaddingValues(bottom = 56.dp + WindowInsets.navigationBars.getBottom(density).dp),
bottom = contentPadding + pagerHeight + WindowInsets.navigationBars.getBottom(density).dp,
),
) { ) {
item { item {
ArticleHeader( ArticleHeader(
title = article.title, title = article.title,
createdAt = article.createdAt, createdAt = article.createdAt.resolveReference(),
score = article.score, score = article.score,
tags = article.tags, tags = article.tags,
modifier = Modifier.padding(top = 16.dp), modifier = Modifier
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
) )
if (article.shortContent.isNotEmpty()) { if (article.shortContent.isNotEmpty()) {
QuickRecap( QuickRecap(
content = article.shortContent, content = article.shortContent,
modifier = Modifier.padding(top = 32.dp), modifier = Modifier
.padding(top = 32.dp)
.padding(horizontal = 16.dp),
) )
} }
@ -125,14 +122,17 @@ private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onL
text = article.content, text = article.content,
style = TangemTheme.typography.body1, style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(top = 16.dp), modifier = Modifier
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
) )
Spacer(modifier = Modifier.height(24.dp)) SpacerH(24.dp)
SecondaryButtonIconStart( SecondaryButtonIconStart(
modifier = Modifier.padding(horizontal = 16.dp),
iconResId = R.drawable.ic_heart_20, iconResId = R.drawable.ic_heart_20,
text = "Like", // TODO [REDACTED_TASK_KEY] export to strings text = stringResourceSafe(R.string.news_like),
size = TangemButtonSize.RoundedAction, size = TangemButtonSize.RoundedAction,
onClick = onLikeClick, onClick = onLikeClick,
) )
@ -140,14 +140,16 @@ private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onL
// TODO [REDACTED_TASK_KEY] add related tokens block // TODO [REDACTED_TASK_KEY] add related tokens block
if (article.sources.isNotEmpty()) { if (article.sources.isNotEmpty()) {
Spacer(modifier = Modifier.height(24.dp)) SpacerH(24.dp)
Row { Row(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text( Text(
text = "Sources", // TODO [REDACTED_TASK_KEY] export to strings text = stringResourceSafe(R.string.news_sources),
style = TangemTheme.typography.h3, style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
) )
Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = "${article.sources.size}", text = "${article.sources.size}",
style = TangemTheme.typography.h3, style = TangemTheme.typography.h3,
@ -159,23 +161,19 @@ private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onL
if (article.sources.isNotEmpty()) { if (article.sources.isNotEmpty()) {
item { item {
val sourcesPagerState = rememberPagerState( LazyRow(
pageCount = { article.sources.size }, modifier = Modifier.padding(vertical = 12.dp),
) state = rememberLazyListState(),
Spacer(modifier = Modifier.height(12.dp)) contentPadding = PaddingValues(horizontal = 16.dp),
HorizontalPager( horizontalArrangement = Arrangement.spacedBy(12.dp),
state = sourcesPagerState, ) {
modifier = Modifier items(
.fillMaxWidth(), items = article.sources,
pageSpacing = 12.dp, key = SourceUM::id,
contentPadding = PaddingValues(horizontal = 0.dp), ) { source ->
) { page -> SourceItem(source = source)
SourceItem( }
source = article.sources[page],
modifier = Modifier.fillMaxWidth(),
)
} }
Spacer(modifier = Modifier.height(12.dp))
} }
} }
} }
@ -206,7 +204,7 @@ private fun QuickRecap(content: String, modifier: Modifier = Modifier) {
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = "Quick recap", // TODO [REDACTED_TASK_KEY] export to strings text = stringResourceSafe(R.string.news_quick_recap),
style = TangemTheme.typography.subtitle2, style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.accent, color = TangemTheme.colors.text.accent,
) )
@ -225,10 +223,13 @@ private fun QuickRecap(content: String, modifier: Modifier = Modifier) {
private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) { private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) {
Column( Column(
modifier = modifier modifier = modifier
.widthIn(max = 216.dp)
.heightIn(min = 132.dp)
.background( .background(
color = TangemTheme.colors.background.primary, color = TangemTheme.colors.background.action,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
) )
.clickable(onClick = source.onClick)
.padding(12.dp), .padding(12.dp),
) { ) {
Row( Row(
@ -239,25 +240,30 @@ private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) {
painter = painterResource(id = R.drawable.ic_explore_16), painter = painterResource(id = R.drawable.ic_explore_16),
contentDescription = null, contentDescription = null,
tint = TangemTheme.colors.icon.informative, tint = TangemTheme.colors.icon.informative,
modifier = Modifier.size(20.dp), modifier = Modifier.size(16.dp),
) )
Spacer(modifier = Modifier.width(4.dp)) SpacerW(4.dp)
Text( Text(
text = source.sourceName, text = source.source.name,
style = TangemTheme.typography.caption1, style = TangemTheme.typography.caption1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors.text.tertiary, color = TangemTheme.colors.text.tertiary,
) )
} }
if (source.title.isNotEmpty()) { if (source.title.isNotEmpty()) {
SpacerH(4.dp)
Text( Text(
text = source.title, text = source.title,
style = TangemTheme.typography.subtitle2, style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(bottom = 12.dp), modifier = Modifier.padding(bottom = 12.dp),
) )
} }
Text( Text(
text = source.publishedAt, text = source.publishedAt.resolveReference(),
style = TangemTheme.typography.caption2, style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary, color = TangemTheme.colors.text.tertiary,
) )
@ -273,10 +279,10 @@ private fun PreviewNewsDetailsContent() {
state = NewsDetailsUM( state = NewsDetailsUM(
articles = MockArticlesFactory.createMockArticles(), articles = MockArticlesFactory.createMockArticles(),
selectedArticleIndex = 0, selectedArticleIndex = 0,
onShareClick = { }, onShareClick = {},
onLikeClick = { }, onLikeClick = {},
onBackClick = {},
), ),
onBackClick = { },
) )
} }
} }

View file

@ -1,22 +1,19 @@
package com.tangem.features.news.details.impl package com.tangem.features.feed.ui.news.details.state
import com.tangem.core.ui.components.label.entity.LabelSize import com.tangem.core.ui.components.label.entity.LabelSize
import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference 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.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
// TODO [REDACTED_TASK_KEY] remove mock data
@Suppress("MaximumLineLength", "LongMethod") @Suppress("MaximumLineLength", "LongMethod")
object MockArticlesFactory { internal object MockArticlesFactory {
fun createMockArticles(): ImmutableList<ArticleUM> = listOf( fun createMockArticles(): ImmutableList<ArticleUM> = listOf(
ArticleUM( ArticleUM(
id = 1, id = 1,
title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds", title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds",
createdAt = "20 Jun, 21:45", createdAt = TextReference.Str("20 Jun, 21:45"),
score = 6.5f, score = 6.5f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG),
@ -27,23 +24,31 @@ object MockArticlesFactory {
SourceUM( SourceUM(
id = 1, id = 1,
title = "Deeper liquidity could drive crypto market beyond \$6T", title = "Deeper liquidity could drive crypto market beyond \$6T",
sourceName = "cointelegraph", source = Source(
publishedAt = "1h ago", id = 11,
name = "Coin-telegraph",
),
publishedAt = TextReference.Str("1h ago"),
url = "https://cointelegraph.com", url = "https://cointelegraph.com",
onClick = {},
), ),
SourceUM( SourceUM(
id = 2, id = 2,
title = "Top gainers and losers in crypto this week", title = "Top gainers and losers in crypto this week",
sourceName = "Investing", source = Source(
publishedAt = "2h ago", id = 10,
name = "Investing",
),
publishedAt = TextReference.Str("2h ago"),
url = "https://investing.com", url = "https://investing.com",
onClick = {},
), ),
).toPersistentList(), ).toPersistentList(),
), ),
ArticleUM( ArticleUM(
id = 2, id = 2,
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
createdAt = "20 Jun, 20:15", createdAt = TextReference.Str("20 Jun, 20:15"),
score = 8.2f, score = 8.2f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("BTC"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("BTC"), size = LabelSize.BIG),
@ -55,16 +60,20 @@ object MockArticlesFactory {
SourceUM( SourceUM(
id = 3, id = 3,
title = "Bitcoin ETFs see massive inflows", title = "Bitcoin ETFs see massive inflows",
sourceName = "Bloomberg", source = Source(
publishedAt = "3h ago", id = 12,
name = "Bloomberg",
),
publishedAt = TextReference.Str("3h ago"),
url = "https://bloomberg.com", url = "https://bloomberg.com",
onClick = {},
), ),
).toPersistentList(), ).toPersistentList(),
), ),
ArticleUM( ArticleUM(
id = 3, id = 3,
title = "Ethereum network upgrade scheduled for Q2 2025", title = "Ethereum network upgrade scheduled for Q2 2025",
createdAt = "20 Jun, 18:30", createdAt = TextReference.Str("20 Jun, 18:30"),
score = 7.8f, score = 7.8f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("ETH"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("ETH"), size = LabelSize.BIG),
@ -76,16 +85,20 @@ object MockArticlesFactory {
SourceUM( SourceUM(
id = 4, id = 4,
title = "Ethereum core devs announce upgrade", title = "Ethereum core devs announce upgrade",
sourceName = "CoinDesk", source = Source(
publishedAt = "5h ago", id = 15,
name = "CoinDesk",
),
publishedAt = TextReference.Str("5h ago"),
url = "https://coindesk.com", url = "https://coindesk.com",
onClick = {},
), ),
).toPersistentList(), ).toPersistentList(),
), ),
ArticleUM( ArticleUM(
id = 4, id = 4,
title = "Solana surpasses Ethereum in daily transaction volume", title = "Solana surpasses Ethereum in daily transaction volume",
createdAt = "20 Jun, 16:00", createdAt = TextReference.Str("20 Jun, 16:00"),
score = 9.1f, score = 9.1f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("SOL"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("SOL"), size = LabelSize.BIG),
@ -97,16 +110,20 @@ object MockArticlesFactory {
SourceUM( SourceUM(
id = 5, id = 5,
title = "Solana transactions hit record", title = "Solana transactions hit record",
sourceName = "The Block", source = Source(
publishedAt = "7h ago", id = 18,
name = "Times",
),
publishedAt = TextReference.Str("7h ago"),
url = "https://theblock.co", url = "https://theblock.co",
onClick = {},
), ),
).toPersistentList(), ).toPersistentList(),
), ),
ArticleUM( ArticleUM(
id = 5, id = 5,
title = "DeFi protocol launches innovative yield farming strategy", title = "DeFi protocol launches innovative yield farming strategy",
createdAt = "20 Jun, 14:20", createdAt = TextReference.Str("20 Jun, 14:20"),
score = 6.9f, score = 6.9f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("DeFi"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("DeFi"), size = LabelSize.BIG),
@ -118,7 +135,7 @@ object MockArticlesFactory {
ArticleUM( ArticleUM(
id = 6, id = 6,
title = "Crypto regulation bill advances in US Senate", title = "Crypto regulation bill advances in US Senate",
createdAt = "20 Jun, 12:45", createdAt = TextReference.Str("20 Jun, 12:45"),
score = 8.7f, score = 8.7f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG),
@ -131,7 +148,7 @@ object MockArticlesFactory {
ArticleUM( ArticleUM(
id = 7, id = 7,
title = "Major bank announces crypto custody services", title = "Major bank announces crypto custody services",
createdAt = "20 Jun, 10:30", createdAt = TextReference.Str("20 Jun, 10:30"),
score = 7.3f, score = 7.3f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("Adoption"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("Adoption"), size = LabelSize.BIG),
@ -143,7 +160,7 @@ object MockArticlesFactory {
ArticleUM( ArticleUM(
id = 8, id = 8,
title = "NFT marketplace reports 300% increase in trading volume", title = "NFT marketplace reports 300% increase in trading volume",
createdAt = "20 Jun, 08:15", createdAt = TextReference.Str("20 Jun, 08:15"),
score = 5.8f, score = 5.8f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("NFT"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("NFT"), size = LabelSize.BIG),
@ -155,7 +172,7 @@ object MockArticlesFactory {
ArticleUM( ArticleUM(
id = 9, id = 9,
title = "Layer 2 solution achieves 100,000 TPS milestone", title = "Layer 2 solution achieves 100,000 TPS milestone",
createdAt = "19 Jun, 22:00", createdAt = TextReference.Str("19 Jun, 22:00"),
score = 8.5f, score = 8.5f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG),
@ -168,7 +185,7 @@ object MockArticlesFactory {
ArticleUM( ArticleUM(
id = 10, id = 10,
title = "Stablecoin market cap reaches new all-time high", title = "Stablecoin market cap reaches new all-time high",
createdAt = "19 Jun, 19:30", createdAt = TextReference.Str("19 Jun, 19:30"),
score = 7.6f, score = 7.6f,
tags = listOf( tags = listOf(
LabelUM(text = TextReference.Str("Stablecoins"), size = LabelSize.BIG), LabelUM(text = TextReference.Str("Stablecoins"), size = LabelSize.BIG),

View file

@ -1,21 +1,22 @@
package com.tangem.features.news.details.impl.ui package com.tangem.features.feed.ui.news.details.state
import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
// TODO [REDACTED_TASK_KEY] make internal internal data class NewsDetailsUM(
data class NewsDetailsUM(
val articles: ImmutableList<ArticleUM>, val articles: ImmutableList<ArticleUM>,
val selectedArticleIndex: Int, val selectedArticleIndex: Int,
val onShareClick: () -> Unit, val onShareClick: () -> Unit,
val onLikeClick: () -> Unit, val onLikeClick: () -> Unit,
val onBackClick: () -> Unit,
val onArticleIndexChanged: (Int) -> Unit = {},
) )
// TODO [REDACTED_TASK_KEY] make internal internal data class ArticleUM(
data class ArticleUM(
val id: Int, val id: Int,
val title: String, val title: String,
val createdAt: String, val createdAt: TextReference,
val score: Float, val score: Float,
val tags: ImmutableList<LabelUM>, val tags: ImmutableList<LabelUM>,
val shortContent: String, val shortContent: String,
@ -23,11 +24,16 @@ data class ArticleUM(
val sources: ImmutableList<SourceUM>, val sources: ImmutableList<SourceUM>,
) )
// TODO [REDACTED_TASK_KEY] make internal internal data class SourceUM(
data class SourceUM(
val id: Int, val id: Int,
val title: String, val title: String,
val sourceName: String, val source: Source,
val publishedAt: String, val publishedAt: TextReference,
val url: String, val url: String,
val onClick: () -> Unit,
)
internal data class Source(
val id: Int,
val name: String,
) )

View file

@ -0,0 +1,40 @@
package com.tangem.features.feed.ui.utils
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.FormattedDate
import com.tangem.core.ui.utils.getFormattedDate
import com.tangem.features.feed.impl.R
import com.tangem.utils.StringsSigns
import org.joda.time.DateTime
internal fun mapFormattedDate(createdAt: String): TextReference {
val formattedDate = getFormattedDate(
createdAt = createdAt,
now = DateTime.now(),
)
return when (formattedDate) {
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
is FormattedDate.HoursAgo -> TextReference.PluralRes(
id = R.plurals.news_published_hours_ago,
count = formattedDate.hours,
formatArgs = wrappedList(formattedDate.hours),
)
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
id = R.plurals.news_published_minutes_ago,
count = formattedDate.minutes,
formatArgs = wrappedList(formattedDate.minutes),
)
is FormattedDate.Today -> TextReference.Combined(
refs = WrappedList(
data = listOf(
TextReference.Res(R.string.common_today),
TextReference.Str(StringsSigns.COMA_SIGN),
TextReference.Str(StringsSigns.WHITE_SPACE),
TextReference.Str(formattedDate.time),
),
),
)
}
}

View file

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

View file

@ -1,23 +0,0 @@
package com.tangem.features.news.details.api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableContentComponent
interface NewsDetailsComponent : ComposableContentComponent {
data class Params(val selectedArticleId: Int = 0)
@Composable
fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
)
interface Factory : ComponentFactory<Params, NewsDetailsComponent>
}

View file

@ -1,43 +0,0 @@
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

@ -1,67 +0,0 @@
package com.tangem.features.news.details.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
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 BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
val uiState by model.uiState.collectAsStateWithLifecycle()
val bsState by bottomSheetState
BackHandler(enabled = bsState == BottomSheetState.EXPANDED) {
navigateBack()
}
NewsDetailsContent(
state = uiState,
onBackClick = ::navigateBack,
modifier = modifier,
isBottomSheetMode = true,
)
}
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsStateWithLifecycle()
NewsDetailsContent(
state = uiState,
onBackClick = model::onBackClick,
modifier = modifier,
isBottomSheetMode = false,
)
}
private fun navigateBack() = router.pop()
@AssistedFactory
interface Factory : NewsDetailsComponent.Factory {
override fun create(
context: AppComponentContext,
params: NewsDetailsComponent.Params,
): DefaultNewsDetailsComponent
}
}

View file

@ -1,41 +0,0 @@
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

@ -1,18 +0,0 @@
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

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

View file

@ -14,9 +14,6 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeActivity import com.tangem.core.ui.screen.ComposeActivity
import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen
@ -38,14 +35,7 @@ import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersSc
import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel 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.ui.TestPushScreen
import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel 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.ArticleUM
import com.tangem.features.news.details.impl.ui.NewsDetailsContent
import com.tangem.features.news.details.impl.ui.NewsDetailsUM
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import javax.inject.Inject import javax.inject.Inject
@ -92,7 +82,6 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.TESTER_ACTIONS, ButtonUM.TESTER_ACTIONS,
ButtonUM.TEST_PUSHES, ButtonUM.TEST_PUSHES,
ButtonUM.ACCOUNTS, ButtonUM.ACCOUNTS,
ButtonUM.NEWS,
), ),
onButtonClick = { buttonUM -> onButtonClick = { buttonUM ->
val route = when (buttonUM) { val route = when (buttonUM) {
@ -103,7 +92,6 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS
ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES
ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS
ButtonUM.NEWS -> TesterScreen.NEWS
} }
innerTesterRouter.open(route) innerTesterRouter.open(route)
@ -174,64 +162,6 @@ internal class TesterActivity : ComposeActivity() {
AccountsScreen(state) 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() },
)
}
composable(route = TesterScreen.NEWS_DETAILS_BOTTOM_SHEET.name) {
NewsDetailsBottomSheetTest(
onDismiss = { innerTesterRouter.back() },
)
}
}
}
@Composable
private fun NewsDetailsBottomSheetTest(onDismiss: () -> Unit) {
data class NewsDetailsBottomSheetContent(
val articles: ImmutableList<ArticleUM>,
) : TangemBottomSheetConfigContent
val content = NewsDetailsBottomSheetContent(
articles = MockArticlesFactory.createMockArticles(),
)
TangemBottomSheet<NewsDetailsBottomSheetContent>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = content,
),
containerColor = TangemTheme.colors.background.tertiary,
) { sheetContent ->
NewsDetailsContent(
state = NewsDetailsUM(
articles = sheetContent.articles,
selectedArticleIndex = 0,
onLikeClick = { },
onShareClick = { },
),
onBackClick = onDismiss,
isBottomSheetMode = true,
)
} }
} }
} }

View file

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

View file

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

View file

@ -1,17 +0,0 @@
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),
NEWS_DETAILS_BOTTOM_SHEET(R.string.news_details_bottom_sheet),
}
}

View file

@ -1,62 +0,0 @@
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

@ -1,46 +0,0 @@
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,
NewsUM.ButtonUM.NEWS_DETAILS_BOTTOM_SHEET,
),
onButtonClick = ::onButtonClick,
)
}
private fun onBackClick() {
router?.back()
}
private fun onButtonClick(button: NewsUM.ButtonUM) {
when (button) {
NewsUM.ButtonUM.NEWS_DETAILS -> router?.open(TesterScreen.NEWS_DETAILS)
NewsUM.ButtonUM.NEWS_DETAILS_BOTTOM_SHEET -> router?.open(TesterScreen.NEWS_DETAILS_BOTTOM_SHEET)
}
}
}

View file

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