Updated on 2026-08-14
This commit is contained in:
parent
636ae3ee50
commit
a71b7ed4ba
43 changed files with 590 additions and 681 deletions
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
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.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -9,6 +12,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -49,6 +53,8 @@ internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = M
|
|||
Text(
|
||||
text = createdAt,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,14 +28,3 @@ data class NewsRelatedTokenDto(
|
|||
@Json(name = "symbol") val symbol: 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,
|
||||
)
|
||||
|
|
@ -18,3 +18,20 @@ data class NewsDetailsResponse(
|
|||
@Json(name = "content") val content: String,
|
||||
@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,
|
||||
)
|
||||
|
|
@ -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.NewsOriginalArticleDto
|
||||
import com.tangem.datasource.api.news.models.response.NewsRelatedTokenDto
|
||||
import com.tangem.domain.models.news.ArticleCategory
|
||||
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
|
||||
import com.tangem.domain.models.news.*
|
||||
|
||||
internal fun NewsDetailsResponse.toDomainDetailedArticle(): DetailedArticle {
|
||||
return DetailedArticle(
|
||||
|
|
@ -54,7 +50,10 @@ internal fun NewsOriginalArticleDto.toDomainOriginalArticle(): OriginalArticle {
|
|||
return OriginalArticle(
|
||||
id = id,
|
||||
title = title,
|
||||
sourceName = sourceName,
|
||||
source = Source(
|
||||
id = source.id,
|
||||
name = source.name,
|
||||
),
|
||||
locale = language,
|
||||
publishedAt = publishedAt,
|
||||
url = url,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable
|
|||
[REDACTED_AUTHOR]
|
||||
* @param id - unique identifier of the article
|
||||
* @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 publishedAt - date of article publishing
|
||||
* @param url - link to source of original article
|
||||
|
|
@ -18,9 +18,15 @@ import kotlinx.serialization.Serializable
|
|||
data class OriginalArticle(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val sourceName: String,
|
||||
val source: Source,
|
||||
val locale: String,
|
||||
val publishedAt: String,
|
||||
val url: String,
|
||||
val imageUrl: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Source(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.news.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.news.DetailedArticle
|
||||
import com.tangem.domain.news.repository.NewsRepository
|
||||
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).
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,7 @@ sealed interface FeedEntryRoute {
|
|||
|
||||
@Serializable
|
||||
data object MarketTokenList : FeedEntryRoute
|
||||
|
||||
@Serializable
|
||||
data class NewsDetail(val articleId: Int, val preselectedArticlesId: List<Int>) : FeedEntryRoute
|
||||
}
|
||||
|
|
@ -15,10 +15,13 @@ import com.tangem.core.decompose.context.childByContext
|
|||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
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.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
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.FeedEntryRoute
|
||||
import com.tangem.features.feed.model.feed.FeedModelClickIntents
|
||||
|
|
@ -55,6 +58,9 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
source = "Market",
|
||||
),
|
||||
onBackClicked = { onChildBack() },
|
||||
onArticleClick = { articleId, preselectedArticlesId ->
|
||||
onArticleClick(articleId, preselectedArticlesId)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -73,8 +79,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onArticleClick(articleId: Int) {
|
||||
innerRouter.push(FeedEntryChildFactory.Child.NewsDetails)
|
||||
override fun onArticleClick(articleId: Int, preselectedArticlesId: List<Int>) {
|
||||
innerRouter.push(
|
||||
FeedEntryChildFactory.Child.NewsDetails(
|
||||
params = DefaultNewsDetailsComponent.Params(
|
||||
articleId = articleId,
|
||||
onBackClicked = { onChildBack() },
|
||||
preselectedArticlesId = preselectedArticlesId,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onOpenAllNews() {
|
||||
|
|
@ -121,6 +135,10 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val background = TangemTheme.colors.background.tertiary
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
val bottomSheetState = remember {
|
||||
derivedStateOf { BottomSheetState.EXPANDED }
|
||||
}
|
||||
|
|
@ -136,6 +154,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
isOpenedInBottomSheet = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
if (stack.value.active.configuration !is FeedEntryChildFactory.Child.Feed) {
|
||||
|
|
@ -157,6 +176,9 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
)
|
||||
},
|
||||
onBackClicked = { router.pop() },
|
||||
onArticleClick = { articleId, preselectedArticlesId ->
|
||||
clickIntents.onArticleClick(articleId, preselectedArticlesId)
|
||||
},
|
||||
),
|
||||
)
|
||||
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
|
||||
|
|
@ -167,6 +189,13 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
shouldAlwaysShowSearchBar = false,
|
||||
),
|
||||
)
|
||||
is FeedEntryRoute.NewsDetail -> FeedEntryChildFactory.Child.NewsDetails(
|
||||
DefaultNewsDetailsComponent.Params(
|
||||
articleId = entryRoute.articleId,
|
||||
onBackClicked = { router.pop() },
|
||||
preselectedArticlesId = entryRoute.preselectedArticlesId,
|
||||
),
|
||||
)
|
||||
null -> FeedEntryChildFactory.Child.Feed
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
|
||||
@Serializable
|
||||
@Immutable
|
||||
data object NewsDetails : Child
|
||||
data class NewsDetails(val params: DefaultNewsDetailsComponent.Params) : Child
|
||||
}
|
||||
|
||||
fun createChild(
|
||||
|
|
@ -67,9 +67,10 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
params = child.params,
|
||||
)
|
||||
}
|
||||
Child.NewsDetails -> {
|
||||
is Child.NewsDetails -> {
|
||||
DefaultNewsDetailsComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = child.params,
|
||||
)
|
||||
}
|
||||
Child.NewsList -> {
|
||||
|
|
|
|||
|
|
@ -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.decompose.ComposableModularBottomSheetContentComponent
|
||||
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.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
|
|
@ -87,13 +86,14 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
MarketsTokenDetailsTopBar(
|
||||
onBackClick = { params.onBackClicked() },
|
||||
isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
||||
shouldShowPriceSubtitle = state.shouldShowPriceSubtitle,
|
||||
tokenName = state.tokenName,
|
||||
tokenPrice = state.priceText,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
backgroundColor = background,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +131,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
val shouldShowPortfolio: Boolean,
|
||||
val analyticsParams: AnalyticsParams?,
|
||||
val onBackClicked: () -> Unit,
|
||||
val onArticleClick: (articleId: Int, preselectedArticlesId: List<Int>) -> Unit,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import com.tangem.domain.onramp.model.OnrampSource
|
|||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
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.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
|
@ -41,6 +43,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val shareManager: ShareManager,
|
||||
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
|
||||
) {
|
||||
|
||||
private val disabledActionsInDemoMode = buildSet {
|
||||
|
|
@ -186,7 +189,20 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData ->
|
||||
currencyData.userWallet.walletId to currencyData.status
|
||||
}
|
||||
if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) {
|
||||
val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull()
|
||||
val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
|
||||
|
||||
when {
|
||||
tokenEnterStatus != null -> router.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
navigationAction = NavigationAction.YieldSupply(
|
||||
isActive = isActiveYield,
|
||||
),
|
||||
),
|
||||
)
|
||||
isActiveYield -> {
|
||||
router.push(
|
||||
AppRoute.YieldSupplyActive(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -194,7 +210,8 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
apy = yieldSupplyApy,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
}
|
||||
else -> {
|
||||
router.push(
|
||||
AppRoute.YieldSupplyPromo(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -204,6 +221,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ internal sealed class QuickActionUM(
|
|||
data class YieldMode(
|
||||
private val apy: String,
|
||||
) : 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)),
|
||||
icon = R.drawable.ic_analytics_up_mini_24,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ internal data class TokenActionsBSContentUM(
|
|||
iconRes = R.drawable.ic_staking_24,
|
||||
),
|
||||
YieldMode(
|
||||
text = resourceReference(R.string.yield_module_start_earning),
|
||||
text = resourceReference(R.string.common_yield_mode),
|
||||
iconRes = R.drawable.ic_analytics_up_mini_24,
|
||||
),
|
||||
;
|
||||
|
|
|
|||
|
|
@ -2,18 +2,63 @@ package com.tangem.features.feed.components.news.details
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.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(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {}
|
||||
private val newsDetailsModel = getOrCreateModel<NewsDetailsModel, Params>(params = params)
|
||||
|
||||
@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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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.market.details.MarketsTokenDetailsModel
|
||||
import com.tangem.features.feed.model.market.list.MarketsListModel
|
||||
import com.tangem.features.feed.model.news.details.NewsDetailsModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -29,4 +30,9 @@ internal interface ModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(MarketsTokenDetailsModel::class)
|
||||
fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(NewsDetailsModel::class)
|
||||
fun provideNewsDetailsModel(model: NewsDetailsModel): Model
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import org.joda.time.DateTime
|
|||
import org.joda.time.DateTimeZone
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.map
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
|
|
@ -311,7 +312,17 @@ internal class FeedComponentModel @Inject constructor(
|
|||
params.feedClickIntents.onMarketOpenClick(sortBy)
|
||||
},
|
||||
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 = {
|
||||
params.feedClickIntents.onOpenAllNews()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@ import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
|||
internal interface FeedModelClickIntents {
|
||||
fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency)
|
||||
fun onMarketOpenClick(sortBy: SortByTypeUM?)
|
||||
fun onArticleClick(articleId: Int)
|
||||
fun onArticleClick(articleId: Int, preselectedArticlesId: List<Int> = emptyList())
|
||||
fun onOpenAllNews()
|
||||
}
|
||||
|
|
@ -241,9 +241,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
onShouldShowPriceSubtitleChange = ::onShouldShowPriceSubtitleChange,
|
||||
relatedNews = MarketsTokenDetailsUM.RelatedNews(
|
||||
articles = persistentListOf(),
|
||||
onArticledClicked = {
|
||||
// TODO in [REDACTED_JIRA]
|
||||
},
|
||||
onArticledClicked = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -313,9 +311,16 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
),
|
||||
).onRight { articles ->
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
val relatedNews = relatedNewsConverter.convert(articles)
|
||||
marketsTokenDetailsUM.copy(
|
||||
relatedNews = marketsTokenDetailsUM.relatedNews.copy(
|
||||
articles = relatedNewsConverter.convert(articles),
|
||||
articles = relatedNews,
|
||||
onArticledClicked = { articledId ->
|
||||
params.onArticleClick(
|
||||
/* articledId */ articledId,
|
||||
/* preselectedIds */ relatedNews.map { it.id },
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.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.ShortArticle
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.features.feed.ui.utils.mapFormattedDate
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
|
||||
|
||||
|
|
@ -51,34 +45,4 @@ class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<Article
|
|||
}
|
||||
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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
flow4 = shouldShowYieldModeMarketPromoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
|
||||
),
|
||||
).conflate(),
|
||||
) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo ->
|
||||
MarketsItemsData(
|
||||
items = uiItems,
|
||||
|
|
@ -138,7 +138,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
flow3 = shouldShowYieldModeMarketPromoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
|
||||
),
|
||||
).conflate(),
|
||||
) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo ->
|
||||
MarketsItemsData(
|
||||
items = uiItems,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -357,7 +357,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = Modifier
|
||||
.height(164.dp)
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
|
|
@ -365,7 +365,9 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
|
||||
item {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier.size(width = 216.dp, height = 164.dp),
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp),
|
||||
onClick = feedListCallbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.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.ShortArticle
|
||||
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.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal class TrendingNewsStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
|
|
@ -111,34 +105,4 @@ internal class TrendingNewsStateFactory(
|
|||
}
|
||||
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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,14 +142,14 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
|
|||
)
|
||||
}
|
||||
|
||||
val marketsNotification = state.marketsNotificationUM
|
||||
AnimatedVisibility(
|
||||
state.isInSearchMode.not() &&
|
||||
state.list !is ListUM.LoadingError && state.isInSearchMode.not() &&
|
||||
state.selectedSortBy != SortByTypeUM.YieldSupply,
|
||||
) {
|
||||
val wrappedNotification = remember(this) { state.marketsNotificationUM }
|
||||
val showMore = stringResourceSafe(R.string.common_show_more)
|
||||
|
||||
when (marketsNotification) {
|
||||
when (wrappedNotification) {
|
||||
is MarketsNotificationUM.YieldSupplyPromo -> {
|
||||
val description = stringResourceSafe(
|
||||
R.string.markets_yield_supply_banner_description,
|
||||
|
|
@ -165,7 +165,7 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
|
|||
}
|
||||
|
||||
YieldSupplyInMarketsPromoNotification(
|
||||
config = marketsNotification.config.copy(
|
||||
config = wrappedNotification.config.copy(
|
||||
subtitle = clickableDescription,
|
||||
),
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
|
|
|
|||
|
|
@ -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 androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -11,57 +15,57 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.text.style.TextOverflow
|
||||
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.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
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.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
|
||||
fun NewsDetailsContent(
|
||||
state: NewsDetailsUM,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isBottomSheetMode: Boolean = false,
|
||||
) {
|
||||
internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = state.selectedArticleIndex,
|
||||
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(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.conditionalCompose(isBottomSheetMode) { systemBarsPadding() },
|
||||
) {
|
||||
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(),
|
||||
.background(background),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (state.articles.isNotEmpty()) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
|
|
@ -73,51 +77,44 @@ fun NewsDetailsContent(
|
|||
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),
|
||||
.align(Alignment.BottomCenter)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
),
|
||||
modifier = modifier,
|
||||
contentPadding = PaddingValues(bottom = 56.dp + WindowInsets.navigationBars.getBottom(density).dp),
|
||||
) {
|
||||
item {
|
||||
ArticleHeader(
|
||||
title = article.title,
|
||||
createdAt = article.createdAt,
|
||||
createdAt = article.createdAt.resolveReference(),
|
||||
score = article.score,
|
||||
tags = article.tags,
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.shortContent.isNotEmpty()) {
|
||||
QuickRecap(
|
||||
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,
|
||||
style = TangemTheme.typography.body1,
|
||||
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(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
iconResId = R.drawable.ic_heart_20,
|
||||
text = "Like", // TODO [REDACTED_TASK_KEY] export to strings
|
||||
text = stringResourceSafe(R.string.news_like),
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
onClick = onLikeClick,
|
||||
)
|
||||
|
|
@ -140,14 +140,16 @@ private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onL
|
|||
// TODO [REDACTED_TASK_KEY] add related tokens block
|
||||
|
||||
if (article.sources.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Row {
|
||||
SpacerH(24.dp)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Sources", // TODO [REDACTED_TASK_KEY] export to strings
|
||||
text = stringResourceSafe(R.string.news_sources),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "${article.sources.size}",
|
||||
style = TangemTheme.typography.h3,
|
||||
|
|
@ -159,23 +161,19 @@ private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onL
|
|||
|
||||
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(),
|
||||
)
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(
|
||||
items = article.sources,
|
||||
key = SourceUM::id,
|
||||
) { source ->
|
||||
SourceItem(source = source)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -206,7 +204,7 @@ private fun QuickRecap(content: String, modifier: Modifier = Modifier) {
|
|||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Quick recap", // TODO [REDACTED_TASK_KEY] export to strings
|
||||
text = stringResourceSafe(R.string.news_quick_recap),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
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) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.widthIn(max = 216.dp)
|
||||
.heightIn(min = 132.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
.clickable(onClick = source.onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(
|
||||
|
|
@ -239,25 +240,30 @@ private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) {
|
|||
painter = painterResource(id = R.drawable.ic_explore_16),
|
||||
contentDescription = null,
|
||||
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 = source.sourceName,
|
||||
text = source.source.name,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
if (source.title.isNotEmpty()) {
|
||||
SpacerH(4.dp)
|
||||
Text(
|
||||
text = source.title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = source.publishedAt,
|
||||
text = source.publishedAt.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
|
@ -273,10 +279,10 @@ private fun PreviewNewsDetailsContent() {
|
|||
state = NewsDetailsUM(
|
||||
articles = MockArticlesFactory.createMockArticles(),
|
||||
selectedArticleIndex = 0,
|
||||
onShareClick = { },
|
||||
onLikeClick = { },
|
||||
onShareClick = {},
|
||||
onLikeClick = {},
|
||||
onBackClick = {},
|
||||
),
|
||||
onBackClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.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 {
|
||||
internal 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",
|
||||
createdAt = TextReference.Str("20 Jun, 21:45"),
|
||||
score = 6.5f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG),
|
||||
|
|
@ -27,23 +24,31 @@ object MockArticlesFactory {
|
|||
SourceUM(
|
||||
id = 1,
|
||||
title = "Deeper liquidity could drive crypto market beyond \$6T",
|
||||
sourceName = "cointelegraph",
|
||||
publishedAt = "1h ago",
|
||||
source = Source(
|
||||
id = 11,
|
||||
name = "Coin-telegraph",
|
||||
),
|
||||
publishedAt = TextReference.Str("1h ago"),
|
||||
url = "https://cointelegraph.com",
|
||||
onClick = {},
|
||||
),
|
||||
SourceUM(
|
||||
id = 2,
|
||||
title = "Top gainers and losers in crypto this week",
|
||||
sourceName = "Investing",
|
||||
publishedAt = "2h ago",
|
||||
source = Source(
|
||||
id = 10,
|
||||
name = "Investing",
|
||||
),
|
||||
publishedAt = TextReference.Str("2h ago"),
|
||||
url = "https://investing.com",
|
||||
onClick = {},
|
||||
),
|
||||
).toPersistentList(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 2,
|
||||
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,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("BTC"), size = LabelSize.BIG),
|
||||
|
|
@ -55,16 +60,20 @@ object MockArticlesFactory {
|
|||
SourceUM(
|
||||
id = 3,
|
||||
title = "Bitcoin ETFs see massive inflows",
|
||||
sourceName = "Bloomberg",
|
||||
publishedAt = "3h ago",
|
||||
source = Source(
|
||||
id = 12,
|
||||
name = "Bloomberg",
|
||||
),
|
||||
publishedAt = TextReference.Str("3h ago"),
|
||||
url = "https://bloomberg.com",
|
||||
onClick = {},
|
||||
),
|
||||
).toPersistentList(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 3,
|
||||
title = "Ethereum network upgrade scheduled for Q2 2025",
|
||||
createdAt = "20 Jun, 18:30",
|
||||
createdAt = TextReference.Str("20 Jun, 18:30"),
|
||||
score = 7.8f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("ETH"), size = LabelSize.BIG),
|
||||
|
|
@ -76,16 +85,20 @@ object MockArticlesFactory {
|
|||
SourceUM(
|
||||
id = 4,
|
||||
title = "Ethereum core devs announce upgrade",
|
||||
sourceName = "CoinDesk",
|
||||
publishedAt = "5h ago",
|
||||
source = Source(
|
||||
id = 15,
|
||||
name = "CoinDesk",
|
||||
),
|
||||
publishedAt = TextReference.Str("5h ago"),
|
||||
url = "https://coindesk.com",
|
||||
onClick = {},
|
||||
),
|
||||
).toPersistentList(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 4,
|
||||
title = "Solana surpasses Ethereum in daily transaction volume",
|
||||
createdAt = "20 Jun, 16:00",
|
||||
createdAt = TextReference.Str("20 Jun, 16:00"),
|
||||
score = 9.1f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("SOL"), size = LabelSize.BIG),
|
||||
|
|
@ -97,16 +110,20 @@ object MockArticlesFactory {
|
|||
SourceUM(
|
||||
id = 5,
|
||||
title = "Solana transactions hit record",
|
||||
sourceName = "The Block",
|
||||
publishedAt = "7h ago",
|
||||
source = Source(
|
||||
id = 18,
|
||||
name = "Times",
|
||||
),
|
||||
publishedAt = TextReference.Str("7h ago"),
|
||||
url = "https://theblock.co",
|
||||
onClick = {},
|
||||
),
|
||||
).toPersistentList(),
|
||||
),
|
||||
ArticleUM(
|
||||
id = 5,
|
||||
title = "DeFi protocol launches innovative yield farming strategy",
|
||||
createdAt = "20 Jun, 14:20",
|
||||
createdAt = TextReference.Str("20 Jun, 14:20"),
|
||||
score = 6.9f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("DeFi"), size = LabelSize.BIG),
|
||||
|
|
@ -118,7 +135,7 @@ object MockArticlesFactory {
|
|||
ArticleUM(
|
||||
id = 6,
|
||||
title = "Crypto regulation bill advances in US Senate",
|
||||
createdAt = "20 Jun, 12:45",
|
||||
createdAt = TextReference.Str("20 Jun, 12:45"),
|
||||
score = 8.7f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG),
|
||||
|
|
@ -131,7 +148,7 @@ object MockArticlesFactory {
|
|||
ArticleUM(
|
||||
id = 7,
|
||||
title = "Major bank announces crypto custody services",
|
||||
createdAt = "20 Jun, 10:30",
|
||||
createdAt = TextReference.Str("20 Jun, 10:30"),
|
||||
score = 7.3f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Adoption"), size = LabelSize.BIG),
|
||||
|
|
@ -143,7 +160,7 @@ object MockArticlesFactory {
|
|||
ArticleUM(
|
||||
id = 8,
|
||||
title = "NFT marketplace reports 300% increase in trading volume",
|
||||
createdAt = "20 Jun, 08:15",
|
||||
createdAt = TextReference.Str("20 Jun, 08:15"),
|
||||
score = 5.8f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("NFT"), size = LabelSize.BIG),
|
||||
|
|
@ -155,7 +172,7 @@ object MockArticlesFactory {
|
|||
ArticleUM(
|
||||
id = 9,
|
||||
title = "Layer 2 solution achieves 100,000 TPS milestone",
|
||||
createdAt = "19 Jun, 22:00",
|
||||
createdAt = TextReference.Str("19 Jun, 22:00"),
|
||||
score = 8.5f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG),
|
||||
|
|
@ -168,7 +185,7 @@ object MockArticlesFactory {
|
|||
ArticleUM(
|
||||
id = 10,
|
||||
title = "Stablecoin market cap reaches new all-time high",
|
||||
createdAt = "19 Jun, 19:30",
|
||||
createdAt = TextReference.Str("19 Jun, 19:30"),
|
||||
score = 7.6f,
|
||||
tags = listOf(
|
||||
LabelUM(text = TextReference.Str("Stablecoins"), size = LabelSize.BIG),
|
||||
|
|
@ -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.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
// TODO [REDACTED_TASK_KEY] make internal
|
||||
data class NewsDetailsUM(
|
||||
internal data class NewsDetailsUM(
|
||||
val articles: ImmutableList<ArticleUM>,
|
||||
val selectedArticleIndex: Int,
|
||||
val onShareClick: () -> Unit,
|
||||
val onLikeClick: () -> Unit,
|
||||
val onBackClick: () -> Unit,
|
||||
val onArticleIndexChanged: (Int) -> Unit = {},
|
||||
)
|
||||
|
||||
// TODO [REDACTED_TASK_KEY] make internal
|
||||
data class ArticleUM(
|
||||
internal data class ArticleUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val createdAt: String,
|
||||
val createdAt: TextReference,
|
||||
val score: Float,
|
||||
val tags: ImmutableList<LabelUM>,
|
||||
val shortContent: String,
|
||||
|
|
@ -23,11 +24,16 @@ data class ArticleUM(
|
|||
val sources: ImmutableList<SourceUM>,
|
||||
)
|
||||
|
||||
// TODO [REDACTED_TASK_KEY] make internal
|
||||
data class SourceUM(
|
||||
internal data class SourceUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val sourceName: String,
|
||||
val publishedAt: String,
|
||||
val source: Source,
|
||||
val publishedAt: TextReference,
|
||||
val url: String,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
internal data class Source(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -59,10 +59,6 @@ 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)
|
||||
implementation(projects.features.news.newsList.api)
|
||||
implementation(projects.features.news.newsList.impl)
|
||||
|
||||
/* SDK */
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
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.screen.ComposeActivity
|
||||
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.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.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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -92,7 +82,6 @@ internal class TesterActivity : ComposeActivity() {
|
|||
ButtonUM.TESTER_ACTIONS,
|
||||
ButtonUM.TEST_PUSHES,
|
||||
ButtonUM.ACCOUNTS,
|
||||
ButtonUM.NEWS,
|
||||
),
|
||||
onButtonClick = { buttonUM ->
|
||||
val route = when (buttonUM) {
|
||||
|
|
@ -103,7 +92,6 @@ 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)
|
||||
|
|
@ -174,64 +162,6 @@ 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() },
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,5 @@ data class TesterMenuUM(
|
|||
TESTER_ACTIONS(R.string.tester_actions),
|
||||
TEST_PUSHES(R.string.test_push),
|
||||
ACCOUNTS(R.string.accounts),
|
||||
NEWS(R.string.news),
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,4 @@ internal enum class TesterScreen {
|
|||
BLOCKCHAIN_PROVIDERS,
|
||||
TEST_PUSHES,
|
||||
ACCOUNTS,
|
||||
NEWS,
|
||||
NEWS_DETAILS,
|
||||
NEWS_DETAILS_BOTTOM_SHEET,
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -302,12 +302,6 @@ include(":features:yield-supply:impl")
|
|||
|
||||
include(":features:feed:api")
|
||||
include(":features:feed:impl")
|
||||
|
||||
include(":features:news:news-details:api")
|
||||
include(":features:news:news-details:impl")
|
||||
|
||||
include(":features:news:news-list:api")
|
||||
include(":features:news:news-list:impl")
|
||||
// endregion Feature modules
|
||||
|
||||
// region Domain modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue