Updated on 2026-08-14
This commit is contained in:
parent
9b134d7058
commit
bc76eaa64a
46 changed files with 764 additions and 154 deletions
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.feed.entry
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.features.feed.entry.BottomSheetState
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface FeedEntryComponent {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.news)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
|
|
@ -86,6 +87,7 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/* Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.features.feed.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
|
|
@ -13,11 +16,13 @@ import com.arkivanov.decompose.value.Value
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
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.ComposableModularContentComponent
|
||||
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.entry.BottomSheetState
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import com.tangem.features.feed.ui.EntryBottomSheetContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -35,7 +40,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
popCallback = { onChildBack() },
|
||||
)
|
||||
|
||||
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, Any>> = childStack(
|
||||
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>> = childStack(
|
||||
key = "main",
|
||||
source = stackNavigation,
|
||||
serializer = FeedEntryChildFactory.Child.serializer(),
|
||||
|
|
@ -59,7 +64,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
bottomSheetState // TODO will be continued in next tasks.
|
||||
val stackState by stack.subscribeAsState()
|
||||
|
||||
BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) {
|
||||
onChildBack()
|
||||
}
|
||||
|
||||
EntryBottomSheetContent(
|
||||
stackState = stackState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
)
|
||||
}
|
||||
|
||||
private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.feed.components
|
|||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
|
|
@ -11,8 +12,9 @@ import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListCo
|
|||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
|
||||
import kotlinx.serialization.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class FeedEntryChildFactory {
|
||||
internal class FeedEntryChildFactory @Inject constructor() {
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
|
|
@ -43,7 +45,7 @@ internal class FeedEntryChildFactory {
|
|||
child: Child,
|
||||
appComponentContext: AppComponentContext,
|
||||
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
|
||||
): Any {
|
||||
): ComposableModularContentComponent {
|
||||
return when (child) {
|
||||
is Child.TokenDetails -> {
|
||||
DefaultMarketsTokenDetailsComponent(
|
||||
|
|
|
|||
|
|
@ -1,23 +1,37 @@
|
|||
package com.tangem.features.feed.components.feed
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
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.decompose.ComposableModularContentComponent
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import com.tangem.features.feed.ui.feed.FeedListContent
|
||||
import com.tangem.features.feed.ui.feed.FeedListHeader
|
||||
|
||||
internal class DefaultFeedComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val feedComponentModel = getOrCreateModel<FeedComponentModel>()
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
val state by feedComponentModel.state.collectAsStateWithLifecycle()
|
||||
FeedListHeader(state.searchBar)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by feedComponentModel.state.collectAsStateWithLifecycle()
|
||||
FeedListContent(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
}
|
||||
override fun Footer() = Unit
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.feed.di
|
||||
|
||||
import com.tangem.features.feed.components.DefaultFeedEntryComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
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 ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindFeedEntryComponent(factory: DefaultFeedEntryComponent.Factory): FeedEntryComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeedComponentModel::class)
|
||||
fun bindsFeedComponentModel(model: FeedComponentModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.features.feed.model.feed
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
|
||||
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toPersistentHashMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class FeedComponentModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
|
||||
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val _state = MutableStateFlow(initialState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SearchBarStateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
onStateUpdate = { newState -> _state.update { newState } },
|
||||
)
|
||||
}
|
||||
|
||||
private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
TrendingNewsStateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
onStateUpdate = { newState -> _state.update { newState } },
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase()
|
||||
subscribeOnTrendingNews()
|
||||
}
|
||||
_state.update { feedListUM ->
|
||||
feedListUM.copy(
|
||||
searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscribeOnTrendingNews() {
|
||||
manageTrendingNewsUseCase().collect { articles ->
|
||||
trendingNewsStateFactory.updateTrendingNewsState(articles)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialState(): FeedListUM {
|
||||
return FeedListUM(
|
||||
currentDate = getCurrentDate(),
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.markets_search_header_title),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
feedListCallbacks = FeedListCallbacks(
|
||||
onSearchClick = {},
|
||||
onMarketOpenClick = {},
|
||||
onArticleClick = {},
|
||||
onOpenAllNews = {},
|
||||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
),
|
||||
news = NewsUM.Loading,
|
||||
trendingArticle = null,
|
||||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach {
|
||||
put(it, MarketChartUM.Loading)
|
||||
} // TODO in [REDACTED_TASK_KEY] add correct sorting
|
||||
}.toPersistentHashMap(),
|
||||
currentSortByType = SortByTypeUM.TopGainers,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCurrentDate(): String {
|
||||
val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault())
|
||||
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.features.feed.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.features.feed.components.FeedEntryChildFactory
|
||||
|
||||
@Composable
|
||||
internal fun EntryBottomSheetContent(
|
||||
stackState: ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
topBar = {
|
||||
AnimatedContent(
|
||||
targetState = stackState.active.instance,
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
) { currentState ->
|
||||
currentState.Title()
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
AnimatedContent(
|
||||
stackState.active.instance,
|
||||
) { currentState ->
|
||||
currentState.Content(modifier = Modifier.padding(contentPadding))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -22,25 +22,25 @@ import androidx.compose.ui.draw.drawBehind
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -48,51 +48,44 @@ 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.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartConfig
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItem
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
|
||||
val density = LocalDensity.current
|
||||
internal fun FeedListHeader(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
SearchBar(
|
||||
modifier = modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp),
|
||||
state = searchBarUM,
|
||||
colors = TangemSearchBarDefaults.defaultTextFieldColors.copy(
|
||||
focusedContainerColor = TangemTheme.colors.field.focused,
|
||||
unfocusedContainerColor = TangemTheme.colors.field.focused,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.drawBehind { drawRect(background) },
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp,
|
||||
)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(bottom = 4.dp),
|
||||
state = state.searchBar,
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 20.dp),
|
||||
text = stringResourceSafe(R.string.feed_market_and_news),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -100,7 +93,7 @@ internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modif
|
|||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = 20.dp),
|
||||
text = state.currentDate,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
|
|
@ -156,6 +149,17 @@ private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks:
|
|||
@Composable
|
||||
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -175,17 +179,6 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb
|
|||
}
|
||||
}
|
||||
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
AnimatedContent(
|
||||
|
|
@ -207,18 +200,38 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb
|
|||
|
||||
@Suppress("CanBeNonNullable")
|
||||
@Composable
|
||||
private fun NewsBlock(
|
||||
private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
AnimatedContent(news) { newsUM ->
|
||||
when (newsUM) {
|
||||
is NewsUM.Content -> {
|
||||
if (newsUM.content.isNotEmpty()) {
|
||||
NewsContentBlock(
|
||||
feedListCallbacks = feedListCallbacks,
|
||||
news = newsUM,
|
||||
trendingArticle = trendingArticle,
|
||||
)
|
||||
}
|
||||
}
|
||||
NewsUM.Loading -> {
|
||||
NewsLoadingBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsContentBlock(
|
||||
feedListCallbacks: FeedListCallbacks,
|
||||
news: ImmutableList<ArticleConfigUM>,
|
||||
news: NewsUM.Content,
|
||||
trendingArticle: ArticleConfigUM?,
|
||||
) {
|
||||
if (news.isNotEmpty()) {
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
|
|
@ -244,13 +257,12 @@ private fun NewsBlock(
|
|||
append(stringResourceSafe(R.string.feed_tangem_ai))
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = feedListCallbacks.onOpenAllNews,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
trendingArticle?.let { article ->
|
||||
|
|
@ -260,8 +272,8 @@ private fun NewsBlock(
|
|||
.padding(horizontal = 16.dp),
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
}
|
||||
|
||||
|
|
@ -272,13 +284,16 @@ private fun NewsBlock(
|
|||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(
|
||||
items = news,
|
||||
items = news.content,
|
||||
key = ArticleConfigUM::id,
|
||||
) { article ->
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = Modifier.size(164.dp),
|
||||
modifier = Modifier
|
||||
.height(164.dp)
|
||||
.widthIn(max = 216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -292,6 +307,7 @@ private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) {
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
title()
|
||||
|
||||
|
|
@ -310,7 +326,10 @@ private fun Charts(
|
|||
onItemClick: (MarketsListItemUM) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BlockCard(modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -380,9 +399,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED)
|
|||
@Composable
|
||||
private fun FeedListPreview() {
|
||||
TangemThemePreview {
|
||||
FeedList(
|
||||
state = createFeedPreviewState(),
|
||||
onHeaderSizeChange = {},
|
||||
)
|
||||
FeedListContent(state = createFeedPreviewState())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.tangem.features.feed.ui.feed
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.DefaultLoadingArticle
|
||||
import com.tangem.common.ui.news.TrendingLoadingArticle
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
|
||||
|
||||
@Composable
|
||||
internal fun MarketLoadingBlock() {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.size(width = 104.dp, height = 18.dp),
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MarketPulseLoadingBlock() {
|
||||
RectangleShimmer()
|
||||
SpacerH(8.dp)
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp))
|
||||
}
|
||||
}
|
||||
SpacerH(12.dp)
|
||||
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NewsLoadingBlock() {
|
||||
Column {
|
||||
RectangleShimmer()
|
||||
SpacerH(12.dp)
|
||||
TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
SpacerH(12.dp)
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
DefaultLoadingArticle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChartsLoading(modifier: Modifier = Modifier) {
|
||||
BlockCard(modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun FeedListLoadingPreview() {
|
||||
TangemThemePreview {
|
||||
Column {
|
||||
NewsLoadingBlock()
|
||||
SpacerH(10.dp)
|
||||
MarketLoadingBlock()
|
||||
SpacerH(10.dp)
|
||||
MarketPulseLoadingBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
package com.tangem.features.feed.ui.feed.preview
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
|
|
@ -39,7 +38,7 @@ internal object FeedListPreviewDataProvider {
|
|||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
),
|
||||
news = articles.filter { it.isTrending.not() }.toImmutableList(),
|
||||
news = NewsUM.Content(articles.filter { it.isTrending.not() }.toImmutableList()),
|
||||
trendingArticle = articles.first { it.isTrending },
|
||||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = createMarketCharts(marketItems, includeErrorState = false),
|
||||
|
|
@ -165,7 +164,7 @@ internal object FeedListPreviewDataProvider {
|
|||
LabelUM(
|
||||
text = TextReference.Str("BTC"),
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
icon = R.drawable.img_btc_22,
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png",
|
||||
),
|
||||
),
|
||||
LabelUM(TextReference.Str("Regulation")),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ internal data class FeedListUM(
|
|||
val currentDate: String,
|
||||
val searchBar: SearchBarUM,
|
||||
val feedListCallbacks: FeedListCallbacks,
|
||||
val news: ImmutableList<ArticleConfigUM>,
|
||||
val news: NewsUM,
|
||||
val trendingArticle: ArticleConfigUM?,
|
||||
val marketChartConfig: MarketChartConfig,
|
||||
)
|
||||
|
|
@ -28,6 +28,12 @@ internal data class FeedListCallbacks(
|
|||
val onSortTypeClick: (SortByTypeUM) -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface NewsUM {
|
||||
data object Loading : NewsUM
|
||||
data class Content(val content: ImmutableList<ArticleConfigUM>) : NewsUM
|
||||
}
|
||||
|
||||
internal data class MarketChartConfig(
|
||||
val marketCharts: ImmutableMap<SortByTypeUM, MarketChartUM>,
|
||||
val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers,
|
||||
|
|
@ -49,7 +55,7 @@ internal sealed interface MarketChartUM {
|
|||
data class LoadingError(val onRetryClicked: () -> Unit) : MarketChartUM
|
||||
}
|
||||
|
||||
data class SortChartConfigUM(
|
||||
internal data class SortChartConfigUM(
|
||||
val sortByType: SortByTypeUM,
|
||||
val isSelected: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
internal class SearchBarStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
private val onStateUpdate: (FeedListUM) -> Unit,
|
||||
) {
|
||||
|
||||
val searchQuery: String
|
||||
get() = currentStateProvider().searchBar.query
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
val currentState = currentStateProvider()
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
searchBar = currentState.searchBar.copy(
|
||||
query = query,
|
||||
isActive = query.isNotEmpty(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
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.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
|
||||
internal class TrendingNewsStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
private val onStateUpdate: (FeedListUM) -> Unit,
|
||||
) {
|
||||
|
||||
fun updateTrendingNewsState(news: List<ShortArticle>) {
|
||||
val trendingArticleIndex = news.indexOfFirst { it.isTrending }
|
||||
val trendingArticle = if (trendingArticleIndex != -1) news[trendingArticleIndex] else null
|
||||
val commonArticles = if (trendingArticleIndex != -1) {
|
||||
news.toMutableList().apply { removeAt(trendingArticleIndex) }
|
||||
} else {
|
||||
news
|
||||
}
|
||||
val currentState = currentStateProvider()
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
trendingArticle = trendingArticle?.let { article ->
|
||||
ArticleConfigUM(
|
||||
id = article.id,
|
||||
title = article.title,
|
||||
score = article.score,
|
||||
isTrending = true,
|
||||
tags = article.categories.map { category ->
|
||||
LabelUM(text = TextReference.Str(category.name))
|
||||
}.plus(
|
||||
article.relatedTokens.map { token ->
|
||||
LabelUM(
|
||||
text = TextReference.Str(token.symbol),
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
iconUrl = getTokenIconUrlFromDefaultHost(
|
||||
tokenId = CryptoCurrency.RawID(token.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
).toPersistentSet(),
|
||||
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
|
||||
isViewed = article.viewed,
|
||||
)
|
||||
},
|
||||
news = NewsUM.Content(
|
||||
commonArticles.map { article ->
|
||||
ArticleConfigUM(
|
||||
id = article.id,
|
||||
title = article.title,
|
||||
score = article.score,
|
||||
isTrending = false,
|
||||
tags = article.categories.map { category ->
|
||||
LabelUM(text = TextReference.Str(category.name))
|
||||
}.plus(
|
||||
article.relatedTokens.map { token ->
|
||||
LabelUM(
|
||||
text = TextReference.Str(token.symbol),
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
iconUrl = getTokenIconUrlFromDefaultHost(
|
||||
tokenId = CryptoCurrency.RawID(token.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
).toPersistentSet(),
|
||||
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
|
||||
isViewed = article.viewed,
|
||||
)
|
||||
}.toPersistentList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,10 @@ 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
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.markets.entry
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface MarketsEntryComponent {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenListComponent : ComposableContentComponent {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
|
|
@ -21,7 +22,6 @@ import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalytics
|
|||
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
|
||||
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
|
||||
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ import com.arkivanov.decompose.value.Value
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
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.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.entry.MarketsEntryComponent
|
||||
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
|
||||
import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ import com.arkivanov.decompose.extensions.compose.stack.Children
|
|||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory
|
||||
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.common.routing.AppRoute.MarketsTokenDetails.AnalyticsParams
|
||||
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.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
||||
import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.MarketsList
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import arrow.core.getOrElse
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
|
|
@ -17,7 +18,6 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
|||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountryError
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
|
||||
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
|
||||
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import com.tangem.core.ui.components.Keyboard
|
|||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
|
|
@ -46,7 +47,6 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ dependencies {
|
|||
implementation(projects.features.tokenRecieve.api)
|
||||
implementation(projects.features.yieldSupply.api)
|
||||
implementation(projects.features.tangempay.details.api)
|
||||
implementation(projects.features.feed.api)
|
||||
|
||||
/** Common modules */
|
||||
implementation(projects.common)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package com.tangem.feature.wallet.child.wallet
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
|
|
@ -13,6 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableDialogComponent
|
||||
|
|
@ -23,6 +25,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogCon
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
|
||||
import com.tangem.feature.walletsettings.component.RenameWalletComponent
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||
import com.tangem.features.markets.entry.MarketsEntryComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
|
|
@ -37,16 +41,25 @@ import kotlinx.coroutines.launch
|
|||
internal class WalletComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted navigate: (WalletRoute) -> Unit,
|
||||
marketsEntryComponentFactory: MarketsEntryComponent.Factory,
|
||||
feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
|
||||
private val marketsEntryComponentFactory: MarketsEntryComponent.Factory,
|
||||
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
|
||||
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: WalletModel = getOrCreateModel()
|
||||
|
||||
private val feedEntryComponent by lazy {
|
||||
feedEntryComponentFactory.create(child("feedEntryComponent"))
|
||||
}
|
||||
private val marketsEntryComponent by lazy {
|
||||
marketsEntryComponentFactory.create(child("marketsEntryComponent"))
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(model.screenLifecycleProvider)
|
||||
doOnResume { model.onResume() }
|
||||
|
|
@ -114,15 +127,23 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
private val marketsEntryComponent = marketsEntryComponentFactory.create(child("marketsEntryComponent"))
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) }
|
||||
var headerSize by remember { mutableStateOf(0.dp) }
|
||||
val dialog by dialog.subscribeAsState()
|
||||
|
||||
WalletScreen(
|
||||
state = model.uiState.collectAsStateWithLifecycle().value,
|
||||
marketsEntryComponent = marketsEntryComponent,
|
||||
bottomSheetContent = {
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
)
|
||||
|
||||
when (val dialog = dialog.child?.instance) {
|
||||
|
|
@ -132,6 +153,27 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (feedFeatureToggle.isFeedEnabled) {
|
||||
feedEntryComponent.BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
marketsEntryComponent.BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(appComponentContext: AppComponentContext, navigate: (WalletRoute) -> Unit): WalletComponent
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
|
|
@ -99,6 +100,7 @@ internal class WalletModel @Inject constructor(
|
|||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||
val innerWalletRouter: InnerWalletRouter,
|
||||
) : Model() {
|
||||
|
|
@ -121,6 +123,7 @@ internal class WalletModel @Inject constructor(
|
|||
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy())
|
||||
}
|
||||
|
||||
updateMarketToggle()
|
||||
suggestToOpenMarkets()
|
||||
|
||||
maybeMigrateNames()
|
||||
|
|
@ -143,6 +146,12 @@ internal class WalletModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateMarketToggle() {
|
||||
stateHolder.update {
|
||||
it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateYieldSupplyApy() {
|
||||
if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ internal object WalletScreenPreviewData {
|
|||
isHidingMode = false,
|
||||
showMarketsOnboarding = false,
|
||||
onDismissMarketsOnboarding = {},
|
||||
isNewMarketEnabled = false,
|
||||
)
|
||||
|
||||
internal val accountScreenState =
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ internal class WalletStateController @Inject constructor() {
|
|||
isHidingMode = false,
|
||||
showMarketsOnboarding = false,
|
||||
onDismissMarketsOnboarding = {},
|
||||
isNewMarketEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,5 +13,6 @@ internal data class WalletScreenState(
|
|||
val event: StateEvent<WalletEvent>,
|
||||
val isHidingMode: Boolean,
|
||||
val showMarketsOnboarding: Boolean,
|
||||
val isNewMarketEnabled: Boolean,
|
||||
val onDismissMarketsOnboarding: () -> Unit,
|
||||
)
|
||||
|
|
@ -51,6 +51,7 @@ import com.tangem.common.ui.expressStatus.expressTransactionsItems
|
|||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
|
||||
import com.tangem.core.ui.components.rememberIsKeyboardVisible
|
||||
import com.tangem.core.ui.components.sheetscaffold.*
|
||||
|
|
@ -87,15 +88,18 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPa
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.entry.MarketsEntryComponent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent) {
|
||||
internal fun WalletScreen(
|
||||
state: WalletScreenState,
|
||||
bottomSheetContent: @Composable (() -> Unit),
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
) {
|
||||
// It means that screen is still initializing
|
||||
if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return
|
||||
|
||||
|
|
@ -116,8 +120,10 @@ internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: Marke
|
|||
snackbarHostState = snackbarHostState,
|
||||
isAutoScroll = isAutoScroll,
|
||||
onAutoScrollReset = { isAutoScroll.value = false },
|
||||
marketsEntryComponent = marketsEntryComponent,
|
||||
bottomSheetContent = bottomSheetContent,
|
||||
alertConfig = alertConfig,
|
||||
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
)
|
||||
|
||||
WalletEventEffect(
|
||||
|
|
@ -136,9 +142,11 @@ private fun WalletContent(
|
|||
walletsListState: LazyListState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
isAutoScroll: State<Boolean>,
|
||||
marketsEntryComponent: MarketsEntryComponent,
|
||||
alertConfig: WalletAlertState?,
|
||||
onAutoScrollReset: () -> Unit,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
bottomSheetContent: @Composable (() -> Unit),
|
||||
) {
|
||||
/*
|
||||
* Don't pass key to remember, because it will brake scroll animation.
|
||||
|
|
@ -286,25 +294,15 @@ private fun WalletContent(
|
|||
)
|
||||
}
|
||||
|
||||
val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) }
|
||||
|
||||
var headerSize by remember { mutableStateOf(0.dp) }
|
||||
|
||||
BaseScaffoldWithMarkets(
|
||||
state = state,
|
||||
listState = listState,
|
||||
selectedWallet = selectedWallet,
|
||||
snackbarHostState = snackbarHostState,
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
|
||||
alertConfig = alertConfig,
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
bottomSheetContent = {
|
||||
marketsEntryComponent.BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = Modifier,
|
||||
)
|
||||
},
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
bottomSheetContent = bottomSheetContent,
|
||||
content = scaffoldContent,
|
||||
)
|
||||
}
|
||||
|
|
@ -394,7 +392,11 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
val maxHeight = LocalWindowSize.current.height
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val backgroundPrimary = TangemTheme.colors.background.primary
|
||||
val background = if (state.isNewMarketEnabled) {
|
||||
TangemTheme.colors.background.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.background.primary
|
||||
}
|
||||
|
||||
val showMarketsHint by remember {
|
||||
derivedStateOf {
|
||||
|
|
@ -407,7 +409,7 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) },
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
val backgroundColor = LocalMainBottomSheetColor.current
|
||||
var isSearchFieldFocused by remember { mutableStateOf(false) }
|
||||
|
|
@ -798,16 +800,11 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::
|
|||
TangemThemePreview {
|
||||
WalletScreen(
|
||||
state = data,
|
||||
marketsEntryComponent = object : MarketsEntryComponent {
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text("Markets Content")
|
||||
}
|
||||
bottomSheetContent = {
|
||||
Text("Markets Content")
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { 10.dp },
|
||||
onBottomSheetStateChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue