Updated on 2026-08-14
This commit is contained in:
parent
509d1ffd3c
commit
df39526354
11 changed files with 241 additions and 20 deletions
|
|
@ -3,12 +3,14 @@ package com.tangem.tap.common.images
|
|||
import android.content.Context
|
||||
import android.util.Log
|
||||
import coil.ImageLoader
|
||||
import coil.memory.MemoryCache
|
||||
import coil.util.Logger
|
||||
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import timber.log.Timber
|
||||
|
||||
private const val COIL_LOG_TAG = "COIL"
|
||||
private const val COIL_MEMORY_CACHE_SIZE = 0.25
|
||||
|
||||
fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageLoader {
|
||||
return ImageLoader.Builder(context)
|
||||
|
|
@ -22,6 +24,11 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
|
|||
.build()
|
||||
}
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder(context)
|
||||
.maxSizePercent(COIL_MEMORY_CACHE_SIZE)
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.common.images
|
||||
|
||||
import android.content.Context
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.coil.ImagePreloader
|
||||
|
||||
internal class DefaultImagePreloader(
|
||||
private val appContext: Context,
|
||||
) : ImagePreloader {
|
||||
override fun preload(url: String) {
|
||||
appContext.imageLoader.enqueue(
|
||||
ImageRequest.Builder(appContext)
|
||||
.data(url)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.tap.di.core
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.core.ui.coil.ImagePreloader
|
||||
import com.tangem.tap.common.images.DefaultImagePreloader
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object ImageLoaderModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideImageLoader(@ApplicationContext context: Context): ImagePreloader {
|
||||
return DefaultImagePreloader(context)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import com.tangem.core.analytics.models.event.TechAnalyticsEvent
|
|||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.coil.ImagePreloader
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.BottomSheetMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
|
|
@ -20,6 +21,8 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
|
|||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.promo.GetStoryContentUseCase
|
||||
import com.tangem.domain.promo.models.StoryContentIds
|
||||
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
|
||||
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
|
||||
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
|
||||
|
|
@ -50,6 +53,8 @@ internal class MainViewModel @Inject constructor(
|
|||
@GlobalUiMessageSender private val messageSender: UiMessageSender,
|
||||
private val keyboardValidator: KeyboardValidator,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getStoryContentUseCase: GetStoryContentUseCase,
|
||||
private val imagePreloader: ImagePreloader,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -80,6 +85,8 @@ internal class MainViewModel @Inject constructor(
|
|||
deleteDeprecatedLogsUseCase()
|
||||
|
||||
sendKeyboardIdentifierEvent()
|
||||
|
||||
preloadImages()
|
||||
}
|
||||
|
||||
/** Loading the resources needed to run the application */
|
||||
|
|
@ -278,4 +285,20 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preload stories to display on startup before navigation to targeted screen
|
||||
private fun preloadImages() {
|
||||
try {
|
||||
viewModelScope.launch {
|
||||
val swapStories = getStoryContentUseCase(StoryContentIds.STORY_FIRST_TIME_SWAP.id).getOrNull()
|
||||
?: return@launch
|
||||
val storiesImages = swapStories.getImageUrls()
|
||||
|
||||
// try to preload images for stories
|
||||
storiesImages.forEach(imagePreloader::preload)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.core.ui.coil
|
||||
|
||||
/**
|
||||
* Preload image and store in runtime memory
|
||||
*/
|
||||
interface ImagePreloader {
|
||||
fun preload(url: String)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ object TangemColorPalette {
|
|||
// region Base
|
||||
val Black = Color(0xFF000000)
|
||||
val White = Color(0xFFFFFFFF)
|
||||
val SoftWhite = Color(0xFFF1F1F1)
|
||||
// endregion Base
|
||||
|
||||
// region Dark
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.components.stories.model.StoriesContentConfig
|
||||
import com.tangem.core.ui.components.stories.model.StoryConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class SwapStoriesContentConfig(
|
||||
|
|
@ -13,7 +13,9 @@ data class SwapStoriesContentConfig(
|
|||
}
|
||||
|
||||
data class SwapStoryConfig(
|
||||
@DrawableRes val imageRes: Int,
|
||||
val imageUrl: String,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
) : StoryConfig {
|
||||
override val duration: Int = 2000
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.tangem.core.ui.format.bigdecimal.format
|
|||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.promo.models.StoryContent
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
|
||||
|
|
@ -125,19 +126,40 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
fun createStoriesState(uiStateHolder: SwapStateHolder): SwapStateHolder {
|
||||
// WARNING! Be careful with indices. Temporary solution.
|
||||
// Use all data from v1/stories api (image url, title, subtitle)
|
||||
@Suppress("MagicNumber")
|
||||
fun createStoriesState(uiStateHolder: SwapStateHolder, swapStory: StoryContent): SwapStateHolder {
|
||||
val storyOrderedImageUrls = swapStory.getImageUrls()
|
||||
if (storyOrderedImageUrls.size != 5) return uiStateHolder
|
||||
|
||||
return uiStateHolder.copy(
|
||||
// todo stories [REDACTED_TASK_KEY] fix when design is ready
|
||||
storiesConfig = SwapStoriesContentConfig(
|
||||
stories = persistentListOf(
|
||||
SwapStoryConfig(
|
||||
imageRes = R.drawable.img_card_with_ring,
|
||||
imageUrl = storyOrderedImageUrls[0],
|
||||
title = resourceReference(R.string.swap_story_first_title),
|
||||
subtitle = resourceReference(R.string.swap_story_first_subtitle),
|
||||
),
|
||||
SwapStoryConfig(
|
||||
imageRes = R.drawable.ill_businessman_3d,
|
||||
imageUrl = storyOrderedImageUrls[1],
|
||||
title = resourceReference(R.string.swap_story_second_title),
|
||||
subtitle = resourceReference(R.string.swap_story_second_subtitle),
|
||||
),
|
||||
SwapStoryConfig(
|
||||
imageRes = R.drawable.ill_one_inch_powered,
|
||||
imageUrl = storyOrderedImageUrls[2],
|
||||
title = resourceReference(R.string.swap_story_third_title),
|
||||
subtitle = resourceReference(R.string.swap_story_third_subtitle),
|
||||
),
|
||||
SwapStoryConfig(
|
||||
imageUrl = storyOrderedImageUrls[3],
|
||||
title = resourceReference(R.string.swap_story_forth_title),
|
||||
subtitle = resourceReference(R.string.swap_story_forth_subtitle),
|
||||
),
|
||||
SwapStoryConfig(
|
||||
imageUrl = storyOrderedImageUrls[4],
|
||||
title = resourceReference(R.string.swap_story_fifth_title),
|
||||
subtitle = resourceReference(R.string.swap_story_fifth_subtitle),
|
||||
),
|
||||
),
|
||||
onClose = actions.onStoriesClose,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,124 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.components.stories.StoriesContainer
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.feature.swap.models.SwapStoriesContentConfig
|
||||
import com.tangem.feature.swap.models.SwapStoryConfig
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private val SubtitleColor = Color(0xFF868692)
|
||||
private const val STORIES_RELATIVE_PADDING = 0.7
|
||||
|
||||
@Composable
|
||||
internal fun SwapStoriesScreen(config: SwapStoriesContentConfig) {
|
||||
BackHandler(onBack = config.onClose)
|
||||
SystemBarsIconsDisposable(darkIcons = false)
|
||||
|
||||
StoriesContainer(
|
||||
config = config,
|
||||
) { current, _ ->
|
||||
// todo stories [REDACTED_TASK_KEY] fix when design is ready
|
||||
Image(
|
||||
painter = painterResource(current.imageRes),
|
||||
contentDescription = null,
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemColorPalette.Black),
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(current.imageUrl)
|
||||
.crossfade(enable = false)
|
||||
.allowHardware(true)
|
||||
.build(),
|
||||
loading = { },
|
||||
error = { },
|
||||
contentDescription = null,
|
||||
)
|
||||
val height = LocalWindowSize.current.height.value
|
||||
val textAlign = (height.dp.value * STORIES_RELATIVE_PADDING).dp
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = textAlign,
|
||||
start = 56.dp,
|
||||
end = 56.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = current.title.resolveReference(),
|
||||
style = TangemTheme.typography.head,
|
||||
color = TangemColorPalette.SoftWhite,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpacerH16()
|
||||
Text(
|
||||
text = current.subtitle.resolveReference(),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
|
||||
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
|
||||
),
|
||||
color = SubtitleColor,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SwapStoriesScreen_Preview() {
|
||||
TangemThemePreview {
|
||||
SwapStoriesScreen(
|
||||
SwapStoriesContentConfig(
|
||||
stories = persistentListOf(
|
||||
SwapStoryConfig(
|
||||
imageUrl = "https://devweb.tangem.com/images/stories/swap/image1.png",
|
||||
title = stringReference("Exchange With Us"),
|
||||
subtitle = stringReference(
|
||||
"Trusted exchange providers let you swap assets effortlessly",
|
||||
),
|
||||
),
|
||||
),
|
||||
onClose = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -27,7 +27,9 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.promo.GetStoryContentUseCase
|
||||
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
|
||||
import com.tangem.domain.promo.models.StoryContentIds
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -89,6 +91,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
|
||||
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
|
||||
private val getStoryContentUseCase: GetStoryContentUseCase,
|
||||
private val featureToggles: SwapFeatureToggles,
|
||||
swapInteractorFactory: SwapInteractor.Factory,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
|
|
@ -168,16 +171,18 @@ internal class SwapViewModel @Inject constructor(
|
|||
get() = swapRouter.currentScreen
|
||||
|
||||
init {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull()
|
||||
val toStatus = initialCurrencyTo?.let { getCryptoCurrencyStatusUseCase(userWalletId, it.id).getOrNull() }
|
||||
val wallet = getUserWalletUseCase(userWalletId).getOrNull()
|
||||
viewModelScope.launch {
|
||||
val isShowPromoStories = shouldShowSwapStoriesUseCase.invokeSync() && featureToggles.isPromoStoriesEnabled
|
||||
|
||||
if (isShowPromoStories) {
|
||||
initStories()
|
||||
swapRouter.openScreen(SwapNavScreen.PromoStories)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull()
|
||||
val toStatus = initialCurrencyTo?.let { getCryptoCurrencyStatusUseCase(userWalletId, it.id).getOrNull() }
|
||||
val wallet = getUserWalletUseCase(userWalletId).getOrNull()
|
||||
|
||||
if (fromStatus == null || wallet == null) {
|
||||
uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back)
|
||||
|
|
@ -283,7 +288,16 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun initStories() {
|
||||
uiState = stateBuilder.createStoriesState(uiState)
|
||||
viewModelScope.launch {
|
||||
getStoryContentUseCase(StoryContentIds.STORY_FIRST_TIME_SWAP.id).fold(
|
||||
ifLeft = {
|
||||
Timber.e("Unable to load stories for ${StoryContentIds.STORY_FIRST_TIME_SWAP.id}")
|
||||
},
|
||||
ifRight = {
|
||||
uiState = stateBuilder.createStoriesState(uiState, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyInitialTokenChoice(
|
||||
|
|
@ -1131,6 +1145,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
swapRouter.openScreen(SwapNavScreen.Success)
|
||||
},
|
||||
onStoriesClose = {
|
||||
viewModelScope.launch { shouldShowSwapStoriesUseCase.neverToShow() }
|
||||
swapRouter.openScreen(SwapNavScreen.Main)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue