diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt
index 1635ee15f2..4e2e9ebb42 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt
@@ -143,7 +143,7 @@ fun TangemSearchField(
innerTextField = innerTextField,
focusManager = focusManager,
interactionSource = interactionSource,
- color = TangemTheme.colors2.field.backgroundDefault,
+ color = TangemTheme.colors2.button.backgroundSecondary,
keyboardController = keyboardController,
)
},
@@ -288,10 +288,9 @@ private fun CancelButton(
if (state.query.isNotEmpty()) {
state.onQueryChange("")
}
- focusManager.clearFocus()
keyboardController?.hide()
- state.onActiveChange(false)
state.onClearClick()
+ focusManager.clearFocus()
},
)
}
diff --git a/core/ui/src/main/res/drawable/ic_return_24.xml b/core/ui/src/main/res/drawable/ic_return_24.xml
new file mode 100644
index 0000000000..f63099d1f3
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_return_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts
index 2e076661b9..41a5739506 100644
--- a/features/feed/impl/build.gradle.kts
+++ b/features/feed/impl/build.gradle.kts
@@ -57,6 +57,7 @@ dependencies {
implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.yieldSupply)
implementation(projects.domain.earn)
+ implementation(projects.domain.search)
/* Compose */
implementation(deps.compose.coil)
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
index 9538938a8c..2576b72a84 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
@@ -22,7 +22,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.news.model.NewsListConfig
-import com.tangem.features.feed.components.earn.DefaultEarnComponent
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
@@ -78,7 +77,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
screenSource = AnalyticsParam.ScreensSources.Token,
)
},
- onMarketOpenClick = { onMarketOpenClick(null) },
),
),
)
@@ -135,14 +133,11 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
}
override fun onOpenEarnPage() {
- innerRouter.push(
- FeedEntryChildFactory.Child.Earn(
- params = DefaultEarnComponent.Params(
- onBackClick = { onChildBack() },
- onMarketOpenClick = { onMarketOpenClick(null) },
- ),
- ),
- )
+ innerRouter.push(FeedEntryChildFactory.Child.Earn)
+ }
+
+ override fun openSearch() {
+ innerRouter.push(FeedEntryChildFactory.Child.Search)
}
}
@@ -244,7 +239,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
paginationConfig = null,
)
},
- onMarketOpenClick = { clickIntents.onMarketOpenClick(null) },
),
)
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
index 812d8b8cef..b4dd71046e 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
@@ -7,15 +7,18 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.features.feed.components.earn.DefaultEarnComponent
-import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
-import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent
+import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
+import com.tangem.features.feed.components.news.list.DefaultNewsListComponent.Params
+import com.tangem.features.feed.components.search.DefaultSearchComponent
+import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
+import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@@ -53,7 +56,11 @@ internal class FeedEntryChildFactory @Inject constructor(
@Serializable
@Immutable
- data class Earn(val params: DefaultEarnComponent.Params) : Child
+ data object Earn : Child
+
+ @Serializable
+ @Immutable
+ data object Search : Child
}
fun createChild(
@@ -86,7 +93,7 @@ internal class FeedEntryChildFactory @Inject constructor(
Child.NewsList -> {
DefaultNewsListComponent(
appComponentContext = appComponentContext,
- params = DefaultNewsListComponent.Params(
+ params = Params(
onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig ->
feedEntryClickIntents.onArticleClick(
articleId = currentArticle,
@@ -102,7 +109,7 @@ internal class FeedEntryChildFactory @Inject constructor(
Child.Feed -> {
DefaultFeedComponent(
appComponentContext = appComponentContext,
- params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents),
+ params = FeedParams(feedClickIntents = feedEntryClickIntents),
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
promoBannersBlockComponentFactory = promoBannersBlockComponentFactory,
newPromoBannersFeatureToggles = newPromoBannersFeatureToggles,
@@ -111,10 +118,19 @@ internal class FeedEntryChildFactory @Inject constructor(
is Child.Earn -> {
DefaultEarnComponent(
appComponentContext = appComponentContext,
- params = child.params,
+ params = DefaultEarnComponent.Params(
+ onBackClick = onBackClicked,
+ onSearchClicked = feedEntryClickIntents::openSearch,
+ ),
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
)
}
+ Child.Search -> DefaultSearchComponent(
+ appComponentContext = appComponentContext,
+ params = DefaultSearchComponent.Params(
+ onBackClick = onBackClicked,
+ ),
+ )
}
}
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt
index e5915d7103..b37c5b7dc7 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt
@@ -35,7 +35,6 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor
import com.tangem.features.feed.model.earn.EarnModel
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.earn.EarnContent
-import kotlinx.serialization.Serializable
internal class DefaultEarnComponent(
appComponentContext: AppComponentContext,
@@ -129,9 +128,8 @@ internal class DefaultEarnComponent(
)
}
- @Serializable
data class Params(
val onBackClick: () -> Unit,
- val onMarketOpenClick: () -> Unit,
+ val onSearchClicked: () -> Unit,
)
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt
index 93292cc527..dcf08d159a 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt
@@ -10,7 +10,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.LifecycleStartEffect
@@ -23,6 +22,8 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
+import com.tangem.core.ui.ds.topbar.TangemTopBar
+import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
@@ -34,7 +35,6 @@ import com.tangem.features.feed.components.market.details.portfolio.api.MarketsP
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
-import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar
import kotlinx.coroutines.flow.collectLatest
@@ -100,10 +100,7 @@ internal class DefaultMarketsTokenDetailsComponent(
val state by model.state.collectAsStateWithLifecycle()
val background = LocalMainBottomSheetColor.current.value
if (LocalRedesignEnabled.current) {
- FeedSearchBar(
- isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
- feedListSearchBar = state.feedListSearchBar,
- modifier = Modifier.drawBehind { drawRect(background) },
+ TangemTopBar(
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@@ -140,6 +137,7 @@ internal class DefaultMarketsTokenDetailsComponent(
.padding(TangemTheme.dimens2.x2_5),
)
},
+ type = TangemTopBarType.BottomSheet,
)
} else {
MarketsTokenDetailsTopBar(
@@ -188,7 +186,6 @@ internal class DefaultMarketsTokenDetailsComponent(
val analyticsParams: AnalyticsParams?,
val onBackClicked: () -> Unit,
val onArticleClick: (articleId: Int, preselectedArticlesId: List) -> Unit,
- val onMarketOpenClick: () -> Unit,
)
@Serializable
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt
new file mode 100644
index 0000000000..b9cf5d8787
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt
@@ -0,0 +1,73 @@
+package com.tangem.features.feed.components.search
+
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+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.core.ui.decompose.ComposableModularBottomSheetContentComponent
+import com.tangem.core.ui.ds.field.search.TangemFieldShape
+import com.tangem.core.ui.ds.field.search.TangemSearchField
+import com.tangem.core.ui.ds.topbar.TangemTopBar
+import com.tangem.core.ui.ds.topbar.TangemTopBarType
+import com.tangem.features.feed.model.search.SearchModel
+import com.tangem.features.feed.ui.search.SearchContent
+import com.tangem.features.feed.ui.search.state.SearchCallbacks
+
+internal class DefaultSearchComponent(
+ appComponentContext: AppComponentContext,
+ private val params: Params,
+) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
+
+ private val model = getOrCreateModel(params = params)
+
+ @Composable
+ override fun Title(bottomSheetState: State) {
+ val state by model.state.collectAsStateWithLifecycle()
+ val focusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(bottomSheetState.value) {
+ if (bottomSheetState.value == BottomSheetState.EXPANDED) {
+ focusRequester.requestFocus()
+ }
+ }
+
+ TangemTopBar(
+ type = TangemTopBarType.BottomSheet,
+ reserveSlotSpace = false,
+ content = {
+ TangemSearchField(
+ state = state.searchBar,
+ shape = TangemFieldShape.Circle,
+ focusRequester = focusRequester,
+ modifier = Modifier.weight(1f),
+ enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
+ )
+ },
+ )
+ }
+
+ @Composable
+ override fun Content(bottomSheetState: State, modifier: Modifier) {
+ val state by model.state.collectAsStateWithLifecycle()
+ val searchCallbacks = remember {
+ SearchCallbacks(
+ onLoadMore = model::loadMore,
+ onClearHintsClick = model::clearSearchHistory,
+ onTextHintClick = model::onTextHintClick,
+ onResultMarketTokenClick = model::onResultMarketTokenClick,
+ )
+ }
+ SearchContent(
+ modifier = modifier,
+ content = state.content,
+ searchCallbacks = searchCallbacks,
+ )
+ }
+
+ data class Params(
+ val onBackClick: () -> Unit,
+ )
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt
index 2fee6f7d0b..72864031f8 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt
@@ -11,6 +11,7 @@ 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 com.tangem.features.feed.model.news.list.NewsListModel
+import com.tangem.features.feed.model.search.SearchModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -65,4 +66,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(EarnTypeFilterModel::class)
fun provideEarnTypeFilterModel(model: EarnTypeFilterModel): Model
+
+ @Binds
+ @IntoMap
+ @ClassKey(SearchModel::class)
+ fun provideSearchModel(model: SearchModel): Model
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt
index 71a2894d6b..ac71423894 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt
@@ -325,7 +325,7 @@ internal class EarnModel @Inject constructor(
onNetworkFilterClick = ::onNetworkFilterClick,
onTypeFilterClick = ::onTypeFilterClick,
onScroll = ::onMostlyUsedScrolled,
- onSearchBarClicked = params.onMarketOpenClick,
+ onSearchBarClicked = params.onSearchClicked,
),
)
}
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt
index 3252a220e0..33fc8bb216 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt
@@ -13,6 +13,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
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.ui.DesignFeatureToggles
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@@ -66,6 +67,7 @@ internal class FeedComponentModel @Inject constructor(
private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase,
private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase,
private val appRouter: AppRouter,
+ private val designFeatureToggles: DesignFeatureToggles,
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
paramsContainer: ParamsContainer,
@@ -233,7 +235,7 @@ internal class FeedComponentModel @Inject constructor(
currentDate = getCurrentDate(),
feedListSearchBar = FeedListSearchBar(
placeholderText = resourceReference(
- id = if (feedFeatureToggle.isEarnBlockEnabled) {
+ id = if (designFeatureToggles.isRedesignEnabled) {
R.string.markets_search_title_placeholder
} else {
R.string.markets_search_header_title
@@ -241,7 +243,11 @@ internal class FeedComponentModel @Inject constructor(
),
onBarClick = {
analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked())
- params.feedClickIntents.onMarketOpenClick(null)
+ if (designFeatureToggles.isRedesignEnabled) {
+ params.feedClickIntents.openSearch()
+ } else {
+ params.feedClickIntents.onMarketOpenClick(null)
+ }
},
),
feedListCallbacks = FeedListCallbacks(
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt
index b9778a2ed6..96ff7cc2f1 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt
@@ -29,4 +29,6 @@ internal interface FeedModelClickIntents {
fun onOpenAllNews()
fun onOpenEarnPage()
+
+ fun openSearch()
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt
index eb07937be6..b92a688326 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt
@@ -53,7 +53,6 @@ import com.tangem.features.feed.model.market.details.converter.TokenMarketInfoCo
import com.tangem.features.feed.model.market.details.formatter.*
import com.tangem.features.feed.model.market.details.state.QuotesStateUpdater
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
-import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
import com.tangem.lib.crypto.BlockchainUtils
@@ -260,10 +259,6 @@ internal class MarketsTokenDetailsModel @Inject constructor(
onScroll = {},
),
onShareClick = ::onShareClick,
- feedListSearchBar = FeedListSearchBar(
- onBarClick = { params.onMarketOpenClick() },
- placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
- ),
),
)
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListBatchFlowManager.kt
index b78cb4a6e8..781dceeb6d 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListBatchFlowManager.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListBatchFlowManager.kt
@@ -75,6 +75,11 @@ internal class MarketsListBatchFlowManager(
private val resultBatches = MutableStateFlow(ResultBatches())
private val uiBatches = stateManager.state.map { it.uiBatches }
+ val rawItems: Flow>
+ get() = batchFlow.state
+ .map { state -> state.data.flatMap { batch -> batch.data } }
+ .distinctUntilChanged()
+
val uiItems: StateFlow>
get() = uiBatches
.map { batches ->
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt
new file mode 100644
index 0000000000..fb91bf9fd9
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt
@@ -0,0 +1,302 @@
+package com.tangem.features.feed.model.search
+
+import arrow.core.getOrElse
+import com.tangem.common.ui.markets.models.MarketsListItemUM
+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.ui.components.marketprice.PriceChangeType
+import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
+import com.tangem.domain.models.account.AccountName
+import com.tangem.domain.search.model.RecentSearchToken
+import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
+import com.tangem.domain.search.usecase.GetSearchResultsUseCase
+import com.tangem.domain.search.usecase.SaveSearchQueryUseCase
+import com.tangem.features.feed.components.search.DefaultSearchComponent
+import com.tangem.features.feed.model.market.list.state.MarketsListUM
+import com.tangem.features.feed.model.market.list.state.SortByTypeUM
+import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager
+import com.tangem.features.feed.model.search.state.SearchStateController
+import com.tangem.features.feed.model.search.state.transformers.*
+import com.tangem.features.feed.ui.search.state.*
+import com.tangem.utils.Provider
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import com.tangem.utils.coroutines.JobHolder
+import com.tangem.utils.coroutines.saveIn
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
+
+@Suppress("LongParameterList")
+@ModelScoped
+internal class SearchModel @Inject constructor(
+ override val dispatchers: CoroutineDispatcherProvider,
+ paramsContainer: ParamsContainer,
+ getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
+ getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
+ private val getSearchResultsUseCase: GetSearchResultsUseCase,
+ private val saveSearchQueryUseCase: SaveSearchQueryUseCase,
+ private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase,
+ private val stateController: SearchStateController,
+) : Model() {
+
+ private val params = paramsContainer.require()
+
+ private val updateQuotesJob = JobHolder()
+ private val searchResultsJob = JobHolder()
+ private var shouldShowAllTokensIncludingUnder100k = false
+
+ private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
+ maybeAppCurrency.getOrElse { AppCurrency.Default }
+ }.stateIn(
+ scope = modelScope,
+ started = SharingStarted.Eagerly,
+ initialValue = AppCurrency.Default,
+ )
+
+ private val searchMarketsListManager by lazy {
+ MarketsListBatchFlowManager(
+ getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
+ batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
+ currentAppCurrency = Provider { currentAppCurrency.value },
+ currentTrendInterval = Provider { MarketsListUM.TrendInterval.H24 },
+ currentSortByType = Provider { SortByTypeUM.Rating },
+ currentSearchText = Provider { stateController.value.searchBar.query },
+ modelScope = modelScope,
+ dispatchers = dispatchers,
+ )
+ }
+
+ val state: StateFlow get() = stateController.uiState
+
+ init {
+ initCallbacks()
+ subscribeToQueryChanges()
+ subscribeToMarketUiItems()
+ subscribeToQuotesPolling()
+ subscribeToAppCurrencyChanges()
+ loadHistory()
+ }
+
+ fun loadMore() {
+ val content = stateController.value.content
+ if (content is SearchContentUM.Results) {
+ val market = content.marketTokens
+ if (market is MarketSearchResultUM.Content) {
+ searchMarketsListManager.loadMore()
+ }
+ }
+ }
+
+ fun clearSearchHistory() {
+ modelScope.launch(dispatchers.default) {
+ clearSearchHistoryUseCase()
+ }
+ }
+
+ fun onTextHintClick(text: String) {
+ stateController.update(UpdateSearchBarQueryTransformer(text))
+ }
+
+ fun onResultMarketTokenClick() {
+ modelScope.launch(dispatchers.default) {
+ saveSearchQueryUseCase(stateController.value.searchBar.query)
+ }
+ }
+
+ private fun initCallbacks() {
+ stateController.update(object : SearchUMTransformer {
+ override fun transform(prevState: SearchUM): SearchUM {
+ return prevState.copy(
+ searchBar = prevState.searchBar.copy(
+ onQueryChange = ::onQueryChange,
+ onActiveChange = ::onActiveChange,
+ onClearClick = ::onClearClick,
+ ),
+ )
+ }
+ })
+ }
+
+ private fun onQueryChange(query: String) {
+ stateController.update(UpdateSearchBarQueryTransformer(query))
+ }
+
+ private fun onActiveChange(isActive: Boolean) {
+ if (!isActive) params.onBackClick()
+ }
+
+ private fun onClearClick() {
+ stateController.update(UpdateSearchBarQueryTransformer(""))
+ }
+
+ private fun subscribeToQueryChanges() {
+ stateController.uiState
+ .map { it.searchBar.query.trim() }
+ .distinctUntilChanged()
+ .onEach { query ->
+ shouldShowAllTokensIncludingUnder100k = false
+ if (query.isEmpty()) {
+ searchMarketsListManager.clearStateAndStopAllActions()
+ updateQuotesJob.cancel()
+ loadHistory()
+ } else {
+ stateController.update(SetSearchResultsLoadingTransformer())
+ searchMarketsListManager.reload(searchText = query)
+ subscribeToSearchResults(query)
+ }
+ }
+ .launchIn(modelScope)
+ }
+
+ private fun subscribeToSearchResults(query: String) {
+ modelScope.launch {
+ getSearchResultsUseCase(
+ query = query,
+ marketTokens = searchMarketsListManager.rawItems,
+ ).collectLatest { searchResult ->
+ val userAssets = searchResult.userAssets.map { entry ->
+ UserAssetItemUM(
+ id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" +
+ "_${entry.currencyStatus.currency.id.value}",
+ tokenIconUrl = entry.currencyStatus.currency.iconUrl,
+ tokenName = entry.currencyStatus.currency.name,
+ tokenSymbol = entry.currencyStatus.currency.symbol,
+ accountName = entry.accountName.toDisplayString(),
+ onClick = {
+ // TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task.
+ },
+ )
+ }.toImmutableList()
+ stateController.update(UpdateUserAssetsTransformer(userAssets))
+ }
+ }.saveIn(searchResultsJob)
+ }
+
+ private fun subscribeToQuotesPolling() {
+ searchMarketsListManager.onLastBatchLoadedSuccess.onEach { batchKey ->
+ searchMarketsListManager.loadCharts(setOf(batchKey), MarketsListUM.TrendInterval.H24)
+ modelScope.loadQuotesWithTimer(UPDATE_QUOTES_TIMER_MILLIS)
+ }.launchIn(modelScope)
+ }
+
+ private fun subscribeToAppCurrencyChanges() {
+ currentAppCurrency.drop(1).onEach {
+ val query = stateController.value.searchBar.query
+ if (query.isNotEmpty()) {
+ searchMarketsListManager.reload()
+ }
+ }.launchIn(modelScope)
+ }
+
+ private fun subscribeToMarketUiItems() {
+ combine(
+ flow = stateController.uiState.map { it.searchBar.query }.distinctUntilChanged(),
+ flow2 = searchMarketsListManager.uiItems,
+ flow3 = searchMarketsListManager.isSearchNotFoundState,
+ flow4 = searchMarketsListManager.isInInitialLoadingErrorState,
+ ) { query, uiItems, isSearchNotFound, isInErrorState ->
+ if (query.isEmpty()) return@combine null
+ val marketResult = when {
+ isSearchNotFound -> MarketSearchResultUM.NotFound
+ isInErrorState -> MarketSearchResultUM.NotFound
+ uiItems.isEmpty() -> MarketSearchResultUM.Empty
+ else -> buildMarketContent(uiItems)
+ }
+ SearchMarketBatchUiSnapshot(
+ marketResult = marketResult,
+ isSearchNotFound = isSearchNotFound,
+ isInErrorState = isInErrorState,
+ )
+ }
+ .filterNotNull()
+ .onEach { snapshot ->
+ stateController.update(ApplySearchMarketBatchTransformer(snapshot))
+ }
+ .launchIn(modelScope)
+ }
+
+ private fun buildMarketContent(allItems: ImmutableList): MarketSearchResultUM.Content {
+ if (shouldShowAllTokensIncludingUnder100k) {
+ return MarketSearchResultUM.Content(items = allItems)
+ }
+
+ val filtered = allItems.filter { !it.isUnder100kMarketCap }.toImmutableList()
+ val hasUnder100k = filtered.size != allItems.size
+
+ return if (hasUnder100k) {
+ MarketSearchResultUM.Content(
+ items = filtered,
+ shouldShowUnder100kNotification = true,
+ onShowUnder100kClick = {
+ shouldShowAllTokensIncludingUnder100k = true
+ stateController.update(
+ UpdateMarketItemsTransformer(
+ MarketSearchResultUM.Content(items = allItems),
+ ),
+ )
+ },
+ )
+ } else {
+ MarketSearchResultUM.Content(items = allItems)
+ }
+ }
+
+ private fun loadHistory() {
+ modelScope.launch {
+ getSearchResultsUseCase(query = "").collectLatest { searchResult ->
+ val textHints = searchResult.textHints.map { hint ->
+ TextHintItemUM(text = hint.text)
+ }.toImmutableList()
+
+ val recentTokens = searchResult.recentTokens.map { token ->
+ token.toMarketsListItemUM()
+ }.toImmutableList()
+
+ stateController.update(UpdateHistoryTransformer(textHints, recentTokens))
+ }
+ }.saveIn(searchResultsJob)
+ }
+
+ private fun RecentSearchToken.toMarketsListItemUM(): MarketsListItemUM {
+ return MarketsListItemUM(
+ id = id,
+ name = name,
+ currencySymbol = symbol,
+ iconUrl = imageUrl,
+ ratingPosition = null,
+ marketCap = null,
+ price = MarketsListItemUM.Price(text = ""),
+ trendPercentText = "",
+ trendType = PriceChangeType.NEUTRAL,
+ chartData = null,
+ isUnder100kMarketCap = false,
+ stakingRate = null,
+ updateTimestamp = timestamp,
+ )
+ }
+
+ private fun AccountName.toDisplayString(): String {
+ return when (this) {
+ is AccountName.DefaultMain -> "Main" // TODO [REDACTED_TASK_KEY] localize
+ is AccountName.Custom -> value
+ }
+ }
+
+ private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
+ launch {
+ while (true) {
+ delay(timeMillis)
+ searchMarketsListManager.updateQuotes()
+ }
+ }.saveIn(updateQuotesJob)
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/SearchStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/SearchStateController.kt
new file mode 100644
index 0000000000..ceccae14fa
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/SearchStateController.kt
@@ -0,0 +1,43 @@
+package com.tangem.features.feed.model.search.state
+
+import com.tangem.core.decompose.di.ModelScoped
+import com.tangem.core.ui.components.fields.entity.SearchBarUM
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.features.feed.impl.R
+import com.tangem.features.feed.model.search.state.transformers.SearchUMTransformer
+import com.tangem.features.feed.ui.search.state.SearchContentUM
+import com.tangem.features.feed.ui.search.state.SearchUM
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import javax.inject.Inject
+
+@ModelScoped
+internal class SearchStateController @Inject constructor() {
+
+ private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState())
+
+ val uiState: StateFlow = mutableUiState.asStateFlow()
+
+ val value: SearchUM
+ get() = uiState.value
+
+ fun update(transformer: SearchUMTransformer) {
+ mutableUiState.update(function = transformer::transform)
+ }
+
+ private fun getInitialState(): SearchUM {
+ return SearchUM(
+ searchBar = SearchBarUM(
+ placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
+ query = "",
+ onQueryChange = {},
+ isActive = false,
+ onActiveChange = {},
+ onClearClick = {},
+ ),
+ content = SearchContentUM.InitialEmpty,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/ApplySearchMarketBatchTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/ApplySearchMarketBatchTransformer.kt
new file mode 100644
index 0000000000..41dde90476
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/ApplySearchMarketBatchTransformer.kt
@@ -0,0 +1,32 @@
+package com.tangem.features.feed.model.search.state.transformers
+
+import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
+import com.tangem.features.feed.ui.search.state.SearchContentUM
+import com.tangem.features.feed.ui.search.state.SearchUM
+
+internal class ApplySearchMarketBatchTransformer(
+ private val snapshot: SearchMarketBatchUiSnapshot,
+) : SearchUMTransformer {
+
+ override fun transform(prevState: SearchUM): SearchUM {
+ if (shouldIgnoreTransientMarketEmpty(prevState, snapshot)) return prevState
+ return UpdateMarketItemsTransformer(snapshot.marketResult).transform(prevState)
+ }
+
+ private fun shouldIgnoreTransientMarketEmpty(prevState: SearchUM, snapshot: SearchMarketBatchUiSnapshot): Boolean {
+ if (snapshot.marketResult !is MarketSearchResultUM.Empty) return false
+ if (snapshot.isSearchNotFound || snapshot.isInErrorState) return false
+ val prev = prevState.content as? SearchContentUM.Results ?: return false
+ return when (prev.marketTokens) {
+ is MarketSearchResultUM.Content, is MarketSearchResultUM.Loading -> true
+ else -> false
+ }
+ }
+}
+
+/** Inputs from the search market list flow before merging into [SearchUM]. */
+internal data class SearchMarketBatchUiSnapshot(
+ val marketResult: MarketSearchResultUM,
+ val isSearchNotFound: Boolean,
+ val isInErrorState: Boolean,
+)
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SearchUMTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SearchUMTransformer.kt
new file mode 100644
index 0000000000..e39fd493b9
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SearchUMTransformer.kt
@@ -0,0 +1,7 @@
+package com.tangem.features.feed.model.search.state.transformers
+
+import com.tangem.features.feed.ui.search.state.SearchUM
+
+internal interface SearchUMTransformer {
+ fun transform(prevState: SearchUM): SearchUM
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SetSearchResultsLoadingTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SetSearchResultsLoadingTransformer.kt
new file mode 100644
index 0000000000..480dd0574a
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SetSearchResultsLoadingTransformer.kt
@@ -0,0 +1,35 @@
+package com.tangem.features.feed.model.search.state.transformers
+
+import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
+import com.tangem.features.feed.ui.search.state.SearchContentUM
+import com.tangem.features.feed.ui.search.state.SearchUM
+import kotlinx.collections.immutable.persistentListOf
+
+internal class SetSearchResultsLoadingTransformer : SearchUMTransformer {
+
+ override fun transform(prevState: SearchUM): SearchUM {
+ val currentContent = prevState.content
+ val currentUserAssets = if (currentContent is SearchContentUM.Results) {
+ currentContent.userAssets
+ } else {
+ persistentListOf()
+ }
+ // Keep showing the previous market list while the new query loads (stale-while-revalidate).
+ // Replacing with Loading + empty batch flashes skeletons / empty section even when the API
+ // returns the same tokens for a refined query.
+ val nextMarketTokens = if (currentContent is SearchContentUM.Results) {
+ when (val market = currentContent.marketTokens) {
+ is MarketSearchResultUM.Content -> market
+ else -> MarketSearchResultUM.Loading
+ }
+ } else {
+ MarketSearchResultUM.Loading
+ }
+ return prevState.copy(
+ content = SearchContentUM.Results(
+ userAssets = currentUserAssets,
+ marketTokens = nextMarketTokens,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateHistoryTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateHistoryTransformer.kt
new file mode 100644
index 0000000000..ec829bf58d
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateHistoryTransformer.kt
@@ -0,0 +1,22 @@
+package com.tangem.features.feed.model.search.state.transformers
+
+import com.tangem.common.ui.markets.models.MarketsListItemUM
+import com.tangem.features.feed.ui.search.state.SearchContentUM
+import com.tangem.features.feed.ui.search.state.SearchUM
+import com.tangem.features.feed.ui.search.state.TextHintItemUM
+import kotlinx.collections.immutable.ImmutableList
+
+internal class UpdateHistoryTransformer(
+ private val textHints: ImmutableList,
+ private val recentTokens: ImmutableList,
+) : SearchUMTransformer {
+
+ override fun transform(prevState: SearchUM): SearchUM {
+ return prevState.copy(
+ content = SearchContentUM.History(
+ textHints = textHints,
+ recentTokens = recentTokens,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateMarketItemsTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateMarketItemsTransformer.kt
new file mode 100644
index 0000000000..00e460055f
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateMarketItemsTransformer.kt
@@ -0,0 +1,27 @@
+package com.tangem.features.feed.model.search.state.transformers
+
+import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
+import com.tangem.features.feed.ui.search.state.SearchContentUM
+import com.tangem.features.feed.ui.search.state.SearchUM
+import kotlinx.collections.immutable.persistentListOf
+
+internal class UpdateMarketItemsTransformer(
+ private val marketResult: MarketSearchResultUM,
+) : SearchUMTransformer {
+
+ override fun transform(prevState: SearchUM): SearchUM {
+ val currentContent = prevState.content
+
+ val currentUserAssets = if (currentContent is SearchContentUM.Results) {
+ currentContent.userAssets
+ } else {
+ persistentListOf()
+ }
+ return prevState.copy(
+ content = SearchContentUM.Results(
+ userAssets = currentUserAssets,
+ marketTokens = marketResult,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateSearchBarQueryTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateSearchBarQueryTransformer.kt
new file mode 100644
index 0000000000..f1dc142848
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateSearchBarQueryTransformer.kt
@@ -0,0 +1,14 @@
+package com.tangem.features.feed.model.search.state.transformers
+
+import com.tangem.features.feed.ui.search.state.SearchUM
+
+internal class UpdateSearchBarQueryTransformer(
+ private val query: String,
+) : SearchUMTransformer {
+
+ override fun transform(prevState: SearchUM): SearchUM {
+ return prevState.copy(
+ searchBar = prevState.searchBar.copy(query = query),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt
new file mode 100644
index 0000000000..d99fdf0e7c
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt
@@ -0,0 +1,27 @@
+package com.tangem.features.feed.model.search.state.transformers
+
+import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
+import com.tangem.features.feed.ui.search.state.SearchContentUM
+import com.tangem.features.feed.ui.search.state.SearchUM
+import com.tangem.features.feed.ui.search.state.UserAssetItemUM
+import kotlinx.collections.immutable.ImmutableList
+
+internal class UpdateUserAssetsTransformer(
+ private val userAssets: ImmutableList,
+) : SearchUMTransformer {
+
+ override fun transform(prevState: SearchUM): SearchUM {
+ val currentContent = prevState.content
+ val currentMarketTokens = if (currentContent is SearchContentUM.Results) {
+ currentContent.marketTokens
+ } else {
+ MarketSearchResultUM.Loading
+ }
+ return prevState.copy(
+ content = SearchContentUM.Results(
+ userAssets = userAssets,
+ marketTokens = currentMarketTokens,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt
index a8efdab5d9..4f1527ad16 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt
@@ -1,5 +1,6 @@
package com.tangem.features.feed.ui
+import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Scaffold
@@ -11,16 +12,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
-import com.arkivanov.decompose.extensions.compose.stack.Children
+import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.stack.ChildStack
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.utils.WindowInsetsZero
import com.tangem.features.feed.components.FeedEntryChildFactory
-import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation
-import com.tangem.features.feed.ui.utils.topBarFeedEntryStackAnimation
+import com.tangem.features.feed.ui.utils.contentFeedEntryAnimatedContentTransitionSpec
+import com.tangem.features.feed.ui.utils.topBarFeedEntryAnimatedContentTransitionSpec
+@OptIn(ExperimentalDecomposeApi::class)
@Composable
internal fun EntryContent(
bottomSheetState: State,
@@ -30,15 +32,15 @@ internal fun EntryContent(
) {
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
- val animationContent = remember { contentFeedEntryStackAnimation() }
- val animationAppBar = remember { topBarFeedEntryStackAnimation() }
+ val animationContent = remember(stackState) { contentFeedEntryAnimatedContentTransitionSpec(stackState) }
+ val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) }
Surface(contentColor = background) {
Scaffold(
containerColor = background,
contentWindowInsets = WindowInsetsZero,
topBar = {
- Children(
+ AnimatedContent(
modifier = Modifier
.then(
if (!isOpenedInBottomSheet) {
@@ -54,18 +56,22 @@ internal fun EntryContent(
}
}
},
- stack = stackState.value,
- animation = animationAppBar,
- ) { child ->
- child.instance.Title(bottomSheetState)
+ targetState = stackState.value.active,
+ transitionSpec = animationAppBar,
+ contentKey = { it.key },
+ label = "FeedEntryAppBar",
+ ) { state ->
+ state.instance.Title(bottomSheetState)
}
},
content = { contentPadding ->
- Children(
- stack = stackState.value,
- animation = animationContent,
- ) { child ->
- child.instance.Content(
+ AnimatedContent(
+ targetState = stackState.value.active,
+ transitionSpec = animationContent,
+ contentKey = { it.key },
+ label = "FeedEntryContent",
+ ) { state ->
+ state.instance.Content(
modifier = Modifier.padding(contentPadding),
bottomSheetState = bottomSheetState,
)
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt
index 34aa87a805..7d9ab43a62 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt
@@ -107,7 +107,7 @@ private fun FeedSearchBarV2(
end = if (endContent != null) TangemTheme.dimens2.x3 else 0.dp,
)
.clip(CircleShape)
- .background(color = TangemTheme.colors2.field.backgroundDefault)
+ .background(color = TangemTheme.colors2.button.backgroundSecondary)
.conditional(condition = isSearchBarClickable) {
clickable(onClick = feedListSearchBar.onBarClick)
}
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt
index 4601283941..443eff2ac5 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt
@@ -17,11 +17,10 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
-import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
-import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
@@ -31,6 +30,8 @@ import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
+import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.features.feed.ui.feed.state.*
internal const val FOURTH_ITEM_INDEX = 3
@@ -87,8 +88,16 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResourceSafe(R.string.common_news),
- style = TangemTheme.typography.h3,
- color = TangemTheme.colors.text.primary1,
+ style = if (isRedesignEnabled) {
+ TangemTheme.typography2.headingSemibold20
+ } else {
+ TangemTheme.typography.h3
+ },
+ color = if (isRedesignEnabled) {
+ TangemTheme.colors2.text.neutral.primary
+ } else {
+ TangemTheme.colors.text.primary1
+ },
)
SpacerW(4.dp)
@@ -113,7 +122,11 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
append(stringResourceSafe(R.string.feed_tangem_ai))
}
},
- style = TangemTheme.typography.subtitle1,
+ style = if (isRedesignEnabled) {
+ TangemTheme.typography2.bodyRegular16.copy(fontWeight = FontWeight.Medium)
+ } else {
+ TangemTheme.typography.subtitle1
+ },
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt
index 65ce49e61e..ed7261d399 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt
@@ -5,10 +5,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
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.markets.PriceChangeInterval
-import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.features.feed.ui.market.detailed.state.*
import kotlinx.collections.immutable.persistentListOf
@@ -52,10 +50,6 @@ internal object MarketsTokenDetailsPreview {
onScroll = {},
),
onShareClick = {},
- feedListSearchBar = FeedListSearchBar(
- placeholderText = TextReference.Str("Search tokens & news"),
- onBarClick = {},
- ),
)
val contentState = MarketsTokenDetailsUM(
@@ -150,9 +144,5 @@ internal object MarketsTokenDetailsPreview {
onScroll = {},
),
onShareClick = {},
- feedListSearchBar = FeedListSearchBar(
- placeholderText = TextReference.Str("Search tokens & news"),
- onBarClick = {},
- ),
)
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt
index c6e0bde66a..7da2ed065b 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt
@@ -2,13 +2,12 @@ package com.tangem.features.feed.ui.market.detailed.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.charts.state.MarketChartDataProducer
-import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.markets.PriceChangeInterval
-import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
+import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@@ -30,7 +29,6 @@ internal data class MarketsTokenDetailsUM(
val onShouldShowPriceSubtitleChange: (Boolean) -> Unit,
val relatedNews: RelatedNews,
val onShareClick: () -> Unit,
- val feedListSearchBar: FeedListSearchBar,
) {
data class ChartState(
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt
new file mode 100644
index 0000000000..d1f01ad1cc
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt
@@ -0,0 +1,348 @@
+package com.tangem.features.feed.ui.search
+
+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.LazyListScope
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.tangem.common.ui.markets.MarketsListItem
+import com.tangem.common.ui.markets.MarketsListItemPlaceholder
+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.list.InfiniteListHandler
+import com.tangem.core.ui.ds.button.*
+import com.tangem.core.ui.ds.image.TangemIcon
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.extensions.clickableSingle
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.feed.ui.search.state.*
+
+private const val PLACEHOLDER_COUNT = 10
+private const val LOAD_MORE_THRESHOLD = 5
+
+@Composable
+internal fun SearchContent(content: SearchContentUM, searchCallbacks: SearchCallbacks, modifier: Modifier = Modifier) {
+ val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
+ val lazyListState = rememberLazyListState()
+
+ LazyColumn(
+ state = lazyListState,
+ modifier = modifier.fillMaxSize(),
+ contentPadding = PaddingValues(
+ start = TangemTheme.dimens2.x4,
+ end = TangemTheme.dimens2.x4,
+ bottom = bottomBarHeight,
+ ),
+ ) {
+ when (content) {
+ is SearchContentUM.InitialEmpty -> Unit
+ is SearchContentUM.History -> searchHistoryItems(
+ history = content,
+ onClearAllClick = searchCallbacks.onClearHintsClick,
+ onHintClick = searchCallbacks.onTextHintClick,
+ )
+ is SearchContentUM.Results -> searchResultsItems(
+ results = content,
+ onResultMarketTokenClick = searchCallbacks.onResultMarketTokenClick,
+ )
+ }
+ }
+ if (content is SearchContentUM.Results) {
+ InfiniteListHandler(
+ listState = lazyListState,
+ buffer = LOAD_MORE_THRESHOLD,
+ triggerLoadMoreCheckOnItemsCountChange = true,
+ onLoadMore = remember(content.marketTokens) {
+ {
+ searchCallbacks.onLoadMore()
+ true
+ }
+ },
+ )
+ }
+}
+
+private fun LazyListScope.searchHistoryItems(
+ history: SearchContentUM.History,
+ onClearAllClick: (() -> Unit),
+ onHintClick: (String) -> Unit,
+) {
+ if (!history.textHints.isEmpty() || !history.recentTokens.isEmpty()) {
+ item(key = "recents") {
+ SectionHeader(
+ title = stringResourceSafe(R.string.markets_search_hint_header),
+ onClearAllClick = onClearAllClick,
+ )
+ }
+ }
+ items(
+ items = history.textHints,
+ key = { "hint_${it.text}" },
+ ) { hint ->
+ TextHintItem(hint = hint, onHintClick = { onHintClick(hint.text) })
+ HorizontalDivider(
+ modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2),
+ color = TangemTheme.colors2.border.neutral.primary,
+ )
+ }
+ items(
+ items = history.recentTokens,
+ key = { "recent_${it.getComposeKey()}" },
+ ) { token ->
+ MarketsListItem(
+ modifier = Modifier
+ .padding(bottom = TangemTheme.dimens2.x2)
+ .background(
+ color = TangemTheme.colors2.surface.level3,
+ shape = RoundedCornerShape(TangemTheme.dimens2.x5),
+ ),
+ model = token,
+ onClick = {}, // TODO in [REDACTED_TASK_KEY]
+ )
+ }
+}
+
+private fun LazyListScope.searchResultsItems(results: SearchContentUM.Results, onResultMarketTokenClick: () -> Unit) {
+ if (results.userAssets.isNotEmpty()) {
+ item(key = "header_portfolio") {
+ SectionHeader(title = stringResourceSafe(R.string.markets_search_portfolio_header))
+ }
+ items(
+ items = results.userAssets,
+ key = { it.id },
+ ) { asset ->
+ UserAssetItem(asset)
+ }
+ }
+
+ when (val market = results.marketTokens) {
+ is MarketSearchResultUM.Empty -> Unit
+ is MarketSearchResultUM.Content -> marketSearchResultItems(
+ market = market,
+ hasUserAssetsSection = results.userAssets.isNotEmpty(),
+ onResultMarketTokenClick = onResultMarketTokenClick,
+ )
+ is MarketSearchResultUM.Loading -> marketSearchResultLoadingItems(
+ hasUserAssetsSection = results.userAssets.isNotEmpty(),
+ )
+ is MarketSearchResultUM.NotFound -> marketSearchResultNotFoundItem()
+ }
+}
+
+private fun LazyListScope.marketSearchResultItems(
+ market: MarketSearchResultUM.Content,
+ hasUserAssetsSection: Boolean,
+ onResultMarketTokenClick: () -> Unit,
+) {
+ if (hasUserAssetsSection) {
+ item(key = "spacer_between_sections") {
+ SpacerH(TangemTheme.dimens2.x9)
+ }
+ }
+ item(key = "header_market") {
+ SectionHeader(title = stringResourceSafe(R.string.markets_common_title))
+ }
+ items(
+ items = market.items,
+ key = { "market_${it.getComposeKey()}" },
+ ) { token ->
+ MarketsListItem(
+ modifier = Modifier
+ .padding(bottom = TangemTheme.dimens2.x2)
+ .background(
+ color = TangemTheme.colors2.surface.level3,
+ shape = RoundedCornerShape(TangemTheme.dimens2.x5),
+ ),
+ model = token,
+ onClick = onResultMarketTokenClick,
+ )
+ }
+ if (market.shouldShowUnder100kNotification) {
+ item(key = "show_tokens_under_100k") {
+ ShowTokensUnder100kItem(
+ onShowTokensClick = market.onShowUnder100kClick,
+ )
+ }
+ }
+}
+
+private fun LazyListScope.marketSearchResultLoadingItems(hasUserAssetsSection: Boolean) {
+ if (hasUserAssetsSection) {
+ item(key = "spacer_between_sections") {
+ SpacerH(TangemTheme.dimens2.x9)
+ }
+ }
+ item(key = "header_market") {
+ SectionHeader(title = stringResourceSafe(R.string.markets_common_title))
+ }
+ items(count = PLACEHOLDER_COUNT) {
+ MarketsListItemPlaceholder()
+ }
+}
+
+private fun LazyListScope.marketSearchResultNotFoundItem() {
+ item(key = "not_found") {
+ Box(
+ modifier = Modifier.fillParentMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = stringResourceSafe(R.string.common_no_results),
+ style = TangemTheme.typography2.bodyRegular14,
+ color = TangemTheme.colors2.text.neutral.tertiary,
+ )
+ }
+ }
+}
+
+@Composable
+private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onHintClick)
+ .padding(vertical = 22.dp, horizontal = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_default_24),
+ contentDescription = null,
+ tint = TangemTheme.colors2.graphic.neutral.primary,
+ modifier = Modifier.size(TangemTheme.dimens2.x5),
+ )
+ SpacerW(TangemTheme.dimens2.x1)
+ Text(
+ modifier = Modifier.weight(1f),
+ text = hint.text,
+ style = TangemTheme.typography2.bodySemibold16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ )
+ SpacerW(TangemTheme.dimens2.x2)
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_return_24),
+ contentDescription = null,
+ tint = TangemTheme.colors2.markers.iconGray,
+ modifier = Modifier.size(TangemTheme.dimens2.x6),
+ )
+ }
+}
+
+// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task.
+@Composable
+private fun UserAssetItem(asset: UserAssetItemUM) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = asset.onClick)
+ .padding(horizontal = 12.dp, vertical = 14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ TangemIcon(
+ tangemIconUM = TangemIconUM.Url(asset.tokenIconUrl, fallbackRes = R.drawable.ic_custom_token_44),
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape),
+ )
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = asset.tokenName,
+ style = TangemTheme.typography2.bodySemibold16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ )
+ Text(
+ text = "${asset.tokenSymbol} · ${asset.accountName}",
+ style = TangemTheme.typography2.captionRegular13,
+ color = TangemTheme.colors2.text.neutral.tertiary,
+ maxLines = 1,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .padding(
+ top = TangemTheme.dimens2.x9,
+ bottom = TangemTheme.dimens2.x3,
+ start = TangemTheme.dimens2.x10,
+ end = TangemTheme.dimens2.x10,
+ )
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k),
+ style = TangemTheme.typography2.bodyRegular14,
+ color = TangemTheme.colors2.text.neutral.secondary,
+ )
+ TangemButton(
+ buttonUM = TangemButtonUM(
+ text = resourceReference(R.string.markets_search_show_tokens),
+ onClick = onShowTokensClick,
+ type = TangemButtonType.Secondary,
+ size = TangemButtonSize.X8,
+ shape = TangemButtonShape.Rounded,
+ ),
+ )
+ }
+}
+
+@Composable
+private fun SectionHeader(title: String, modifier: Modifier = Modifier, onClearAllClick: (() -> Unit)? = null) {
+ Row(
+ modifier = modifier
+ .padding(
+ vertical = TangemTheme.dimens2.x3,
+ horizontal = TangemTheme.dimens2.x2,
+ )
+ .padding(bottom = TangemTheme.dimens2.x3),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = title,
+ style = TangemTheme.typography2.headingSemibold20,
+ color = TangemTheme.colors2.text.neutral.primary,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ )
+
+ if (onClearAllClick != null) {
+ Text(
+ modifier = Modifier.clickableSingle(onClick = onClearAllClick),
+ text = stringResourceSafe(R.string.markets_search_clear_all_hints),
+ style = TangemTheme.typography2.bodySemibold16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt
new file mode 100644
index 0000000000..227ab6c942
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt
@@ -0,0 +1,538 @@
+package com.tangem.features.feed.ui.search.preview
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import com.tangem.common.ui.charts.state.MarketChartRawData
+import com.tangem.common.ui.markets.models.MarketsListItemUM
+import com.tangem.core.ui.components.marketprice.PriceChangeType
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.features.feed.ui.search.SearchContent
+import com.tangem.features.feed.ui.search.state.*
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+
+/** Labeled UI state for [SearchContent] previews. */
+internal data class SearchContentPreviewScenario(
+ val title: String,
+ val content: SearchContentUM,
+)
+
+/** Sample market rows, portfolio assets, and hints for [SearchContent] previews (callbacks are no-op). */
+@Suppress("MagicNumber", "StringLiteralDuplication", "LargeClass")
+internal object SearchContentPreviewFixtures {
+
+ private val chartYSample = persistentListOf(
+ 0.4, 0.2, 0.45, 0.35, 0.5, 0.55, 0.48, 0.62, 0.58, 0.7, 0.65, 0.8,
+ )
+
+ val scenarioInitialEmpty = SearchContentPreviewScenario(
+ title = "InitialEmpty – blank screen before input",
+ content = SearchContentUM.InitialEmpty,
+ )
+
+ val scenarioHistoryEmptyBoth = SearchContentPreviewScenario(
+ title = "History – empty hints and recents (no Recents header)",
+ content = SearchContentUM.History(
+ textHints = persistentListOf(),
+ recentTokens = persistentListOf(),
+ ),
+ )
+
+ val scenarioHistoryHintsOnly = SearchContentPreviewScenario(
+ title = "History – text hints only",
+ content = SearchContentUM.History(
+ textHints = hintsSample(),
+ recentTokens = persistentListOf(),
+ ),
+ )
+
+ val scenarioHistoryRecentsOnly = SearchContentPreviewScenario(
+ title = "History – recent tokens only",
+ content = SearchContentUM.History(
+ textHints = persistentListOf(),
+ recentTokens = recentsSample(),
+ ),
+ )
+
+ val scenarioHistoryFull = SearchContentPreviewScenario(
+ title = "History – hints + recents + Clear all",
+ content = SearchContentUM.History(
+ textHints = hintsSample(),
+ recentTokens = recentsSample(),
+ ),
+ )
+
+ val scenarioResultsMarketEmptyNoPortfolio = SearchContentPreviewScenario(
+ title = "Results – Market.Empty, empty portfolio",
+ content = SearchContentUM.Results(
+ userAssets = persistentListOf(),
+ marketTokens = MarketSearchResultUM.Empty,
+ ),
+ )
+
+ val scenarioResultsMarketEmptyWithPortfolio = SearchContentPreviewScenario(
+ title = "Results – Market.Empty, with portfolio",
+ content = SearchContentUM.Results(
+ userAssets = portfolioTwo(),
+ marketTokens = MarketSearchResultUM.Empty,
+ ),
+ )
+
+ val scenarioResultsLoadingNoPortfolio = SearchContentPreviewScenario(
+ title = "Results – Market.Loading, no portfolio",
+ content = SearchContentUM.Results(
+ userAssets = persistentListOf(),
+ marketTokens = MarketSearchResultUM.Loading,
+ ),
+ )
+
+ val scenarioResultsLoadingWithPortfolio = SearchContentPreviewScenario(
+ title = "Results – Market.Loading + portfolio (spacer + Market header)",
+ content = SearchContentUM.Results(
+ userAssets = portfolioTwo(),
+ marketTokens = MarketSearchResultUM.Loading,
+ ),
+ )
+
+ val scenarioResultsNotFoundNoPortfolio = SearchContentPreviewScenario(
+ title = "Results – Market.NotFound, no portfolio",
+ content = SearchContentUM.Results(
+ userAssets = persistentListOf(),
+ marketTokens = MarketSearchResultUM.NotFound,
+ ),
+ )
+
+ val scenarioResultsNotFoundWithPortfolio = SearchContentPreviewScenario(
+ title = "Results – Market.NotFound + portfolio",
+ content = SearchContentUM.Results(
+ userAssets = portfolioTwo(),
+ marketTokens = MarketSearchResultUM.NotFound,
+ ),
+ )
+
+ val scenarioResultsContentMarketOnly = SearchContentPreviewScenario(
+ title = "Results – Market.Content, market only",
+ content = SearchContentUM.Results(
+ userAssets = persistentListOf(),
+ marketTokens = MarketSearchResultUM.Content(
+ items = marketListShort(),
+ shouldShowUnder100kNotification = false,
+ onShowUnder100kClick = {},
+ ),
+ ),
+ )
+
+ val scenarioResultsContentPortfolioAndMarket = SearchContentPreviewScenario(
+ title = "Results – portfolio + market (no under 100k)",
+ content = SearchContentUM.Results(
+ userAssets = portfolioTwo(),
+ marketTokens = MarketSearchResultUM.Content(
+ items = marketListShort(),
+ shouldShowUnder100kNotification = false,
+ onShowUnder100kClick = {},
+ ),
+ ),
+ )
+
+ val scenarioResultsContentWithUnder100kBanner = SearchContentPreviewScenario(
+ title = "Results – portfolio + market + under 100k banner",
+ content = SearchContentUM.Results(
+ userAssets = portfolioTwo(),
+ marketTokens = MarketSearchResultUM.Content(
+ items = marketListShort(),
+ shouldShowUnder100kNotification = true,
+ onShowUnder100kClick = {},
+ ),
+ ),
+ )
+
+ val scenarioResultsLongMarketScroll = SearchContentPreviewScenario(
+ title = "Results – long market list (scroll)",
+ content = SearchContentUM.Results(
+ userAssets = portfolioTwo(),
+ marketTokens = MarketSearchResultUM.Content(
+ items = marketListLong(),
+ shouldShowUnder100kNotification = true,
+ onShowUnder100kClick = {},
+ ),
+ ),
+ )
+
+ private fun marketToken(
+ rawId: String,
+ name: String,
+ symbol: String,
+ trend: PriceChangeType = PriceChangeType.UP,
+ priceText: String = "$98,765.43",
+ trendText: String = "+2.34%",
+ marketCap: String? = "$1.2 T",
+ rating: String? = "1",
+ chart: MarketChartRawData? = MarketChartRawData(y = chartYSample),
+ staking: Boolean = false,
+ updateTimestamp: Long = rawId.hashCode().toLong(),
+ ): MarketsListItemUM = MarketsListItemUM(
+ id = CryptoCurrency.RawID(rawId),
+ name = name,
+ currencySymbol = symbol,
+ iconUrl = null,
+ ratingPosition = rating,
+ marketCap = marketCap,
+ price = MarketsListItemUM.Price(text = priceText),
+ trendPercentText = trendText,
+ trendType = trend,
+ chartData = chart,
+ isUnder100kMarketCap = false,
+ stakingRate = if (staking) stringReference("APY 4.2%") else null,
+ updateTimestamp = updateTimestamp,
+ )
+
+ private fun userAsset(
+ id: String,
+ name: String,
+ symbol: String,
+ accountName: String,
+ iconUrl: String? = null,
+ ): UserAssetItemUM = UserAssetItemUM(
+ id = id,
+ tokenIconUrl = iconUrl,
+ tokenName = name,
+ tokenSymbol = symbol,
+ accountName = accountName,
+ onClick = {},
+ )
+
+ private fun textHint(text: String): TextHintItemUM = TextHintItemUM(text = text)
+
+ private fun hintsSample(): ImmutableList = persistentListOf(
+ textHint("bitcoin"),
+ textHint("sol"),
+ textHint("very long search query example for ellipsis"),
+ )
+
+ private fun recentsSample(): ImmutableList = persistentListOf(
+ marketToken(rawId = "btc_r", name = "Bitcoin", symbol = "BTC"),
+ marketToken(
+ rawId = "eth_r",
+ name = "Ethereum",
+ symbol = "ETH",
+ trend = PriceChangeType.DOWN,
+ trendText = "−1.02%",
+ rating = "2",
+ ),
+ marketToken(
+ rawId = "long_r",
+ name = "A Very Long Token Name That Should Ellipsize In The List",
+ symbol = "LONG",
+ trend = PriceChangeType.NEUTRAL,
+ trendText = "0.00%",
+ marketCap = "$999.123456789 B",
+ rating = "42",
+ ),
+ )
+
+ private fun portfolioTwo(): ImmutableList = persistentListOf(
+ userAsset(id = "p1", name = "Ethereum", symbol = "ETH", accountName = "Main wallet"),
+ userAsset(
+ id = "p2",
+ name = "Polygon",
+ symbol = "POL",
+ accountName = "Account with a long label for preview",
+ ),
+ )
+
+ private fun marketListShort(): ImmutableList = persistentListOf(
+ marketToken(rawId = "m1", name = "Bitcoin", symbol = "BTC", rating = "1"),
+ marketToken(
+ rawId = "m2",
+ name = "Ethereum",
+ symbol = "ETH",
+ trend = PriceChangeType.DOWN,
+ trendText = "−0.55%",
+ rating = "2",
+ ),
+ marketToken(
+ rawId = "m3",
+ name = "Solana",
+ symbol = "SOL",
+ trend = PriceChangeType.NEUTRAL,
+ trendText = "0.12%",
+ rating = "3",
+ ),
+ )
+
+ @Suppress("LongMethod")
+ private fun marketListLong(): ImmutableList = listOf(
+ marketToken(
+ rawId = "L1",
+ name = "Arbitrum",
+ symbol = "ARB",
+ trend = PriceChangeType.UP,
+ priceText = "$1.12",
+ trendText = "+8.1%",
+ marketCap = "$3.1 B",
+ rating = "10",
+ ),
+ marketToken(
+ rawId = "L2",
+ name = "Optimism",
+ symbol = "OP",
+ trend = PriceChangeType.DOWN,
+ priceText = "$2.34",
+ trendText = "−3.2%",
+ marketCap = "$2.8 B",
+ rating = "11",
+ ),
+ marketToken(
+ rawId = "L3",
+ name = "Base",
+ symbol = "—",
+ trend = PriceChangeType.NEUTRAL,
+ priceText = "$0.98",
+ trendText = "0.0%",
+ marketCap = "$1.9 B",
+ rating = "12",
+ chart = null,
+ ),
+ marketToken(
+ rawId = "L4",
+ name = "Avalanche",
+ symbol = "AVAX",
+ trend = PriceChangeType.UP,
+ priceText = "$36.5",
+ trendText = "+4.4%",
+ marketCap = "$14 B",
+ rating = "13",
+ staking = true,
+ ),
+ marketToken(
+ rawId = "L5",
+ name = "Polkadot",
+ symbol = "DOT",
+ trend = PriceChangeType.DOWN,
+ priceText = "$6.12",
+ trendText = "−2.1%",
+ marketCap = "$8 B",
+ rating = "14",
+ ),
+ marketToken(
+ rawId = "L6",
+ name = "Cosmos",
+ symbol = "ATOM",
+ trend = PriceChangeType.UP,
+ priceText = "$8.90",
+ trendText = "+1.1%",
+ marketCap = "$3.4 B",
+ rating = "15",
+ ),
+ marketToken(
+ rawId = "L7",
+ name = "Near",
+ symbol = "NEAR",
+ trend = PriceChangeType.DOWN,
+ priceText = "$4.56",
+ trendText = "−0.8%",
+ marketCap = "$4.5 B",
+ rating = "16",
+ ),
+ marketToken(
+ rawId = "L8",
+ name = "Sui",
+ symbol = "SUI",
+ trend = PriceChangeType.UP,
+ priceText = "$2.10",
+ trendText = "+12.3%",
+ marketCap = "$2.2 B",
+ rating = "17",
+ ),
+ ).toImmutableList()
+
+ fun allScenarios(): List = listOf(
+ scenarioInitialEmpty,
+ scenarioHistoryEmptyBoth,
+ scenarioHistoryHintsOnly,
+ scenarioHistoryRecentsOnly,
+ scenarioHistoryFull,
+ scenarioResultsMarketEmptyNoPortfolio,
+ scenarioResultsMarketEmptyWithPortfolio,
+ scenarioResultsLoadingNoPortfolio,
+ scenarioResultsLoadingWithPortfolio,
+ scenarioResultsNotFoundNoPortfolio,
+ scenarioResultsNotFoundWithPortfolio,
+ scenarioResultsContentMarketOnly,
+ scenarioResultsContentPortfolioAndMarket,
+ scenarioResultsContentWithUnder100kBanner,
+ scenarioResultsLongMarketScroll,
+ )
+}
+
+private val SearchContentPreviewCallbacks = SearchCallbacks(
+ onLoadMore = {},
+ onClearHintsClick = {},
+ onTextHintClick = { _ -> },
+ onResultMarketTokenClick = {},
+)
+
+/** All [SearchContentPreviewScenario] values for the Preview Parameter dropdown in Android Studio. */
+internal class SearchContentPreviewParameterProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = SearchContentPreviewFixtures.allScenarios().asSequence()
+}
+
+@Composable
+private fun SearchContentPreviewHost(
+ scenario: SearchContentPreviewScenario,
+ modifier: Modifier = Modifier,
+ previewHeightDp: Int = 720,
+) {
+ Box(
+ modifier = modifier
+ .height(previewHeightDp.dp)
+ .fillMaxWidth()
+ .background(TangemTheme.colors.background.primary),
+ ) {
+ SearchContent(
+ content = scenario.content,
+ searchCallbacks = SearchContentPreviewCallbacks,
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+}
+
+// region Named previews (quick access without cycling parameters)
+
+@Composable
+@Preview(name = "01 Initial empty", showBackground = true, widthDp = 360, heightDp = 720)
+@Preview(
+ name = "01 Initial empty (night)",
+ showBackground = true,
+ widthDp = 360,
+ heightDp = 720,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun SearchContentPreview_InitialEmpty() {
+ TangemThemePreviewRedesign {
+ SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioInitialEmpty)
+ }
+}
+
+@Composable
+@Preview(name = "02 History full", showBackground = true, widthDp = 360, heightDp = 720)
+@Preview(
+ name = "02 History full (night)",
+ showBackground = true,
+ widthDp = 360,
+ heightDp = 720,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun SearchContentPreview_HistoryFull() {
+ TangemThemePreviewRedesign {
+ SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioHistoryFull)
+ }
+}
+
+@Composable
+@Preview(name = "03 Results loading + portfolio", showBackground = true, widthDp = 360, heightDp = 720)
+@Preview(
+ name = "03 Results loading + portfolio (night)",
+ showBackground = true,
+ widthDp = 360,
+ heightDp = 720,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun SearchContentPreview_ResultsLoadingWithPortfolio() {
+ TangemThemePreviewRedesign {
+ SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioResultsLoadingWithPortfolio)
+ }
+}
+
+@Composable
+@Preview(name = "04 Results not found", showBackground = true, widthDp = 360, heightDp = 720)
+@Preview(
+ name = "04 Results not found (night)",
+ showBackground = true,
+ widthDp = 360,
+ heightDp = 720,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun SearchContentPreview_ResultsNotFound() {
+ TangemThemePreviewRedesign {
+ SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioResultsNotFoundNoPortfolio)
+ }
+}
+
+@Composable
+@Preview(name = "05 Results market + under 100k", showBackground = true, widthDp = 360, heightDp = 800)
+@Preview(
+ name = "05 Results market + under 100k (night)",
+ showBackground = true,
+ widthDp = 360,
+ heightDp = 800,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun SearchContentPreview_ResultsWithUnder100kBanner() {
+ TangemThemePreviewRedesign {
+ SearchContentPreviewHost(
+ scenario = SearchContentPreviewFixtures.scenarioResultsContentWithUnder100kBanner,
+ previewHeightDp = 800,
+ )
+ }
+}
+
+@Composable
+@Preview(name = "06 Results long scroll", showBackground = true, widthDp = 360, heightDp = 640)
+@Preview(
+ name = "06 Results long scroll (night)",
+ showBackground = true,
+ widthDp = 360,
+ heightDp = 640,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun SearchContentPreview_ResultsLongList() {
+ TangemThemePreviewRedesign {
+ SearchContentPreviewHost(
+ scenario = SearchContentPreviewFixtures.scenarioResultsLongMarketScroll,
+ previewHeightDp = 640,
+ )
+ }
+}
+
+// endregion
+
+/** All scenarios via Preview Parameter; scenario label is the [SearchContentPreviewScenario] title property. */
+@Composable
+@Preview(name = "All scenarios (parameter)", showBackground = true, widthDp = 360, heightDp = 720)
+@Preview(
+ name = "All scenarios (parameter, night)",
+ showBackground = true,
+ widthDp = 360,
+ heightDp = 720,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun SearchContentPreview_AllScenarios(
+ @PreviewParameter(SearchContentPreviewParameterProvider::class) scenario: SearchContentPreviewScenario,
+) {
+ TangemThemePreviewRedesign {
+ SearchContentPreviewHost(
+ scenario = scenario,
+ previewHeightDp = when (scenario.title) {
+ SearchContentPreviewFixtures.scenarioResultsLongMarketScroll.title -> 640
+ SearchContentPreviewFixtures.scenarioResultsContentWithUnder100kBanner.title -> 800
+ else -> 720
+ },
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt
new file mode 100644
index 0000000000..e793512ce1
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt
@@ -0,0 +1,8 @@
+package com.tangem.features.feed.ui.search.state
+
+internal data class SearchCallbacks(
+ val onLoadMore: () -> Unit,
+ val onClearHintsClick: () -> Unit,
+ val onTextHintClick: (hint: String) -> Unit,
+ val onResultMarketTokenClick: () -> Unit,
+)
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt
new file mode 100644
index 0000000000..e4628ad30c
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt
@@ -0,0 +1,52 @@
+package com.tangem.features.feed.ui.search.state
+
+import androidx.compose.runtime.Immutable
+import com.tangem.common.ui.markets.models.MarketsListItemUM
+import com.tangem.core.ui.components.fields.entity.SearchBarUM
+import kotlinx.collections.immutable.ImmutableList
+
+data class SearchUM(
+ val searchBar: SearchBarUM,
+ val content: SearchContentUM,
+)
+
+@Immutable
+sealed interface SearchContentUM {
+
+ data class History(
+ val textHints: ImmutableList,
+ val recentTokens: ImmutableList,
+ ) : SearchContentUM
+
+ data class Results(
+ val userAssets: ImmutableList,
+ val marketTokens: MarketSearchResultUM,
+ ) : SearchContentUM
+
+ data object InitialEmpty : SearchContentUM
+}
+
+@Immutable
+sealed interface MarketSearchResultUM {
+
+ data class Content(
+ val items: ImmutableList,
+ val shouldShowUnder100kNotification: Boolean = false,
+ val onShowUnder100kClick: () -> Unit = {},
+ ) : MarketSearchResultUM
+
+ data object Loading : MarketSearchResultUM
+ data object NotFound : MarketSearchResultUM
+ data object Empty : MarketSearchResultUM
+}
+
+data class TextHintItemUM(val text: String)
+
+data class UserAssetItemUM(
+ val id: String,
+ val tokenIconUrl: String?,
+ val tokenName: String,
+ val tokenSymbol: String,
+ val accountName: String,
+ val onClick: () -> Unit,
+)
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt
index 638e28ca11..02de097c9a 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt
@@ -1,71 +1,70 @@
package com.tangem.features.feed.ui.utils
+import androidx.compose.animation.*
import androidx.compose.animation.core.tween
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.unit.dp
-import com.arkivanov.decompose.FaultyDecomposeApi
-import com.arkivanov.decompose.extensions.compose.stack.animation.*
+import androidx.compose.runtime.State
+import com.arkivanov.decompose.Child
+import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
-import com.tangem.core.ui.utils.toPx
import com.tangem.features.feed.components.FeedEntryChildFactory
-private const val DELAY_FOR_TRANSITION = 400
+private fun FeedEntryChildFactory.Child?.usesFadeStackTransition(): Boolean = when (this) {
+ is FeedEntryChildFactory.Child.Search -> true
+ is FeedEntryChildFactory.Child.TokenList -> params.shouldAlwaysShowSearchBar
+ else -> false
+}
-@OptIn(FaultyDecomposeApi::class)
-internal fun topBarFeedEntryStackAnimation(): StackAnimation<
- FeedEntryChildFactory.Child,
- ComposableModularBottomSheetContentComponent,
- > =
- stackAnimation { to, from, _ ->
- val isSearchToTokenList =
- (to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
- val isFromSearchTokenList =
- (from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
- if (isSearchToTokenList || isFromSearchTokenList) {
- fade()
- } else {
- slide()
- }
+internal typealias FeedEntryActiveChild =
+ Child.Created
+
+internal typealias FeedEntryChildStack =
+ ChildStack
+
+internal fun topBarFeedEntryAnimatedContentTransitionSpec(
+ stackState: State,
+): AnimatedContentTransitionScope.() -> ContentTransform =
+ { feedEntryAnimatedContentTransform(stackState.value) }
+
+internal fun contentFeedEntryAnimatedContentTransitionSpec(
+ stackState: State,
+): AnimatedContentTransitionScope.() -> ContentTransform =
+ { feedEntryAnimatedContentTransform(stackState.value) }
+
+private fun AnimatedContentTransitionScope.feedEntryAnimatedContentTransform(
+ stack: FeedEntryChildStack,
+): ContentTransform {
+ val shouldUseFade = initialState.configuration.usesFadeStackTransition() ||
+ targetState.configuration.usesFadeStackTransition()
+ return if (shouldUseFade) {
+ fadeIn(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS)) togetherWith
+ fadeOut(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS))
+ } else {
+ feedEntrySlideTransform(stack)
}
+}
-@OptIn(FaultyDecomposeApi::class)
-internal fun contentFeedEntryStackAnimation(): StackAnimation<
- FeedEntryChildFactory.Child,
- ComposableModularBottomSheetContentComponent,
- > =
- stackAnimation { to, from, _ ->
- val isSearchToTokenList =
- (to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
- val isFromSearchTokenList =
- (from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
- if (isSearchToTokenList || isFromSearchTokenList) {
- fadeAndSlideInt()
- } else {
- slide()
- }
- }
-
-private fun fadeAndSlideInt(): StackAnimator =
- stackAnimator(animationSpec = tween(DELAY_FOR_TRANSITION)) { factor, _, content ->
- val alpha = 1f - kotlin.math.abs(factor)
- val slidePx = 32.dp.toPx()
- val yOffset = when {
- factor > 0 -> (slidePx * factor).toInt()
- factor < 0 -> (slidePx * factor * -1).toInt()
- else -> 0
- }
- content(
- Modifier
- .alpha(alpha)
- .offsetY(yOffset),
+private fun AnimatedContentTransitionScope.feedEntrySlideTransform(
+ stack: FeedEntryChildStack,
+): ContentTransform {
+ val isPushing = stack.backStack.lastOrNull()?.configuration == initialState.configuration
+ return if (isPushing) {
+ slideInHorizontally(
+ animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS),
+ initialOffsetX = { it },
+ ) togetherWith slideOutHorizontally(
+ animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS),
+ targetOffsetX = { -it },
+ )
+ } else {
+ slideInHorizontally(
+ animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS),
+ initialOffsetX = { -it },
+ ) togetherWith slideOutHorizontally(
+ animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS),
+ targetOffsetX = { it },
)
}
+}
-private fun Modifier.offsetY(pixels: Int): Modifier = layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- placeable.placeRelative(x = 0, y = pixels)
- }
-}
\ No newline at end of file
+private const val FEED_ENTRY_FADE_DURATION_MS = 300
+private const val FEED_ENTRY_SLIDE_DURATION_MS = 300
\ No newline at end of file