Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-13 08:54:47 +02:00
parent 530b2d93ad
commit 822b82d1da
20 changed files with 211 additions and 123 deletions

View file

@ -87,6 +87,7 @@ internal class NewsListModel @Inject constructor(
init {
observeNewsList()
batchFlowManager.reload()
loadCategories()
}

View file

@ -2,14 +2,20 @@ package com.tangem.features.feed.model.search
import androidx.compose.runtime.Stable
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
import com.tangem.features.feed.components.search.SearchTokenSelectorComponent
import com.tangem.features.feed.model.search.state.TokenSelectorStateController
import com.tangem.features.feed.model.search.state.transformers.BuildTokenSelectorSectionsTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ -18,6 +24,9 @@ internal class SearchTokenSelectorModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val stateController: TokenSelectorStateController,
private val userWalletsListRepository: UserWalletsListRepository,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val walletIconUMConverter: WalletIconUMConverter,
) : Model() {
private val params = paramsContainer.require<SearchTokenSelectorComponent.Params>()
@ -26,13 +35,25 @@ internal class SearchTokenSelectorModel @Inject constructor(
get() = stateController.uiState
init {
stateController.update(
BuildTokenSelectorSectionsTransformer(
entries = params.entries,
appCurrency = params.appCurrency,
isBalanceHidden = params.isBalanceHidden,
onTokenSelected = params.onTokenSelected,
),
)
modelScope.launch(dispatchers.default) {
val requiredWalletIds = params.entries.map { it.userWalletId }.toSet()
val walletIcons = userWalletsListRepository.userWallets
.filterNotNull()
.first()
.filter { it.walletId in requiredWalletIds }
.associate { wallet ->
wallet.walletId to walletIconUMConverter.convert(getWalletIconUseCase(wallet))
}
stateController.update(
BuildTokenSelectorSectionsTransformer(
entries = params.entries,
appCurrency = params.appCurrency,
isBalanceHidden = params.isBalanceHidden,
walletIcons = walletIcons,
onTokenSelected = params.onTokenSelected,
),
)
}
}
}

View file

@ -4,14 +4,17 @@ import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.markets.tokenselector.AccountHeaderData
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.common.ui.markets.tokenselector.TokenSelectorSectionUM
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.collections.immutable.toImmutableList
internal class BuildTokenSelectorSectionsTransformer(
private val entries: List<UserAssetEntry>,
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val walletIcons: Map<UserWalletId, DeviceIconUM>,
private val onTokenSelected: (UserAssetEntry) -> Unit,
) : TokenSelectorUMTransformer {
@ -30,11 +33,12 @@ internal class BuildTokenSelectorSectionsTransformer(
val byWallet = entries.groupBy { it.userWalletId }
val shouldShowWalletHeaders = byWallet.size > 1
for ((_, walletEntries) in byWallet) {
for ((walletId, walletEntries) in byWallet) {
if (shouldShowWalletHeaders) {
sections.add(
TokenSelectorSectionUM.WalletHeader(
walletName = walletEntries.first().userWalletName,
deviceIcon = walletIcons[walletId] ?: DeviceIconUM.Stub(cardsCount = 1),
),
)
}

View file

@ -20,7 +20,6 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.topFade
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
@ -139,13 +138,18 @@ private fun EntryContentV2(
isOpenedInBottomSheet: Boolean,
) {
val background = LocalMainBottomSheetColor.current.value
var topBarHeight by remember { mutableStateOf(0.dp) }
val hazeState = rememberHazeState()
val fadeHeightOverride = remember { mutableStateOf<Dp?>(null) }
val effectiveFadeHeight = fadeHeightOverride.value ?: topBarHeight
val statusBarInset = if (isOpenedInBottomSheet) {
0.dp
} else {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
}
val effectiveTopBarHeight = topBarHeight + statusBarInset
val effectiveFadeHeight = fadeHeightOverride.value ?: effectiveTopBarHeight
Surface(contentColor = background) {
Surface(color = background, contentColor = background) {
CompositionLocalProvider(
LocalHazeState provides hazeState,
LocalContentTopFadeHeightOverride provides fadeHeightOverride,
@ -154,9 +158,8 @@ private fun EntryContentV2(
ContentBlock(
bottomSheetState = bottomSheetState,
effectiveFadeHeight = effectiveFadeHeight,
isOpenedInBottomSheet = isOpenedInBottomSheet,
stackState = stackState,
topBarHeight = topBarHeight,
topBarHeight = effectiveTopBarHeight,
)
TitleBlock(
bottomSheetState = bottomSheetState,
@ -188,14 +191,10 @@ private fun BoxScope.TitleBlock(
Box(
modifier = modifier
.align(Alignment.TopStart)
.then(
if (!isOpenedInBottomSheet) {
Modifier.statusBarsPadding()
} else {
Modifier
},
)
.onGloballyPositioned { coordinates ->
.then(if (!isOpenedInBottomSheet) Modifier.statusBarsPadding() else Modifier),
) {
Box(
modifier = Modifier.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
val height = coordinates.size.height.toDp()
@ -203,19 +202,20 @@ private fun BoxScope.TitleBlock(
}
}
},
) {
AnimatedContent(
targetState = stackState.value.active,
transitionSpec = animationAppBar,
contentKey = { it.key },
label = "FeedEntryAppBar",
) { state ->
state.instance.Title(bottomSheetState)
) {
AnimatedContent(
targetState = stackState.value.active,
transitionSpec = animationAppBar,
contentKey = { it.key },
label = "FeedEntryAppBar",
) { state ->
state.instance.Title(bottomSheetState)
}
CollapsedTitleClickOverlay(
bottomSheetState = bottomSheetState,
onExpandSheet = onExpandSheet,
)
}
CollapsedTitleClickOverlay(
bottomSheetState = bottomSheetState,
onExpandSheet = onExpandSheet,
)
}
}
@ -223,7 +223,6 @@ private fun BoxScope.TitleBlock(
private fun BoxScope.ContentBlock(
bottomSheetState: State<BottomSheetState>,
effectiveFadeHeight: Dp,
isOpenedInBottomSheet: Boolean,
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
topBarHeight: Dp,
modifier: Modifier = Modifier,
@ -238,26 +237,13 @@ private fun BoxScope.ContentBlock(
child.instance.Content(
modifier = Modifier
.fillMaxSize()
.conditionalCompose(
condition = !isOpenedInBottomSheet,
modifier = {
padding(top = topBarHeight)
},
)
.hazeSourceTangem(zIndex = 0f, state = LocalHazeState.current)
.conditionalCompose(
condition = isOpenedInBottomSheet,
modifier = {
topFade(
height = effectiveFadeHeight,
color = TangemTheme.colors2.surface.level2.copy(BASE_FADE_LEVEL),
solidStop = .6f,
)
},
.topFade(
height = effectiveFadeHeight,
color = TangemTheme.colors2.surface.level2.copy(BASE_FADE_LEVEL),
solidStop = .6f,
),
contentPadding = PaddingValues(
top = if (isOpenedInBottomSheet) topBarHeight else TangemTheme.dimens2.x2_5,
),
contentPadding = PaddingValues(top = topBarHeight),
bottomSheetState = bottomSheetState,
)
}

View file

@ -127,13 +127,12 @@ internal fun NewsListContentV2(contentPadding: PaddingValues, state: NewsListUM)
)
TopFade(
modifier = Modifier.padding(top = contentPadding.calculateTopPadding()),
colorStops = arrayOf(
0f to fadeColor,
FIRST_STEP to fadeColor.copy(FIRST_STEP_FADE_LEVEL),
1f to Color.Transparent,
),
height = 20.dp + chipsHeight,
height = topPadding + TangemTheme.dimens2.x5 + chipsHeight,
)
LazyRow(

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.common.ui.markets.tokenselector.TokenSelectorSectionUM
import com.tangem.core.ui.ds.image.DeviceIconUM
import io.mockk.*
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.BeforeEach
@ -215,7 +216,10 @@ class BuildTokenSelectorSectionsTransformerTest {
val prevStateWithSections = TokenSelectorContentUM(
sections = persistentListOf(
TokenSelectorSectionUM.WalletHeader(walletName = "Old Wallet"),
TokenSelectorSectionUM.WalletHeader(
walletName = "Old Wallet",
deviceIcon = DeviceIconUM.Stub(cardsCount = 1),
),
),
)
@ -236,6 +240,7 @@ class BuildTokenSelectorSectionsTransformerTest {
entries = entries,
appCurrency = appCurrency,
isBalanceHidden = false,
walletIcons = emptyMap(),
onTokenSelected = onTokenSelected,
)
}