From b95730613d785aea5be477d4bc6159e76e4e7218 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Jul 2026 18:23:24 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../choosetoken/model/ChooseTokenModel.kt | 12 +++++++++ .../choosetoken/model/MarketBlockDelegate.kt | 12 ++++++--- .../model/PortfolioListBlockDelegate.kt | 15 +++++++++++ .../impl/choosetoken/ui/ChooseTokenScreen.kt | 27 ++++++++++++++----- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/ChooseTokenModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/ChooseTokenModel.kt index 214422ed0a..4b0097ba4b 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/ChooseTokenModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/ChooseTokenModel.kt @@ -20,7 +20,9 @@ import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMark import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenFullUM import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenInitialUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @Suppress("LongParameterList") @@ -77,6 +79,13 @@ internal class ChooseTokenModel @Inject constructor( ) init { + if (bridge.settings.isShowMarketBlock) { + modelScope.launch { + delay(MARKETS_INITIAL_LOAD_DELAY) + marketBlockDelegate.loadDefaultMarkets() + } + } + addToPortfolioManager.onDismiss.receiveAsFlow() .onEach { marketBlockDelegate.addToPortfolioSlot.dismiss() } .launchIn(modelScope) @@ -128,5 +137,8 @@ internal class ChooseTokenModel @Inject constructor( companion object { const val DEBOUNCE_SEARCH_DELAY = 500L + + /** Roughly the bottom sheet entrance animation duration. */ + private const val MARKETS_INITIAL_LOAD_DELAY = 400L } } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt index b5e472f1af..667760f723 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt @@ -126,9 +126,6 @@ internal class MarketBlockDelegate @AssistedInject constructor( } .launchIn(modelScope) - // Initial load of default markets - defaultMarketsListManager.reload() - visibleMarketItemIds .mapNotNull { rawIDS -> if (rawIDS.isNotEmpty()) { @@ -157,6 +154,15 @@ internal class MarketBlockDelegate @AssistedInject constructor( .launchIn(modelScope) } + /** + * Starts the initial load of the default markets list. Not invoked in [init] on purpose: + * the caller decides *if* (market block may be disabled entirely, e.g. Transfer flow) and + * *when* (deferred past the bottom sheet entrance animation, [REDACTED_TASK_KEY]) to trigger it. + */ + fun loadDefaultMarkets() { + defaultMarketsListManager.reload() + } + private fun createDefaultMarketsFlow(): Flow { val marketsTitle = TextReference.Res(R.string.markets_pulse_common_title) return combine( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioListBlockDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioListBlockDelegate.kt index 515cabd4d1..e2c66d5371 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioListBlockDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioListBlockDelegate.kt @@ -19,12 +19,15 @@ import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult import com.tangem.features.commonfeatures.api.choosetoken.model.TokenListUMData import com.tangem.features.commonfeatures.impl.choosetoken.SettingContextUseCase import com.tangem.features.commonfeatures.impl.choosetoken.converter.ChooseTokenListItemConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.mapNotNullValues import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @Suppress("LongParameterList") @@ -33,6 +36,7 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor( private val settingContext: SettingContextUseCase, private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, private val getWalletsUseCase: GetWalletsUseCase, + dispatchers: CoroutineDispatcherProvider, @Assisted private val modelScope: CoroutineScope, @Assisted private val searchQueryState: StateFlow, @Assisted private val featureSettings: ChooseTokenBridge.Settings, @@ -48,6 +52,8 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor( val portfolioList: SharedFlow> = buildDataFlow() .distinctUntilChanged() + .throttleLatest(windowMs = UM_UPDATES_THROTTLE_MS) + .flowOn(dispatchers.default) .shareIn(modelScope, SharingStarted.Eagerly, replay = 1) private fun buildDataFlow(): Flow> = channelFlow { @@ -153,4 +159,13 @@ internal interface ClickIntents { fun onAccountExpandClick(account: Account) fun onAccountCollapseClick(account: Account) +} + +private const val UM_UPDATES_THROTTLE_MS = 100L + +/** Emits the first value immediately, then at most one (latest) value per [windowMs]. */ +@OptIn(ExperimentalCoroutinesApi::class) +private fun Flow.throttleLatest(windowMs: Long): Flow = conflate().transformLatest { value -> + emit(value) + delay(windowMs) } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt index 65166caad5..3072f97c1e 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt @@ -3,6 +3,7 @@ package com.tangem.features.commonfeatures.impl.choosetoken.ui import androidx.compose.animation.AnimatedContent import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateIntAsState @@ -160,7 +161,7 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) { val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() val lazyListState = rememberLazyListState() - TangemSharedTransitionLayout(modifier) { + Box(modifier) { LazyColumn( modifier = Modifier .fillMaxSize() @@ -228,10 +229,13 @@ private fun SetupMarketScrollTracker(marketsState: SwapMarketState, lazyListStat @Composable private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) { - val visibleItems by remember { + val keyToId = remember(marketState.items) { + marketState.items.associateBy({ it.getComposeKey() }, { it.id }) + } + val visibleItems by remember(keyToId) { derivedStateOf { lazyListState.layoutInfo.visibleItemsInfo.mapNotNull { itemInfo -> - marketState.items.find { it.getComposeKey() == itemInfo.key }?.id + (itemInfo.key as? String)?.let(keyToId::get) } } } @@ -476,7 +480,7 @@ private fun AccountRow( modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { - ProvideSharedTransitionScope(Modifier.weight(1f)) { + AccountRowSharedTransitionLayout(Modifier.weight(1f)) { val iconSharedContentState = rememberSharedContentState(key = "account-icon-${portfolio.id}") val titleSharedContentState = rememberSharedContentState(key = "account-title-${portfolio.id}") val boundsTransform = BoundsTransform { _, _ -> tween(ACCOUNT_BOUNDS_ANIM_MS) } @@ -528,9 +532,7 @@ private fun AccountRow( ) val startStyle = TangemTheme.typography2.captionSemibold12 val stopStyle = TangemTheme.typography2.bodySemibold16 - val textStyle by remember(animationFraction.value) { - derivedStateOf { lerp(startStyle, stopStyle, animationFraction.value) } - } + val textStyle = lerp(startStyle, stopStyle, animationFraction.value) val resizedTitle = when (val titleUM = tokenRowUM.titleUM) { is TangemTokenRowUM.TitleUM.Content -> titleUM.copy( text = styledStringReference( @@ -579,6 +581,17 @@ private fun AccountRow( } } +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +private fun AccountRowSharedTransitionLayout( + modifier: Modifier = Modifier, + content: @Composable SharedTransitionScope.() -> Unit, +) { + TangemSharedTransitionLayout(modifier) { + ProvideSharedTransitionScope(content = content) + } +} + @Composable private fun AccountTail(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { Box(