diff --git a/app/src/main/java/io/novafoundation/nova/app/di/app/navigation/SettingsNavigationModule.kt b/app/src/main/java/io/novafoundation/nova/app/di/app/navigation/SettingsNavigationModule.kt index 4c2e3b2da5..6abdb481f5 100644 --- a/app/src/main/java/io/novafoundation/nova/app/di/app/navigation/SettingsNavigationModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/di/app/navigation/SettingsNavigationModule.kt @@ -5,6 +5,7 @@ import dagger.Provides import io.novafoundation.nova.app.root.navigation.NavigationHolder import io.novafoundation.nova.app.root.navigation.Navigator import io.novafoundation.nova.app.root.navigation.settings.SettingsNavigator +import io.novafoundation.nova.app.root.presentation.RootRouter import io.novafoundation.nova.common.di.scope.ApplicationScope import io.novafoundation.nova.feature_settings_impl.SettingsRouter import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter @@ -15,8 +16,9 @@ class SettingsNavigationModule { @ApplicationScope @Provides fun provideRouter( + rootRouter: RootRouter, navigationHolder: NavigationHolder, walletConnectRouter: WalletConnectRouter, navigator: Navigator, - ): SettingsRouter = SettingsNavigator(navigationHolder, walletConnectRouter, navigator) + ): SettingsRouter = SettingsNavigator(navigationHolder, rootRouter, walletConnectRouter, navigator) } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt index c08822e2a5..b31933209d 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt @@ -52,14 +52,18 @@ import io.novafoundation.nova.feature_account_impl.presentation.startCreateWalle import io.novafoundation.nova.feature_account_impl.presentation.watchOnly.change.ChangeWatchAccountFragment import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.detail.BalanceDetailFragment +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowFragment +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload import io.novafoundation.nova.feature_assets.presentation.model.OperationParcelizeModel import io.novafoundation.nova.feature_assets.presentation.receive.ReceiveFragment import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft import io.novafoundation.nova.feature_assets.presentation.send.amount.SelectSendFragment import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_assets.presentation.send.confirm.ConfirmSendFragment -import io.novafoundation.nova.feature_assets.presentation.swap.AssetSwapFlowFragment -import io.novafoundation.nova.feature_assets.presentation.swap.SwapFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.asset.AssetSwapFlowFragment +import io.novafoundation.nova.feature_assets.presentation.swap.asset.SwapFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowFragment +import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowPayload import io.novafoundation.nova.feature_assets.presentation.tokens.add.enterInfo.AddTokenEnterInfoFragment import io.novafoundation.nova.feature_assets.presentation.tokens.add.enterInfo.AddTokenEnterInfoPayload import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.ManageChainTokensFragment @@ -323,10 +327,6 @@ class Navigator( navController?.navigate(R.id.action_open_receive, ReceiveFragment.getBundle(assetPayload)) } - override fun openAssetFilters() { - navController?.navigate(R.id.action_mainFragment_to_assetFiltersFragment) - } - override fun openAssetSearch() { navController?.navigate(R.id.action_mainFragment_to_assetSearchFragment) } @@ -387,6 +387,26 @@ class Navigator( navController?.navigate(R.id.action_close_send_flow) } + override fun openSendNetworks(payload: NetworkFlowPayload) { + navController?.navigate(R.id.action_sendFlow_to_sendFlowNetwork, NetworkFlowFragment.createPayload(payload)) + } + + override fun openReceiveNetworks(payload: NetworkFlowPayload) { + navController?.navigate(R.id.action_receiveFlow_to_receiveFlowNetwork, NetworkFlowFragment.createPayload(payload)) + } + + override fun openSwapNetworks(payload: NetworkSwapFlowPayload) { + navController?.navigate(R.id.action_selectAssetSwapFlowFragment_to_swapFlowNetworkFragment, NetworkSwapFlowFragment.createPayload(payload)) + } + + override fun openBuyNetworks(payload: NetworkFlowPayload) { + navController?.navigate(R.id.action_buyFlow_to_buyFlowNetwork, NetworkFlowFragment.createPayload(payload)) + } + + override fun returnToMainSwapScreen() { + navController?.navigate(R.id.action_return_to_swap_settings) + } + override fun openSwapFlow() { val payload = SwapFlowPayload.InitialSelecting navController?.navigate(R.id.action_mainFragment_to_swapFlow, AssetSwapFlowFragment.getBundle(payload)) diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/settings/SettingsNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/settings/SettingsNavigator.kt index 8291139690..92a5ac4d00 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/settings/SettingsNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/settings/SettingsNavigator.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.app.R import io.novafoundation.nova.app.root.navigation.BaseNavigator import io.novafoundation.nova.app.root.navigation.NavigationHolder import io.novafoundation.nova.app.root.navigation.Navigator +import io.novafoundation.nova.app.root.presentation.RootRouter import io.novafoundation.nova.feature_account_impl.presentation.pincode.PinCodeAction import io.novafoundation.nova.feature_account_impl.presentation.pincode.PincodeFragment import io.novafoundation.nova.feature_settings_impl.SettingsRouter @@ -19,11 +20,16 @@ import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions. class SettingsNavigator( navigationHolder: NavigationHolder, + private val rootRouter: RootRouter, private val walletConnectDelegate: WalletConnectRouter, private val delegate: Navigator ) : BaseNavigator(navigationHolder), SettingsRouter { + override fun returnToWallet() { + rootRouter.returnToWallet() + } + override fun openWallets() { delegate.openWallets() } @@ -80,6 +86,8 @@ class SettingsNavigator( override fun openLanguages() = performNavigation(R.id.action_mainFragment_to_languagesFragment) + override fun openAppearance() = performNavigation(R.id.action_mainFragment_to_appearanceFragment) + override fun openChangePinCode() = performNavigation( actionId = R.id.action_change_pin_code, args = PincodeFragment.getPinCodeBundle(PinCodeAction.Change) diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt index 99cbbb0e24..ba72cd2dae 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt @@ -6,8 +6,8 @@ import io.novafoundation.nova.app.root.navigation.NavigationHolder import io.novafoundation.nova.app.root.navigation.Navigator import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_assets.presentation.balance.detail.BalanceDetailFragment -import io.novafoundation.nova.feature_assets.presentation.swap.AssetSwapFlowFragment -import io.novafoundation.nova.feature_assets.presentation.swap.SwapFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.asset.AssetSwapFlowFragment +import io.novafoundation.nova.feature_assets.presentation.swap.asset.SwapFlowPayload import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.SwapConfirmationFragment import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload @@ -33,12 +33,12 @@ class SwapNavigator( override fun selectAssetIn(selectedAsset: AssetPayload?) { val payload = SwapFlowPayload.ReselectAssetIn(selectedAsset) - navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapFlow, AssetSwapFlowFragment.getBundle(payload)) + navigationHolder.navController?.navigate(R.id.action_swapSettingsFragment_to_select_swap_token_graph, AssetSwapFlowFragment.getBundle(payload)) } override fun selectAssetOut(selectedAsset: AssetPayload?) { val payload = SwapFlowPayload.ReselectAssetOut(selectedAsset) - navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapFlow, AssetSwapFlowFragment.getBundle(payload)) + navigationHolder.navController?.navigate(R.id.action_swapSettingsFragment_to_select_swap_token_graph, AssetSwapFlowFragment.getBundle(payload)) } override fun openSendCrossChain(destination: AssetPayload, recipientAddress: String?) { diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index 8efb8f4078..d085d337b2 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -217,7 +217,7 @@ + + - - + + - - + + android:name="io.novafoundation.nova.feature_assets.presentation.receive.flow.asset.AssetReceiveFlowFragment" + android:label="AssetReceiveFlowFragment"> + + + + + android:name="io.novafoundation.nova.feature_assets.presentation.buy.flow.asset.AssetBuyFlowFragment" + android:label="AssetBuyFlowFragment"> + + + + + android:id="@+id/sendFlowNetworkFragment" + android:name="io.novafoundation.nova.feature_assets.presentation.send.flow.network.NetworkSendFlowFragment" + android:label="NetworkSendFlowFragment" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/select_swap_token_nav_graph.xml b/app/src/main/res/navigation/select_swap_token_nav_graph.xml new file mode 100644 index 0000000000..45123fda55 --- /dev/null +++ b/app/src/main/res/navigation/select_swap_token_nav_graph.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/start_swap_nav_graph.xml b/app/src/main/res/navigation/start_swap_nav_graph.xml index 4f0ebf6382..9d9fdb5c4a 100644 --- a/app/src/main/res/navigation/start_swap_nav_graph.xml +++ b/app/src/main/res/navigation/start_swap_nav_graph.xml @@ -2,7 +2,7 @@ - - + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 82a2016493..927170fa5d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { // App version - versionName = '8.7.3' - versionCode = 157 + versionName = '9.0.0' + versionCode = 158 applicationId = "io.novafoundation.nova" releaseApplicationSuffix = "market" @@ -176,7 +176,6 @@ buildscript { jUnitDep = "junit:junit:$junitVersion" mockitoDep = "org.mockito:mockito-inline:$mockitoVersion" robolectricDep = "org.robolectric:robolectric:$robolectricVersion" - robolectricMultidexDep = "org.robolectric:shadows-multidex:$robolectricVersion" archCoreTestDep = "androidx.arch.core:core-testing:$architectureComponentVersion" progressButtonDep = "com.github.razir.progressbutton:progressbutton:$progressButtonsVersion" diff --git a/caip/src/main/java/io/novafoundation/nova/caip/slip44/Slip44CoinRepository.kt b/caip/src/main/java/io/novafoundation/nova/caip/slip44/Slip44CoinRepository.kt index ab89fb3db7..f53b2a864e 100644 --- a/caip/src/main/java/io/novafoundation/nova/caip/slip44/Slip44CoinRepository.kt +++ b/caip/src/main/java/io/novafoundation/nova/caip/slip44/Slip44CoinRepository.kt @@ -2,7 +2,7 @@ package io.novafoundation.nova.caip.slip44 import io.novafoundation.nova.caip.slip44.endpoint.Slip44CoinApi import io.novafoundation.nova.caip.slip44.endpoint.Slip44CoinRemote -import io.novafoundation.nova.runtime.ext.unifiedSymbol +import io.novafoundation.nova.runtime.ext.normalizeSymbol import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -26,7 +26,7 @@ internal class RealSlip44CoinRepository( .associateBy { it.symbol } } - return slip44Coins[chainAsset.unifiedSymbol()] + return slip44Coins[chainAsset.normalizeSymbol()] ?.index ?.toIntOrNull() } diff --git a/common/build.gradle b/common/build.gradle index 0a119a8950..fa5059f0cb 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -42,6 +42,9 @@ android { buildConfigField "String", "LEDGER_BLEUTOOTH_GUIDE", "\"https://support.ledger.com/hc/en-us/articles/360019138694-Set-up-Bluetooth-connection\"" buildConfigField "String", "APP_UPDATE_SOURCE_LINK", "\"https://play.google.com/store/apps/details?id=io.novafoundation.nova.market\"" + + buildConfigField "String", "ASSET_COLORED_ICON_URL", "\"https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/tokens/colored\"" + buildConfigField "String", "ASSET_WHITE_ICON_URL", "\"https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/tokens/white/v1\"" } buildTypes { diff --git a/common/src/main/java/io/novafoundation/nova/common/data/model/AssetIconMode.kt b/common/src/main/java/io/novafoundation/nova/common/data/model/AssetIconMode.kt new file mode 100644 index 0000000000..76fda50d5f --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/data/model/AssetIconMode.kt @@ -0,0 +1,5 @@ +package io.novafoundation.nova.common.data.model + +enum class AssetIconMode { + COLORED, WHITE +} diff --git a/common/src/main/java/io/novafoundation/nova/common/data/model/AssetViewMode.kt b/common/src/main/java/io/novafoundation/nova/common/data/model/AssetViewMode.kt new file mode 100644 index 0000000000..04a0870730 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/data/model/AssetViewMode.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.common.data.model + +enum class AssetViewMode { + TOKENS, NETWORKS +} + +fun AssetViewMode.switch(): AssetViewMode { + return when (this) { + AssetViewMode.TOKENS -> AssetViewMode.NETWORKS + AssetViewMode.NETWORKS -> AssetViewMode.TOKENS + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsIconModeRepository.kt b/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsIconModeRepository.kt new file mode 100644 index 0000000000..1ff99d29f8 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsIconModeRepository.kt @@ -0,0 +1,52 @@ +package io.novafoundation.nova.common.data.repository + +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.common.data.storage.Preferences +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface AssetsIconModeRepository { + fun assetsIconModeFlow(): Flow + + fun setAssetsIconMode(assetsViewMode: AssetIconMode) + + fun getIconMode(): AssetIconMode +} + +private const val PREFS_ASSETS_ICON_MODE = "PREFS_ASSETS_ICON_MODE" +private val ASSET_ICON_MODE_DEFAULT = AssetIconMode.COLORED + +class RealAssetsIconModeRepository( + private val preferences: Preferences +) : AssetsIconModeRepository { + + override fun assetsIconModeFlow(): Flow { + return preferences.stringFlow(PREFS_ASSETS_ICON_MODE) + .map { + it?.fromPrefsValue() ?: ASSET_ICON_MODE_DEFAULT + } + } + + override fun setAssetsIconMode(assetsViewMode: AssetIconMode) { + preferences.putString(PREFS_ASSETS_ICON_MODE, assetsViewMode.toPrefsValue()) + } + + override fun getIconMode(): AssetIconMode { + return preferences.getString(PREFS_ASSETS_ICON_MODE)?.fromPrefsValue() ?: ASSET_ICON_MODE_DEFAULT + } + + private fun AssetIconMode.toPrefsValue(): String { + return when (this) { + AssetIconMode.COLORED -> "colored" + AssetIconMode.WHITE -> "white" + } + } + + private fun String.fromPrefsValue(): AssetIconMode? { + return when (this) { + "colored" -> AssetIconMode.COLORED + "white" -> AssetIconMode.WHITE + else -> null + } + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsViewModeRepository.kt b/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsViewModeRepository.kt new file mode 100644 index 0000000000..072c9cbcf1 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsViewModeRepository.kt @@ -0,0 +1,55 @@ +package io.novafoundation.nova.common.data.repository + +import io.novafoundation.nova.common.data.model.AssetViewMode +import io.novafoundation.nova.common.data.storage.Preferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +interface AssetsViewModeRepository { + + fun getAssetViewMode(): AssetViewMode + + fun assetsViewModeFlow(): Flow + + suspend fun setAssetsViewMode(assetsViewMode: AssetViewMode) +} + +private const val PREFS_ASSETS_VIEW_MODE = "PREFS_ASSETS_VIEW_MODE" +private val ASSET_VIEW_MODE_DEFAULT = AssetViewMode.TOKENS + +class RealAssetsViewModeRepository( + private val preferences: Preferences +) : AssetsViewModeRepository { + + override fun getAssetViewMode(): AssetViewMode { + return preferences.getString(PREFS_ASSETS_VIEW_MODE)?.fromPrefsValue() ?: ASSET_VIEW_MODE_DEFAULT + } + + override fun assetsViewModeFlow(): Flow { + return preferences.stringFlow(PREFS_ASSETS_VIEW_MODE) + .map { + it?.fromPrefsValue() ?: ASSET_VIEW_MODE_DEFAULT + } + } + + override suspend fun setAssetsViewMode(assetsViewMode: AssetViewMode) = withContext(Dispatchers.IO) { + preferences.putString(PREFS_ASSETS_VIEW_MODE, assetsViewMode.toPrefsValue()) + } + + private fun AssetViewMode.toPrefsValue(): String { + return when (this) { + AssetViewMode.NETWORKS -> "networks" + AssetViewMode.TOKENS -> "tokens" + } + } + + private fun String.fromPrefsValue(): AssetViewMode? { + return when (this) { + "networks" -> AssetViewMode.NETWORKS + "tokens" -> AssetViewMode.TOKENS + else -> null + } + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/di/CommonApi.kt b/common/src/main/java/io/novafoundation/nova/common/di/CommonApi.kt index 9c52fe3e0e..01b93537de 100644 --- a/common/src/main/java/io/novafoundation/nova/common/di/CommonApi.kt +++ b/common/src/main/java/io/novafoundation/nova/common/di/CommonApi.kt @@ -14,12 +14,15 @@ import io.novafoundation.nova.common.data.network.HttpExceptionHandler import io.novafoundation.nova.common.data.network.NetworkApiCreator import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser import io.novafoundation.nova.common.data.network.rpc.SocketSingleRequestExecutor +import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository import io.novafoundation.nova.common.data.secrets.v1.SecretStoreV1 import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences import io.novafoundation.nova.common.di.modules.Caching +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor import io.novafoundation.nova.common.interfaces.ActivityIntentProvider import io.novafoundation.nova.common.interfaces.BuildTypeProvider import io.novafoundation.nova.common.interfaces.FileCache @@ -29,6 +32,7 @@ import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer import io.novafoundation.nova.common.mixin.api.NetworkStateMixin import io.novafoundation.nova.common.mixin.condition.ConditionMixinFactory import io.novafoundation.nova.common.mixin.hints.ResourcesHintsMixinFactory +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.AppVersionProvider import io.novafoundation.nova.common.resources.ClipboardManager import io.novafoundation.nova.common.resources.ContextManager @@ -63,6 +67,36 @@ import java.util.Random interface CommonApi { + val systemCallExecutor: SystemCallExecutor + + val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory + + val resourcesHintsMixinFactory: ResourcesHintsMixinFactory + + val okHttpClient: OkHttpClient + + val fileCache: FileCache + + val permissionsAskerFactory: PermissionsAskerFactory + + val bluetoothManager: BluetoothManager + + val locationManager: LocationManager + + val listChooserMixinFactory: ListChooserMixin.Factory + + val partialRetriableMixinFactory: PartialRetriableMixin.Factory + + val automaticInteractionGate: AutomaticInteractionGate + + val bannerVisibilityRepository: BannerVisibilityRepository + + val provideActivityIntentProvider: ActivityIntentProvider + + val googleApiAvailabilityProvider: GoogleApiAvailabilityProvider + + val coinGeckoLinkParser: CoinGeckoLinkParser + fun computationalCache(): ComputationalCache fun imageLoader(): ImageLoader @@ -154,33 +188,11 @@ interface CommonApi { fun buildTypeProvider(): BuildTypeProvider - val systemCallExecutor: SystemCallExecutor - - val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory - - val resourcesHintsMixinFactory: ResourcesHintsMixinFactory - - val okHttpClient: OkHttpClient - - val fileCache: FileCache - - val permissionsAskerFactory: PermissionsAskerFactory - - val bluetoothManager: BluetoothManager - - val locationManager: LocationManager - - val listChooserMixinFactory: ListChooserMixin.Factory - - val partialRetriableMixinFactory: PartialRetriableMixin.Factory + fun assetsViewModeRepository(): AssetsViewModeRepository - val automaticInteractionGate: AutomaticInteractionGate - - val bannerVisibilityRepository: BannerVisibilityRepository + fun assetsIconModeService(): AssetsIconModeRepository - val provideActivityIntentProvider: ActivityIntentProvider + fun assetIconProvider(): AssetIconProvider - val googleApiAvailabilityProvider: GoogleApiAvailabilityProvider - - val coinGeckoLinkParser: CoinGeckoLinkParser + fun assetViewModeInteractor(): AssetViewModeInteractor } diff --git a/common/src/main/java/io/novafoundation/nova/common/di/modules/CommonModule.kt b/common/src/main/java/io/novafoundation/nova/common/di/modules/CommonModule.kt index 8de4f7e85f..bf958cbee2 100644 --- a/common/src/main/java/io/novafoundation/nova/common/di/modules/CommonModule.kt +++ b/common/src/main/java/io/novafoundation/nova/common/di/modules/CommonModule.kt @@ -9,6 +9,7 @@ import coil.ImageLoader import coil.decode.SvgDecoder import dagger.Module import dagger.Provides +import io.novafoundation.nova.common.BuildConfig import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.address.CachingAddressIconGenerator import io.novafoundation.nova.common.address.StatelessAddressIconGenerator @@ -19,7 +20,11 @@ import io.novafoundation.nova.common.data.RealGoogleApiAvailabilityProvider import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.data.memory.RealComputationalCache import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser +import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository +import io.novafoundation.nova.common.data.repository.RealAssetsIconModeRepository +import io.novafoundation.nova.common.data.repository.RealAssetsViewModeRepository import io.novafoundation.nova.common.data.repository.RealBannerVisibilityRepository import io.novafoundation.nova.common.data.secrets.v1.SecretStoreV1 import io.novafoundation.nova.common.data.secrets.v1.SecretStoreV1Impl @@ -30,6 +35,8 @@ import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferencesImpl import io.novafoundation.nova.common.data.storage.encrypt.EncryptionUtil import io.novafoundation.nova.common.di.scope.ApplicationScope +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.domain.interactor.RealAssetViewModeInteractor import io.novafoundation.nova.common.interfaces.FileCache import io.novafoundation.nova.common.interfaces.FileProvider import io.novafoundation.nova.common.interfaces.InternalFileSystemCache @@ -40,6 +47,8 @@ import io.novafoundation.nova.common.mixin.condition.ConditionMixinFactory import io.novafoundation.nova.common.mixin.condition.RealConditionMixinFactory import io.novafoundation.nova.common.mixin.hints.ResourcesHintsMixinFactory import io.novafoundation.nova.common.mixin.impl.CustomDialogProvider +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.presentation.RealAssetIconProvider import io.novafoundation.nova.common.resources.AppVersionProvider import io.novafoundation.nova.common.resources.ClipboardManager import io.novafoundation.nova.common.resources.ContextManager @@ -347,4 +356,28 @@ class CommonModule { fun provideCoinGeckoLinkParser(): CoinGeckoLinkParser { return CoinGeckoLinkParser() } + + @Provides + @ApplicationScope + fun provideAssetsViewModeRepository(preferences: Preferences): AssetsViewModeRepository = RealAssetsViewModeRepository(preferences) + + @Provides + @ApplicationScope + fun provideAssetViewModeInteractor(repository: AssetsViewModeRepository): AssetViewModeInteractor { + return RealAssetViewModeInteractor(repository) + } + + @Provides + @ApplicationScope + fun provideAssetsIconModeRepository(preferences: Preferences): AssetsIconModeRepository = RealAssetsIconModeRepository(preferences) + + @Provides + @ApplicationScope + fun provideAssetIconProvider(repository: AssetsIconModeRepository): AssetIconProvider { + return RealAssetIconProvider( + repository, + BuildConfig.ASSET_COLORED_ICON_URL, + BuildConfig.ASSET_WHITE_ICON_URL, + ) + } } diff --git a/common/src/main/java/io/novafoundation/nova/common/domain/interactor/AssetViewModeInteractor.kt b/common/src/main/java/io/novafoundation/nova/common/domain/interactor/AssetViewModeInteractor.kt new file mode 100644 index 0000000000..f9a7d15fe3 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/domain/interactor/AssetViewModeInteractor.kt @@ -0,0 +1,30 @@ +package io.novafoundation.nova.common.domain.interactor + +import io.novafoundation.nova.common.data.model.AssetViewMode +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository +import kotlinx.coroutines.flow.Flow + +interface AssetViewModeInteractor { + + fun getAssetViewMode(): AssetViewMode + + fun assetsViewModeFlow(): Flow + + suspend fun setAssetsViewMode(assetsViewMode: AssetViewMode) +} + +class RealAssetViewModeInteractor( + private val assetsViewModeRepository: AssetsViewModeRepository +) : AssetViewModeInteractor { + override fun getAssetViewMode(): AssetViewMode { + return assetsViewModeRepository.getAssetViewMode() + } + + override fun assetsViewModeFlow(): Flow { + return assetsViewModeRepository.assetsViewModeFlow() + } + + override suspend fun setAssetsViewMode(assetsViewMode: AssetViewMode) { + assetsViewModeRepository.setAssetsViewMode(assetsViewMode) + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt b/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt new file mode 100644 index 0000000000..b764abf57e --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt @@ -0,0 +1,54 @@ +package io.novafoundation.nova.common.presentation + +import io.novafoundation.nova.common.R +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.common.utils.images.asIcon + +interface AssetIconProvider { + + companion object; + + fun getAssetIconOrFallback(iconName: String): Icon + + fun getAssetIconOrFallback(iconName: String, iconMode: AssetIconMode): Icon +} + +class RealAssetIconProvider( + private val assetsIconModeRepository: AssetsIconModeRepository, + private val coloredBaseUrl: String, + private val whiteBaseUrl: String +) : AssetIconProvider { + + override fun getAssetIconOrFallback(iconName: String): Icon { + return getAssetIconOrFallback(iconName, assetsIconModeRepository.getIconMode()) + } + + override fun getAssetIconOrFallback(iconName: String, iconMode: AssetIconMode): Icon { + val iconUrl = when (iconMode) { + AssetIconMode.COLORED -> "$coloredBaseUrl/$iconName" + AssetIconMode.WHITE -> "$whiteBaseUrl/$iconName" + } + + return iconUrl.asIcon() + } +} + +val AssetIconProvider.Companion.fallbackIcon: Icon + get() = R.drawable.ic_nova.asIcon() + +fun AssetIconProvider.getAssetIconOrFallback( + iconName: String?, + fallback: Icon = AssetIconProvider.fallbackIcon +): Icon { + return iconName?.let { getAssetIconOrFallback(it) } ?: fallback +} + +fun AssetIconProvider.getAssetIconOrFallback( + iconName: String?, + iconMode: AssetIconMode, + fallback: Icon = AssetIconProvider.fallbackIcon +): Icon { + return iconName?.let { getAssetIconOrFallback(it, iconMode) } ?: fallback +} diff --git a/common/src/main/java/io/novafoundation/nova/common/presentation/ColoredText.kt b/common/src/main/java/io/novafoundation/nova/common/presentation/ColoredText.kt index 2fe200f4a3..89dc088417 100644 --- a/common/src/main/java/io/novafoundation/nova/common/presentation/ColoredText.kt +++ b/common/src/main/java/io/novafoundation/nova/common/presentation/ColoredText.kt @@ -6,7 +6,7 @@ import io.novafoundation.nova.common.utils.letOrHide import io.novafoundation.nova.common.utils.setTextColorRes data class ColoredText( - val text: String, + val text: CharSequence, @ColorRes val colorRes: Int, ) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/ContextExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/ContextExt.kt index c203835eb0..89068ac9be 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/ContextExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/ContextExt.kt @@ -16,6 +16,7 @@ import androidx.annotation.StyleRes import androidx.core.content.ContextCompat import io.novafoundation.nova.common.R import io.novafoundation.nova.common.view.shape.addRipple +import io.novafoundation.nova.common.view.shape.getMaskedRipple import io.novafoundation.nova.common.view.shape.getRippleMask import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable import kotlin.math.roundToInt @@ -108,6 +109,8 @@ interface WithContextExtensions { val Float.dpF: Float get() = dpF(providedContext) + fun getRippleDrawable(cornerSizeInDp: Int) = providedContext.getMaskedRipple(cornerSizeInDp) + fun addRipple(to: Drawable, mask: Drawable? = getRippleMask()) = providedContext.addRipple(to, mask) fun Drawable.withRippleMask(mask: Drawable = getRippleMask()) = addRipple(this, mask) @@ -145,3 +148,7 @@ fun Drawable.withRippleMask(mask: Drawable = getRippleMask()) = context.addRippl context(View) val Int.dp: Int get() = dp(this@View.context) + +context(View) +val Int.dpF: Float + get() = dpF(this@View.context) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt index 21560bebc6..06686e8ced 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt @@ -56,6 +56,10 @@ inline fun Flow>.filterList(crossinline handler: suspend (T) -> Bool list.filter { item -> handler(item) } } +inline fun Flow>.filterSet(crossinline handler: suspend (T) -> Boolean) = map { set -> + set.filter { item -> handler(item) }.toSet() +} + inline fun Flow>.mapList(crossinline mapper: suspend (T) -> R) = map { list -> list.map { item -> mapper(item) } } @@ -141,6 +145,8 @@ inline fun withFlowScope(crossinline block: suspend (scope: CoroutineScope) fun combineToPair(flow1: Flow, flow2: Flow): Flow> = combine(flow1, flow2, ::Pair) +fun combineToTriple(flow1: Flow, flow2: Flow, flow3: Flow): Flow> = combine(flow1, flow2, flow3, ::Triple) + /** * Modifies flow so that it firstly emits [LoadingState.Loading] state for each element from upstream. * Then, it constructs new source via [sourceSupplier] and emits all of its items wrapped into [LoadingState.Loaded] state diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index 4717260d45..c6e87dfe0e 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -23,6 +23,7 @@ import java.util.Collections import java.util.Date import java.util.UUID import java.util.concurrent.TimeUnit +import kotlinx.coroutines.launch import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -597,3 +598,7 @@ fun Calendar.resetDay() { set(Calendar.SECOND, 0) set(Calendar.MILLISECOND, 0) } + +inline fun CoroutineScope.launchUnit(crossinline block: suspend CoroutineScope.() -> Unit) { + launch { block() } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/PayloadCreator.kt b/common/src/main/java/io/novafoundation/nova/common/utils/PayloadCreator.kt new file mode 100644 index 0000000000..7b941fd5e3 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/PayloadCreator.kt @@ -0,0 +1,25 @@ +package io.novafoundation.nova.common.utils + +import android.os.Bundle +import android.os.Parcelable +import io.novafoundation.nova.common.base.BaseFragment + +const val KEY_PAYLOAD = "KEY_PAYLOAD" + +interface PayloadCreator { + + fun createPayload(payload: T): Bundle +} + +class FragmentPayloadCreator : PayloadCreator { + + override fun createPayload(payload: T): Bundle { + return Bundle().apply { + putParcelable(KEY_PAYLOAD, payload) + } + } +} + +fun BaseFragment<*>.payload(): T { + return requireArguments().getParcelable(KEY_PAYLOAD)!! +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/QrCodeGenerator.kt b/common/src/main/java/io/novafoundation/nova/common/utils/QrCodeGenerator.kt index 69c08a9d6b..31fbfd5e08 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/QrCodeGenerator.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/QrCodeGenerator.kt @@ -4,6 +4,7 @@ import android.graphics.Bitmap import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.google.zxing.qrcode.encoder.Encoder +import com.google.zxing.qrcode.encoder.QRCode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -22,10 +23,14 @@ class QrCodeGenerator( const val MAX_PAYLOAD_LENGTH = 512 } + fun generateQrCode(input: String): QRCode { + val hints = HashMap() + return Encoder.encode(input, ErrorCorrectionLevel.H, hints) + } + suspend fun generateQrBitmap(input: String): Bitmap { return withContext(Dispatchers.Default) { - val hints = HashMap() - val qrCode = Encoder.encode(input, ErrorCorrectionLevel.H, hints) + val qrCode = generateQrCode(input) val byteMatrix = qrCode.matrix val width = byteMatrix.width + PADDING_SIZE val height = byteMatrix.height + PADDING_SIZE diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/SpannableDSL.kt b/common/src/main/java/io/novafoundation/nova/common/utils/SpannableDSL.kt index e358d3a78c..2636f883f5 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/SpannableDSL.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/SpannableDSL.kt @@ -48,7 +48,7 @@ class SpannableBuilder(private val resourceManager: ResourceManager) { private val builder = SpannableStringBuilder() - fun appendColored(text: String, @ColorRes color: Int) { + fun appendColored(text: CharSequence, @ColorRes color: Int) { val span = ForegroundColorSpan(resourceManager.getColor(color)) append(text, span) @@ -60,7 +60,7 @@ class SpannableBuilder(private val resourceManager: ResourceManager) { return appendColored(text, color) } - fun append(text: String) { + fun append(text: CharSequence) { builder.append(text) } @@ -75,7 +75,7 @@ class SpannableBuilder(private val resourceManager: ResourceManager) { fun build(): SpannableString = SpannableString(builder) - private fun append(text: String, span: Any): SpannableBuilder { + private fun append(text: CharSequence, span: Any): SpannableBuilder { builder.append(text, span, Spanned.SPAN_INCLUSIVE_INCLUSIVE) return this diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/TokenSymbol.kt b/common/src/main/java/io/novafoundation/nova/common/utils/TokenSymbol.kt index 24af37c2c6..624e0194bb 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/TokenSymbol.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/TokenSymbol.kt @@ -7,11 +7,17 @@ import java.math.RoundingMode @JvmInline value class TokenSymbol(val value: String) { + companion object; // extensions + override fun toString() = value } fun String.asTokenSymbol() = TokenSymbol(this) +fun BigDecimal.formatTokenAmount(roundingMode: RoundingMode = RoundingMode.FLOOR): String { + return format(roundingMode) +} + fun BigDecimal.formatTokenAmount(tokenSymbol: TokenSymbol, roundingMode: RoundingMode = RoundingMode.FLOOR): String { return format(roundingMode).withTokenSymbol(tokenSymbol) } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt index 64ca9c9624..76402edeac 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt @@ -264,6 +264,10 @@ fun RecyclerView.findFirstVisiblePosition(): Int { return (layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() } +fun RecyclerView.findLastVisiblePosition(): Int { + return (layoutManager as LinearLayoutManager).findLastVisibleItemPosition() +} + fun ScrollView.scrollOnFocusTo(vararg focusableTargets: View) { val listener = View.OnFocusChangeListener { view, hasFocus -> if (hasFocus) { diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/DynamicPrecisionFormatter.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/DynamicPrecisionFormatter.kt index b5f2554719..5cfb1a8c47 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/DynamicPrecisionFormatter.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/DynamicPrecisionFormatter.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.common.utils.formatting import java.lang.Integer.max import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import kotlin.math.min class DynamicPrecisionFormatter( @@ -10,6 +11,8 @@ class DynamicPrecisionFormatter( private val minPrecision: Int, ) : NumberFormatter { + private val patternCache = mutableMapOf() + override fun format(number: BigDecimal, roundingMode: RoundingMode): String { // scale() - total amount of digits after 0., // precision() - amount of non-zero digits in decimal part @@ -18,6 +21,11 @@ class DynamicPrecisionFormatter( val formattingPrecision = max(minScale, requiredPrecision) - return decimalFormatterFor(patternWith(formattingPrecision), roundingMode).format(number) + val formatter = patternCache.getOrPut(formattingPrecision) { decimalFormatterFor(patternWith(formattingPrecision)) } + if (formatter.roundingMode != roundingMode) { + formatter.roundingMode = roundingMode + } + + return formatter.format(number) } } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/FixedPrecisionFormatter.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/FixedPrecisionFormatter.kt index 361e7e2afc..c03638b908 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/FixedPrecisionFormatter.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/FixedPrecisionFormatter.kt @@ -5,8 +5,12 @@ import java.math.RoundingMode class FixedPrecisionFormatter(private val precision: Int) : NumberFormatter { + private val delegate = decimalFormatterFor(patternWith(precision)) + override fun format(number: BigDecimal, roundingMode: RoundingMode): String { - val delegate = decimalFormatterFor(patternWith(precision), roundingMode) + if (delegate.roundingMode != roundingMode) { + delegate.roundingMode = roundingMode + } return delegate.format(number) } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt index 8c8b907eef..1c58aa033f 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt @@ -169,7 +169,7 @@ fun formatDateISO_8601_NoMs(date: Date): String { return dateTimeFormatISO_8601_NoMs.format(date) } -fun decimalFormatterFor(pattern: String, roundingMode: RoundingMode): DecimalFormat { +fun decimalFormatterFor(pattern: String): DecimalFormat { return DecimalFormat(pattern).apply { val symbols = decimalFormatSymbols @@ -178,12 +178,11 @@ fun decimalFormatterFor(pattern: String, roundingMode: RoundingMode): DecimalFor decimalFormatSymbols = symbols - this.roundingMode = roundingMode decimalFormatSymbols = decimalFormatSymbols } } -fun String.toAmountWithFraction(): AmountWithFraction { +fun CharSequence.toAmountWithFraction(): AmountWithFraction { val amountAndFraction = this.split(DECIMAL_SEPARATOR) val amount = amountAndFraction[0] val fraction = amountAndFraction.getOrNull(1) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/images/Icon.kt b/common/src/main/java/io/novafoundation/nova/common/utils/images/Icon.kt index 54c0b3e420..1f78a20199 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/images/Icon.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/images/Icon.kt @@ -11,11 +11,11 @@ import io.novafoundation.nova.common.utils.makeVisible sealed class Icon { - class FromLink(val data: String) : Icon() + data class FromLink(val data: String) : Icon() - class FromDrawable(val data: Drawable) : Icon() + data class FromDrawable(val data: Drawable) : Icon() - class FromDrawableRes(@DrawableRes val res: Int) : Icon() + data class FromDrawableRes(@DrawableRes val res: Int) : Icon() } typealias ExtraImageRequestBuilding = ImageRequest.Builder.() -> Unit @@ -40,3 +40,9 @@ fun ImageView.setIconOrMakeGone(icon: Icon?, imageLoader: ImageLoader, builder: fun Drawable.asIcon() = Icon.FromDrawable(this) fun @receiver:DrawableRes Int.asIcon() = Icon.FromDrawableRes(this) fun String.asIcon() = Icon.FromLink(this) + +fun ImageLoader.Companion.formatIcon(icon: Icon): Any = when (icon) { + is Icon.FromDrawable -> icon.data + is Icon.FromDrawableRes -> icon.res + is Icon.FromLink -> icon.data +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAdapter.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAdapter.kt new file mode 100644 index 0000000000..2ea0f99561 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAdapter.kt @@ -0,0 +1,8 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable + +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableBaseItem + +interface ExpandableAdapter { + + fun getItems(): List +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAnimationExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAnimationExt.kt new file mode 100644 index 0000000000..a54d6a34cc --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAnimationExt.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable + +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimationItemState + +fun ExpandableAnimationItemState.flippedFraction(): Float { + return 1f - animationFraction +} + +fun Float.flippedFraction(): Float { + return 1f - this +} + +fun ExpandableAnimationItemState.expandingFraction(): Float { + return when (animationType) { + ExpandableAnimationItemState.Type.EXPANDING -> animationFraction + ExpandableAnimationItemState.Type.COLLAPSING -> flippedFraction() + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAnimationSettings.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAnimationSettings.kt new file mode 100644 index 0000000000..2d001e0117 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableAnimationSettings.kt @@ -0,0 +1,7 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable + +import android.view.animation.Interpolator + +class ExpandableAnimationSettings(val duration: Long, val interpolator: Interpolator) { + companion object; +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableItemAnimator.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableItemAnimator.kt new file mode 100644 index 0000000000..2c7a806f42 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableItemAnimator.kt @@ -0,0 +1,299 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.view.ViewPropertyAnimator +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.recyclerview.widget.SimpleItemAnimator +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimationItemState +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator + +/** + * Potential problems: + * - If in one time we will run add and move animation or remove and move animation - one of an animation will be cancelled + */ +abstract class ExpandableItemAnimator( + private val settings: ExpandableAnimationSettings, + private val expandableAnimator: ExpandableAnimator +) : SimpleItemAnimator() { + + private var preparedForAnimation = false + + private val addAnimations = mutableMapOf>() // Parent item to children + private val removeAnimations = mutableMapOf>() // Parent item to children + private val moveAnimations = mutableListOf() + + private val pendingAddAnimations = mutableSetOf() + private val pendingRemoveAnimations = mutableSetOf() + private val pendingMoveAnimations = mutableSetOf() + + init { + addDuration = settings.duration + removeDuration = settings.duration + moveDuration = settings.duration + + supportsChangeAnimations = false + } + + /** + * Use this method before adapter.submitList() to prepare items for animation. + * Item animations will be skipped otherwise + */ + fun prepareForAnimation() { + preparedForAnimation = true + } + + override fun animateAdd(holder: ViewHolder): Boolean { + val notPreparedForAnimation = !preparedForAnimation + val notExpandableChildItem = holder !is ExpandableChildViewHolder || holder.expandableItem == null + if (notPreparedForAnimation || notExpandableChildItem) { + dispatchAddFinished(holder) + return false + } + + val item = (holder as ExpandableChildViewHolder).expandableItem!! + + // Reset move state helps clear translationY when animation is being to be canceled + if (pendingMoveAnimations.contains(holder)) { + holder.itemView.animate().cancel() + + resetMoveState(holder) + } + + if (pendingRemoveAnimations.contains(holder)) { + holder.itemView.animate().cancel() + } else { + preAddImpl(holder) + } + + if (item.groupId !in addAnimations) addAnimations[item.groupId] = mutableListOf() + addAnimations[item.groupId]?.add(holder) + + expandableAnimator.prepareAnimationToState(item.groupId, ExpandableAnimationItemState.Type.EXPANDING) + + return true + } + + override fun animateRemove(holder: ViewHolder): Boolean { + val notPreparedForAnimation = !preparedForAnimation + val notExpandableChildItem = holder !is ExpandableChildViewHolder || holder.expandableItem == null + if (notPreparedForAnimation || notExpandableChildItem) { + dispatchRemoveFinished(holder) + return false + } + + val item = (holder as ExpandableChildViewHolder).expandableItem!! + + // Reset move state helps clear translationY when animation is being to be canceled + if (pendingMoveAnimations.contains(holder)) { + holder.itemView.animate().cancel() + + resetMoveState(holder) + } + + if (pendingAddAnimations.contains(holder)) { + holder.itemView.animate().cancel() + } else { + preRemoveImpl(holder) + } + + if (item.groupId !in removeAnimations) removeAnimations[item.groupId] = mutableListOf() + removeAnimations[item.groupId]?.add(holder) + + expandableAnimator.prepareAnimationToState(item.groupId, ExpandableAnimationItemState.Type.COLLAPSING) + + return true + } + + override fun animateMove(holder: ViewHolder, fromX: Int, fromY: Int, toX: Int, toY: Int): Boolean { + val notPreparedForAnimation = !preparedForAnimation + if (notPreparedForAnimation || holder !is ExpandableBaseViewHolder<*>) { + dispatchMoveFinished(holder) + return false + } + + // Reset add state helps clear alpha and scale when animation is being to be canceled + if (pendingAddAnimations.contains(holder)) { + holder.itemView.animate().cancel() + resetAddState(holder) + } + + // Reset remove state helps clear alpha and scale when animation is being to be canceled + if (pendingRemoveAnimations.contains(holder)) { + holder.itemView.animate().cancel() + } + + if (pendingMoveAnimations.contains(holder)) { + holder.itemView.animate().cancel() + } + + preMoveImpl(holder, fromY, toY) + moveAnimations.add(holder) + return true + } + + override fun animateChange(oldHolder: ViewHolder?, newHolder: ViewHolder?, fromLeft: Int, fromTop: Int, toLeft: Int, toTop: Int): Boolean { + if (oldHolder == newHolder) { + dispatchChangeFinished(newHolder, false) + } else { + dispatchChangeFinished(oldHolder, true) + dispatchChangeFinished(newHolder, false) + } + return false + } + + override fun runPendingAnimations() { + // Add animation + expand items + runExpandableAnimationFor(addAnimations, pendingAddAnimations) { animateAddImpl(it) } + + // Remove animation + collapse items + runExpandableAnimationFor(removeAnimations, pendingRemoveAnimations) { animateRemoveImpl(it) } + + // Move animation + val animatingViewHolders = moveAnimations.toList() + moveAnimations.clear() + + for (holder in animatingViewHolders) { + animateMoveImpl(holder) + } + + pendingMoveAnimations.addAll(animatingViewHolders) + + // Set prepare for animation = false to return to skipping animations + if (pendingAddAnimations.isNotEmpty() || pendingRemoveAnimations.isNotEmpty() || pendingMoveAnimations.isNotEmpty()) { + preparedForAnimation = false + } + } + + private fun runExpandableAnimationFor( + animationGroup: MutableMap>, + pendingAnimations: MutableSet, + runAnimation: (ViewHolder) -> Unit + ) { + val parentItemIds = animationGroup.keys.toList() + val animatingViewHolders = animationGroup.flatMap { (_, viewHolders) -> viewHolders } + animationGroup.clear() + + parentItemIds.forEach { expandableAnimator.runAnimationFor(it) } + for (holder in animatingViewHolders) { + runAnimation(holder) + } + + pendingAnimations.addAll(animatingViewHolders) + } + + abstract fun preAddImpl(holder: ViewHolder) + + abstract fun getAddAnimator(holder: ViewHolder): ViewPropertyAnimator + + abstract fun preRemoveImpl(holder: ViewHolder) + + abstract fun getRemoveAnimator(holder: ViewHolder): ViewPropertyAnimator + + abstract fun preMoveImpl(holder: ViewHolder, fromY: Int, toY: Int) + + abstract fun getMoveAnimator(holder: ViewHolder): ViewPropertyAnimator + + abstract fun resetAddState(holder: ViewHolder) + + abstract fun resetRemoveState(holder: ViewHolder) + + abstract fun resetMoveState(holder: ViewHolder) + + private fun animateAddImpl(holder: ViewHolder) { + val animation = getAddAnimator(holder) + animation.setInterpolator(settings.interpolator) + .setDuration(settings.duration) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + addFinished(holder) + animation.setListener(null) + } + }).start() + } + + private fun animateRemoveImpl(holder: ViewHolder) { + val animation = getRemoveAnimator(holder) + animation.setInterpolator(settings.interpolator) + .setDuration(settings.duration) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + resetRemoveState(holder) + removeFinished(holder) + animation.setListener(null) + } + }).start() + } + + private fun animateMoveImpl(holder: ViewHolder) { + val animation = getMoveAnimator(holder) + animation.setInterpolator(settings.interpolator) + .setDuration(settings.duration) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + moveFinished(holder) + animation.setListener(null) + } + }).start() + } + + override fun endAnimation(viewHolder: ViewHolder) { + viewHolder.itemView.animate().cancel() + } + + override fun endAnimations() { + pendingAddAnimations.iterator().forEach { it.itemView.animate().cancel() } + pendingAddAnimations.clear() + + pendingRemoveAnimations.iterator().forEach { it.itemView.animate().cancel() } + pendingRemoveAnimations.clear() + + pendingMoveAnimations.iterator().forEach { it.itemView.animate().cancel() } + pendingMoveAnimations.clear() + + addAnimations.clear() + removeAnimations.clear() + moveAnimations.clear() + + expandableAnimator.cancelAnimations() + } + + override fun isRunning(): Boolean { + return addAnimations.isNotEmpty() || + removeAnimations.isNotEmpty() || + moveAnimations.isNotEmpty() || + pendingAddAnimations.isNotEmpty() || + pendingRemoveAnimations.isNotEmpty() || + pendingMoveAnimations.isNotEmpty() + } + + private fun addFinished(holder: ViewHolder) { + pendingAddAnimations.remove(holder) + internalDispatchAnimationFinished(holder) + } + + private fun removeFinished(holder: ViewHolder) { + pendingRemoveAnimations.remove(holder) + internalDispatchAnimationFinished(holder) + } + + private fun moveFinished(holder: ViewHolder) { + pendingMoveAnimations.remove(holder) + internalDispatchAnimationFinished(holder) + } + + private fun internalDispatchAnimationFinished(holder: ViewHolder) { + if (holder in pendingAddAnimations) return + if (holder in pendingRemoveAnimations) return + if (holder in pendingMoveAnimations) return + + dispatchAnimationFinished(holder) + dispatchFinishedWhenDone() + } + + private fun dispatchFinishedWhenDone() { + if (!isRunning) { + dispatchAnimationsFinished() + } + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableItemDecoration.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableItemDecoration.kt new file mode 100644 index 0000000000..ec30ca0b92 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableItemDecoration.kt @@ -0,0 +1,98 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable + +import android.graphics.Canvas +import android.graphics.Rect +import android.view.View +import androidx.core.view.children +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import io.novafoundation.nova.common.utils.indexOfFirstOrNull +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimationItemState +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableBaseItem +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableChildItem +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableParentItem + +private data class ItemWithViewHolder(val position: Int, val item: ExpandableBaseItem, val viewHolder: ViewHolder?) + +abstract class ExpandableItemDecoration( + private val adapter: ExpandableAdapter, + private val animator: ExpandableAnimator +) : RecyclerView.ItemDecoration() { + + abstract fun onDrawGroup( + canvas: Canvas, + animationState: ExpandableAnimationItemState, + recyclerView: RecyclerView, + parentItem: ExpandableParentItem, + parent: ViewHolder?, + children: List + ) + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + } + + override fun onDraw(canvas: Canvas, recyclerView: RecyclerView, state: RecyclerView.State) { + val items = getParentAndChildren(recyclerView) + for ((parentItem, children) in items) { + val animationState = animator.getStateForPosition(parentItem.position) ?: continue + val childViewHolders = children.mapNotNull { it.viewHolder } + onDrawGroup(canvas, animationState, recyclerView, parentItem.item as ExpandableParentItem, parentItem.viewHolder, childViewHolders) + } + } + + private fun getParentAndChildren(recyclerView: RecyclerView): Map> { + // Searching all view holders in recycler view and match them with adapter items + val items = recyclerView.children.toList() + .mapNotNull { + val viewHolder = recyclerView.getChildViewHolder(it) + val expandableViewHolder = viewHolder as? ExpandableBaseViewHolder<*> ?: return@mapNotNull null + val item = expandableViewHolder.expandableItem ?: return@mapNotNull null + ItemWithViewHolder(viewHolder.bindingAdapterPosition, item, viewHolder) + } + + // Grouping view holders by parents + val parentsWithChildren = mutableMapOf>() + + val parents = items.filter { it.item is ExpandableParentItem }.associateBy { it.item.getId() } + val children = items.filter { it.item is ExpandableChildItem } + parents.values.forEach { parentsWithChildren[it] = mutableListOf() } + + children.forEach { child -> + val item = child.item as ExpandableChildItem + val parent = parents[item.groupId] ?: getParentForItem(recyclerView, item) ?: return@forEach + val parentChildren = parentsWithChildren[parent] ?: mutableListOf() + parentChildren.add(child) + parentsWithChildren[parent] = parentChildren + } + + return parentsWithChildren + } + + private fun getParentForItem(recyclerView: RecyclerView, item: ExpandableChildItem): ItemWithViewHolder? { + val positionInAdapter = adapter.getItems().indexOfFirstOrNull { it.getId() == item.groupId } ?: return null + val parentItem = adapter.getItems()[positionInAdapter] + val globalAdapterPosition = positionInAdapter.convertToGlobalAdapterPosition(recyclerView, adapter as Adapter<*>) + val viewHolder = recyclerView.findViewHolderForAdapterPosition(globalAdapterPosition) + return ItemWithViewHolder(positionInAdapter, parentItem as ExpandableParentItem, viewHolder) + } + + // Useful to find global position if ConcatAdapter is used + private fun Int.convertToGlobalAdapterPosition(recyclerView: RecyclerView, localAdapter: Adapter<*>): Int { + val globalAdapter = recyclerView.adapter + return if (globalAdapter is ConcatAdapter) { + val localAdapterIndex = globalAdapter.adapters.indexOf(localAdapter) + if (localAdapterIndex > 0) { + val adaptersBeforeTarget = globalAdapter.adapters.subList(0, localAdapterIndex - 1) + val offset = adaptersBeforeTarget.sumOf { it.itemCount } + this + offset + } else { + this + } + } else { + this + } + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableParentViewHolder.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableParentViewHolder.kt new file mode 100644 index 0000000000..503bbcb7b9 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/ExpandableParentViewHolder.kt @@ -0,0 +1,21 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable + +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableBaseItem +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableChildItem +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableParentItem + +interface ExpandableBaseViewHolder { + var expandableItem: T? +} + +/** + * The view holder that may show ExpandableChildItem's + * It's used to check the type of viewHolder in [ExpandableItemDecoration] + */ +interface ExpandableParentViewHolder : ExpandableBaseViewHolder + +/** + * The view holder that is shown as an ExpandableChildItem + * It's used to check the type of viewHolder in [ExpandableItemDecoration] + */ +interface ExpandableChildViewHolder : ExpandableBaseViewHolder diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/animator/ExpandableAnimator.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/animator/ExpandableAnimator.kt new file mode 100644 index 0000000000..eb54259080 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/animator/ExpandableAnimator.kt @@ -0,0 +1,107 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable.animator + +import android.animation.Animator +import android.animation.ValueAnimator +import androidx.core.animation.addListener +import androidx.recyclerview.widget.RecyclerView +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAdapter +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings +import io.novafoundation.nova.common.utils.recyclerView.expandable.flippedFraction +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableBaseItem +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableParentItem + +/** + * EXPANDING: animationFraction = 0f - fully collapsed and animationFraction = 1f - fully expanded + * COLLAPSING: animationFraction = 0f - fully expanded and animationFraction = 1f - fully collapsed + * So animationFraction is always move from 0f to 1f + */ +class ExpandableAnimationItemState(val animationType: Type, animationFraction: Float) { + + var animationFraction: Float = animationFraction + internal set(value) { + field = value.coerceIn(0f, 1f) + } + + enum class Type { + EXPANDING, COLLAPSING + } +} + +private class RunningAnimation(val currentState: ExpandableAnimationItemState, val animator: Animator) + +class ExpandableAnimator( + private val recyclerView: RecyclerView, + private val animationSettings: ExpandableAnimationSettings, + private val expandableAdapter: ExpandableAdapter +) { + + // It contains only items that is animating right now + private val runningAnimations = mutableMapOf() + + // Return current animation state for parent position or calculate state in [getExpandableItemState] if it isn't animating now + fun getStateForPosition(position: Int): ExpandableAnimationItemState? { + val items = expandableAdapter.getItems() + val item = items.getOrNull(position) ?: return null + if (item !is ExpandableParentItem) return null + + return runningAnimations[item.getId()]?.currentState ?: getExpandableItemState(position, items) + } + + // Just prepare an animation without running + fun prepareAnimationToState(parentId: String, type: ExpandableAnimationItemState.Type) { + val existingSettings = runningAnimations[parentId] + + // No need to run animation if animation state is running and current type is the same + if (existingSettings == null) { + val state = ExpandableAnimationItemState(type, 0f) + setAnimationFor(parentId, state) + } else { + // No need to update animation state if it's the same and already running + if (existingSettings.currentState.animationType == type) { + return + } + + // Toggle animation state and flipping fraction to continue the animation but to another side + val state = ExpandableAnimationItemState(type, existingSettings.currentState.flippedFraction()) + setAnimationFor(parentId, state) + } + } + + fun runAnimationFor(parentId: String) { + val existingSettings = runningAnimations[parentId] + existingSettings?.animator?.start() + } + + private fun setAnimationFor(parentId: String, state: ExpandableAnimationItemState) { + runningAnimations[parentId]?.animator?.cancel() // Cancel previous animation if it's exist + + val animator = ValueAnimator.ofFloat(state.animationFraction, 1f) + .setDuration(animationSettings.duration) + + animator.interpolator = animationSettings.interpolator + animator.addUpdateListener { + state.animationFraction = it.animatedValue as Float + recyclerView.invalidate() + } // Invalidate recycler view to trigger onDraw in Item Decoration + animator.addListener(onEnd = { runningAnimations.remove(parentId) }) + + runningAnimations[parentId] = RunningAnimation(state, animator) + } + + fun cancelAnimations() { + runningAnimations.values + .toList() // Copy list to avoid ConcurrentModificationException + .forEach { it.animator.cancel() } + } + + private fun getExpandableItemState(position: Int, items: List): ExpandableAnimationItemState { + val nextItem = items.getOrNull(position + 1) + + // If next item is not a parent item it means current item is fully expanded + return if (nextItem == null || nextItem is ExpandableParentItem) { + ExpandableAnimationItemState(ExpandableAnimationItemState.Type.COLLAPSING, 1f) + } else { + ExpandableAnimationItemState(ExpandableAnimationItemState.Type.EXPANDING, 1f) + } + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableBaseItem.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableBaseItem.kt new file mode 100644 index 0000000000..2bcd71970e --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableBaseItem.kt @@ -0,0 +1,8 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable.items + +/** + * The item that may show ExpandingItem's + */ +interface ExpandableBaseItem { + fun getId(): String +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableChildItem.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableChildItem.kt new file mode 100644 index 0000000000..902d8a1e72 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableChildItem.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable.items + +/** + * The item that may be shown or hidden From ExpandableItem + */ +interface ExpandableChildItem : ExpandableBaseItem { + + val groupId: String +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableParentItem.kt b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableParentItem.kt new file mode 100644 index 0000000000..dee85b835e --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/recyclerView/expandable/items/ExpandableParentItem.kt @@ -0,0 +1,6 @@ +package io.novafoundation.nova.common.utils.recyclerView.expandable.items + +/** + * The item that may show ExpandingItem's + */ +interface ExpandableParentItem : ExpandableBaseItem diff --git a/common/src/main/java/io/novafoundation/nova/common/view/AmountView.kt b/common/src/main/java/io/novafoundation/nova/common/view/AmountView.kt index 1f06343917..2cb0e6491a 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/AmountView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/AmountView.kt @@ -8,9 +8,10 @@ import android.view.View import android.widget.EditText import androidx.constraintlayout.widget.ConstraintLayout import coil.ImageLoader -import coil.load import io.novafoundation.nova.common.R import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.common.utils.images.setIcon import io.novafoundation.nova.common.utils.setTextOrHide import io.novafoundation.nova.common.view.shape.getBlockDrawable import io.novafoundation.nova.common.view.shape.getCornersStateDrawable @@ -87,8 +88,8 @@ class AmountView @JvmOverloads constructor( stakingAssetImage.setImageDrawable(image) } - fun loadAssetImage(imageUrl: String?) { - stakingAssetImage.load(imageUrl, imageLoader) + fun loadAssetImage(icon: Icon) { + stakingAssetImage.setIcon(icon, imageLoader) } fun setAssetName(name: String) { diff --git a/common/src/main/java/io/novafoundation/nova/common/view/IconButton.kt b/common/src/main/java/io/novafoundation/nova/common/view/IconButton.kt index aae822e54f..3a73e6bf75 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/IconButton.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/IconButton.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.common.R import io.novafoundation.nova.common.utils.WithContextExtensions import io.novafoundation.nova.common.utils.updatePadding import io.novafoundation.nova.common.utils.useAttributes +import io.novafoundation.nova.common.view.shape.getMaskedRipple class IconButton @JvmOverloads constructor( context: Context, @@ -18,7 +19,7 @@ class IconButton @JvmOverloads constructor( init { updatePadding(top = 6.dp, bottom = 6.dp, start = 12.dp, end = 12.dp) - background = addRipple(getRoundedCornerDrawable(R.color.button_background_secondary)) + background = context.getMaskedRipple(cornerSizeInDp = 10) attrs?.let(::applyAttributes) } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/QrCodeView.kt b/common/src/main/java/io/novafoundation/nova/common/view/QrCodeView.kt new file mode 100644 index 0000000000..af48b0b7a5 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/view/QrCodeView.kt @@ -0,0 +1,189 @@ +package io.novafoundation.nova.common.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import androidx.core.graphics.toRect +import coil.ImageLoader +import coil.request.ImageRequest +import com.google.zxing.qrcode.encoder.QRCode +import io.novafoundation.nova.common.R +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.dp +import io.novafoundation.nova.common.utils.dpF +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.common.utils.images.formatIcon + +class QrCodeModel( + val qrCode: QRCode, + val overlayBackground: Drawable?, + val overlayPaddingInDp: Int, + val centerOverlay: Icon, +) + +class QrCodeView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val qrColor = context.getColor(R.color.qr_code_content) + private val backgroundColor = context.getColor(R.color.qr_code_background) + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + + private var data: QRCode? = null + + private var overlayPadding: Int = 0 + private var centerOverlay: Drawable? = null + private var overlayBackground: Drawable? = null + + private val overlaySize = 64.dp + private val overlayQuiteZone = 0 + private val qrPadding = 16.dpF + + private val centerRect: RectF = RectF() + + private val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) { + if (isInEditMode) { + ImageLoader.invoke(context) + } else { + FeatureUtils.getCommonApi(context).imageLoader() + } + } + + fun setQrModel(model: QrCodeModel) { + this.data = model.qrCode + this.overlayBackground = model.overlayBackground + this.overlayPadding = model.overlayPaddingInDp.dp(context) + + if (model.centerOverlay != null) { + val centerOverlayRequest = getCenterOverlayImageRequest(model.centerOverlay) { + this.centerOverlay = it + invalidate() + } + + imageLoader.enqueue(centerOverlayRequest) + } + invalidate() + } + + private fun getCenterOverlayImageRequest(icon: Icon, target: (Drawable?) -> Unit): ImageRequest { + return ImageRequest.Builder(context) + .data(ImageLoader.formatIcon(icon)) + .target(onSuccess = target) + .size(overlaySize, overlaySize) + .build() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + setMeasuredDimension(widthSize, heightSize) + + centerRect.left = (measuredWidth / 2 - overlaySize / 2).toFloat() + centerRect.top = (measuredHeight / 2 - overlaySize / 2).toFloat() + centerRect.right = (measuredWidth / 2 + overlaySize / 2).toFloat() + centerRect.bottom = (measuredHeight / 2 + overlaySize / 2).toFloat() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + canvas.save() + + paint.color = qrColor + canvas.drawColor(backgroundColor) + data?.let { renderQRCodeImage(canvas, it) } + + canvas.restore() + } + + private fun renderQRCodeImage(canvas: Canvas, data: QRCode) { + renderQRImage(canvas, data, width, height) + } + + private fun renderQRImage(canvas: Canvas, code: QRCode, width: Int, height: Int) { + paint.color = qrColor + val input = code.matrix ?: throw IllegalStateException() + val inputWidth = input.width + val inputHeight = input.height + val outputWidth = Math.max(width, inputWidth) - qrPadding * 2 + val outputHeight = Math.max(height, inputHeight) - qrPadding * 2 + val multiple = Math.min(outputWidth / inputWidth, outputHeight / inputHeight) + val leftPadding = qrPadding + val topPadding = qrPadding + val FINDER_PATTERN_SIZE = 7f + val CIRCLE_SCALE_DOWN_FACTOR = 0.7f + val circleSize = (multiple * CIRCLE_SCALE_DOWN_FACTOR) + val circleRadius = circleSize / 2 + + var inputY = 0 + var outputY = topPadding + + while (inputY < inputHeight) { + var inputX = 0 + var outputX = leftPadding + while (inputX < inputWidth) { + if (input[inputX, inputY].toInt() == 1) { + val overlaysFinder = inputX <= FINDER_PATTERN_SIZE && inputY <= FINDER_PATTERN_SIZE || + inputX >= inputWidth - FINDER_PATTERN_SIZE && inputY <= FINDER_PATTERN_SIZE || + inputX <= FINDER_PATTERN_SIZE && inputY >= inputHeight - FINDER_PATTERN_SIZE + + val overlaysCenter = (this.overlay != null) && ( + this.centerRect.intersects( + outputX - overlayQuiteZone, + outputY - overlayQuiteZone, + outputX + circleSize + overlayQuiteZone, + outputY + circleRadius + overlayQuiteZone + ) + ) + + if (!overlaysCenter && !overlaysFinder) { + canvas.drawCircle(paddingStart + outputX + circleRadius, paddingStart + outputY + circleRadius, circleRadius, paint) + } + } + inputX++ + outputX += multiple + } + inputY++ + outputY += multiple + } + val cornerCircleDiameter = multiple * FINDER_PATTERN_SIZE + drawFinderPatternCircleStyle(canvas, leftPadding, topPadding, cornerCircleDiameter) + drawFinderPatternCircleStyle(canvas, leftPadding + (inputWidth - FINDER_PATTERN_SIZE) * multiple, topPadding, cornerCircleDiameter) + drawFinderPatternCircleStyle(canvas, leftPadding, topPadding + (inputHeight - FINDER_PATTERN_SIZE) * multiple, cornerCircleDiameter) + + drawCenterOverlay(canvas) + } + + private fun drawFinderPatternCircleStyle(canvas: Canvas, x: Float, y: Float, circleDiameter: Float) { + val radius = circleDiameter / 2 + val WHITE_CIRCLE_RADIUS = circleDiameter * 5 / 7 / 2 + val WHITE_CIRCLE_OFFSET = circleDiameter / 7 + WHITE_CIRCLE_RADIUS + val MIDDLE_DOT_RADIUS = circleDiameter * 3 / 7 / 2 + val MIDDLE_DOT_OFFSET = circleDiameter * 2 / 7 + MIDDLE_DOT_RADIUS + paint.color = qrColor + canvas.drawCircle(x + radius, y + radius, radius, paint) + paint.color = backgroundColor + canvas.drawCircle(x + WHITE_CIRCLE_OFFSET, y + WHITE_CIRCLE_OFFSET, WHITE_CIRCLE_RADIUS, paint) + paint.color = qrColor + canvas.drawCircle(x + MIDDLE_DOT_OFFSET, y + MIDDLE_DOT_OFFSET, MIDDLE_DOT_RADIUS, paint) + } + + private fun drawCenterOverlay(canvas: Canvas) { + val overlayBackgroundWithInsets = centerRect.toRect().apply { + inset(overlayPadding, overlayPadding) + } + + overlayBackground?.bounds = overlayBackgroundWithInsets + centerOverlay?.bounds = overlayBackgroundWithInsets + + overlayBackground?.draw(canvas) + centerOverlay?.draw(canvas) + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt b/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt index 4da2908487..b87c1a8ff6 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt @@ -195,7 +195,7 @@ open class TableCellView @JvmOverloads constructor( tableCellTitle.setDrawableStart(icon, widthInDp = 16, paddingInDp = 4, tint = tintRes) } - fun showValue(primary: CharSequence, secondary: String? = null) { + fun showValue(primary: CharSequence, secondary: CharSequence? = null) { postToSelf { contentGroup.makeVisible() @@ -278,7 +278,7 @@ open class TableCellView @JvmOverloads constructor( } } -fun TableCellView.showValueOrHide(primary: CharSequence?, secondary: String? = null) { +fun TableCellView.showValueOrHide(primary: CharSequence?, secondary: CharSequence? = null) { if (primary != null) { showValue(primary, secondary) } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/bottomSheet/list/dynamic/DynamicListBottomSheet.kt b/common/src/main/java/io/novafoundation/nova/common/view/bottomSheet/list/dynamic/DynamicListBottomSheet.kt index da5bd6d575..4dd6b54d50 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/bottomSheet/list/dynamic/DynamicListBottomSheet.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/bottomSheet/list/dynamic/DynamicListBottomSheet.kt @@ -11,12 +11,14 @@ import androidx.recyclerview.widget.RecyclerView import io.novafoundation.nova.common.R import io.novafoundation.nova.common.utils.DialogExtensions import io.novafoundation.nova.common.utils.WithContextExtensions +import io.novafoundation.nova.common.utils.setTextOrHide import io.novafoundation.nova.common.utils.setVisible import io.novafoundation.nova.common.view.bottomSheet.BaseBottomSheet import kotlinx.android.synthetic.main.bottom_sheet_dynamic_list.dynamicListSheetContent import kotlinx.android.synthetic.main.bottom_sheet_dynamic_list.dynamicListSheetHeader import kotlinx.android.synthetic.main.bottom_sheet_dynamic_list.dynamicListSheetItemContainer import kotlinx.android.synthetic.main.bottom_sheet_dynamic_list.dynamicListSheetRightAction +import kotlinx.android.synthetic.main.bottom_sheet_dynamic_list.dynamicListSheetSubtitle import kotlinx.android.synthetic.main.bottom_sheet_dynamic_list.dynamicListSheetTitle typealias ClickHandler = (BaseDynamicListBottomSheet, T) -> Unit @@ -56,6 +58,10 @@ abstract class BaseDynamicListBottomSheet(context: Context) : dynamicListSheetTitle.text = title } + fun setSubtitle(subtitle: CharSequence?) { + dynamicListSheetSubtitle.setTextOrHide(subtitle) + } + final override fun setTitle(titleId: Int) { dynamicListSheetTitle.setText(titleId) } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/recyclerview/item/OperationListItem.kt b/common/src/main/java/io/novafoundation/nova/common/view/recyclerview/item/OperationListItem.kt index 7a00301eaf..a5f76b4294 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/recyclerview/item/OperationListItem.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/recyclerview/item/OperationListItem.kt @@ -60,7 +60,6 @@ class OperationListItem @kotlin.jvm.JvmOverloads constructor( fun setIconStyle(iconStyle: IconStyle) { when (iconStyle) { IconStyle.BORDERED_CIRCLE -> { - icon.setPadding(6.dp) icon.setBackgroundResource(R.drawable.bg_icon_container_on_color) icon.setImageTintRes(R.color.icon_secondary) } diff --git a/common/src/main/res/anim/asset_mode_fade_in.xml b/common/src/main/res/anim/asset_mode_fade_in.xml new file mode 100644 index 0000000000..33a1e91d0d --- /dev/null +++ b/common/src/main/res/anim/asset_mode_fade_in.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/common/src/main/res/anim/asset_mode_fade_out.xml b/common/src/main/res/anim/asset_mode_fade_out.xml new file mode 100644 index 0000000000..a7dccf1656 --- /dev/null +++ b/common/src/main/res/anim/asset_mode_fade_out.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/common/src/main/res/anim/asset_mode_slide_bottom_in.xml b/common/src/main/res/anim/asset_mode_slide_bottom_in.xml new file mode 100644 index 0000000000..42fca22211 --- /dev/null +++ b/common/src/main/res/anim/asset_mode_slide_bottom_in.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/common/src/main/res/anim/asset_mode_slide_bottom_out.xml b/common/src/main/res/anim/asset_mode_slide_bottom_out.xml new file mode 100644 index 0000000000..d6e7826cac --- /dev/null +++ b/common/src/main/res/anim/asset_mode_slide_bottom_out.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/common/src/main/res/anim/asset_mode_slide_top_in.xml b/common/src/main/res/anim/asset_mode_slide_top_in.xml new file mode 100644 index 0000000000..f17339bd58 --- /dev/null +++ b/common/src/main/res/anim/asset_mode_slide_top_in.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/common/src/main/res/anim/asset_mode_slide_top_out.xml b/common/src/main/res/anim/asset_mode_slide_top_out.xml new file mode 100644 index 0000000000..702bd58091 --- /dev/null +++ b/common/src/main/res/anim/asset_mode_slide_top_out.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/common/src/main/res/color/appearance_selectable_text.xml b/common/src/main/res/color/appearance_selectable_text.xml new file mode 100644 index 0000000000..0eff9797cc --- /dev/null +++ b/common/src/main/res/color/appearance_selectable_text.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/bg_appearance_container.xml b/common/src/main/res/drawable/bg_appearance_container.xml new file mode 100644 index 0000000000..35e891d176 --- /dev/null +++ b/common/src/main/res/drawable/bg_appearance_container.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/bg_primary_list_item_corner_12.xml b/common/src/main/res/drawable/bg_primary_list_item_corner_12.xml new file mode 100644 index 0000000000..70b4025ffd --- /dev/null +++ b/common/src/main/res/drawable/bg_primary_list_item_corner_12.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/bg_primary_list_item_corner_12_solid.xml b/common/src/main/res/drawable/bg_primary_list_item_corner_12_solid.xml new file mode 100644 index 0000000000..ca2c3db466 --- /dev/null +++ b/common/src/main/res/drawable/bg_primary_list_item_corner_12_solid.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/bg_token_container.xml b/common/src/main/res/drawable/bg_token_container.xml index 26cb5eb585..8953dac9b2 100644 --- a/common/src/main/res/drawable/bg_token_container.xml +++ b/common/src/main/res/drawable/bg_token_container.xml @@ -1,10 +1,25 @@ - + + + - + - + + + + + + + + + + + + + + + + + - - \ No newline at end of file diff --git a/common/src/main/res/drawable/ic_asset_view_networks.xml b/common/src/main/res/drawable/ic_asset_view_networks.xml new file mode 100644 index 0000000000..d96811e9b4 --- /dev/null +++ b/common/src/main/res/drawable/ic_asset_view_networks.xml @@ -0,0 +1,14 @@ + + + + diff --git a/common/src/main/res/drawable/ic_asset_view_tokens.xml b/common/src/main/res/drawable/ic_asset_view_tokens.xml new file mode 100644 index 0000000000..b4a235b52e --- /dev/null +++ b/common/src/main/res/drawable/ic_asset_view_tokens.xml @@ -0,0 +1,14 @@ + + + + diff --git a/common/src/main/res/drawable/ic_nova.xml b/common/src/main/res/drawable/ic_nova.xml index 7c3883474a..3509569d13 100644 --- a/common/src/main/res/drawable/ic_nova.xml +++ b/common/src/main/res/drawable/ic_nova.xml @@ -1,9 +1,11 @@ - + android:viewportWidth="48" + android:viewportHeight="48"> + + + diff --git a/common/src/main/res/drawable/ic_receive_history.xml b/common/src/main/res/drawable/ic_receive_history.xml new file mode 100644 index 0000000000..6523824cb6 --- /dev/null +++ b/common/src/main/res/drawable/ic_receive_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/common/src/main/res/drawable/ic_send_history.xml b/common/src/main/res/drawable/ic_send_history.xml new file mode 100644 index 0000000000..255dc6da7d --- /dev/null +++ b/common/src/main/res/drawable/ic_send_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/common/src/main/res/drawable/ic_settings_appearance.xml b/common/src/main/res/drawable/ic_settings_appearance.xml new file mode 100644 index 0000000000..e9ff47a09b --- /dev/null +++ b/common/src/main/res/drawable/ic_settings_appearance.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/common/src/main/res/drawable/ic_staking_history.xml b/common/src/main/res/drawable/ic_staking_history.xml new file mode 100644 index 0000000000..e9874f87b4 --- /dev/null +++ b/common/src/main/res/drawable/ic_staking_history.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/src/main/res/drawable/ic_swap_history.xml b/common/src/main/res/drawable/ic_swap_history.xml new file mode 100644 index 0000000000..405a5488c7 --- /dev/null +++ b/common/src/main/res/drawable/ic_swap_history.xml @@ -0,0 +1,12 @@ + + + + diff --git a/common/src/main/res/drawable/ic_token_dot.xml b/common/src/main/res/drawable/ic_token_dot.xml deleted file mode 100644 index 90b21487ca..0000000000 --- a/common/src/main/res/drawable/ic_token_dot.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/common/src/main/res/drawable/ic_token_dot_colored.xml b/common/src/main/res/drawable/ic_token_dot_colored.xml new file mode 100644 index 0000000000..6bf4dc417f --- /dev/null +++ b/common/src/main/res/drawable/ic_token_dot_colored.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/common/src/main/res/drawable/ic_token_dot_white.xml b/common/src/main/res/drawable/ic_token_dot_white.xml new file mode 100644 index 0000000000..d968154d4c --- /dev/null +++ b/common/src/main/res/drawable/ic_token_dot_white.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/common/src/main/res/layout/item_operation_list_item.xml b/common/src/main/res/layout/item_operation_list_item.xml index 7f06bfdb39..11dcd20dc5 100644 --- a/common/src/main/res/layout/item_operation_list_item.xml +++ b/common/src/main/res/layout/item_operation_list_item.xml @@ -14,10 +14,11 @@ android:layout_marginStart="16dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" + android:scaleType="centerCrop" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/ic_staking_filled" /> + tools:src="@drawable/ic_send_history" /> + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/ic_token_dot_colored" + tools:visibility="visible" /> Has alcanzado el límite de %s proxies añadidos en %s. Elimina proxies para añadir nuevos. Se ha alcanzado el número máximo de proxies Las redes personalizadas agregadas\naquí aparecerán + Coloreado + Apariencia + iconos de token + Blanco La dirección de contrato ingresada ya está presente en Nova como un token %s. La dirección de contrato ingresada ya está presente en Nova como un token %s. ¿Estás seguro de que quieres modificarlo? Este token ya existe @@ -185,6 +189,8 @@ Ingrese la dirección del contrato Ingrese los decimales Ingrese el símbolo + Redes + Tokens Agregar token Dirección del contrato Decimales @@ -207,6 +213,7 @@ Ledger no admite este token Buscar por red o token No se encontraron redes o tokens con\n el nombre ingresado + Buscar por token Sus carteras No tiene tokens para enviar.\nCompre o reciba tokens en su\ncuenta. Token para pagar @@ -235,6 +242,7 @@ Respaldar Autenticación biométrica ¡Compra iniciada! Por favor, espere hasta 60 minutos. Puede seguir el estado en el correo electrónico. + Seleccionar red para comprar %s Para continuar la compra será redirigido desde la aplicación Nova Wallet a %s ¿Continuar en el navegador? Equilibrar nodos automáticamente @@ -968,6 +976,8 @@ Al habilitar las notificaciones push, acepta nuestros %s y %s Intente de nuevo más tarde accediendo a los ajustes de notificación desde la pestaña de Configuración ¡No te pierdas de nada! + Seleccionar red para recibir %s + Copiar dirección Pega el json o sube un archivo… Subir archivo Restaurar JSON @@ -1122,9 +1132,11 @@ Pistas no disponibles Nova necesita que la ubicación esté habilitada para poder realizar el escaneo bluetooth y encontrar tu dispositivo Ledger Por favor, habilita la geolocalización en las configuraciones del dispositivo + Seleccionar red Seleccione pistas para %d de %d Dirección o w3n + Seleccionar red para enviar %s El destinatario es una cuenta del sistema. No está controlado por ninguna empresa o individuo.\n¿Estás seguro de que aún quieres realizar esta transferencia? Los tokens se perderán Otorgar autoridad a @@ -1534,6 +1546,7 @@ Ingresa otro monto Para pagar la comisión de red con %s, Nova intercambiará automáticamente %s por %s para mantener el balance mínimo de %s en tu cuenta. Una comisión de red cobrada por la cadena de bloques para procesar y validar cualquier transacción. Puede variar dependiendo de las condiciones de la red o la velocidad de la transacción. + Seleccionar red para intercambiar %s El fondo no tiene suficiente liquidez para intercambiar La diferencia de precio se refiere a la diferencia en el precio entre dos activos diferentes. Al realizar un intercambio en cripto, la diferencia de precio es generalmente la diferencia entre el precio del activo por el que estás intercambiando y el precio del activo con el que estás intercambiando. Diferencia de precio @@ -1626,6 +1639,7 @@ Comprar con Recibir Recibir %s + Enviar solo el token %1$s y tokens en la red %2$s a esta dirección, o podrías perder tus fondos Enviar Intercambiar Activos diff --git a/common/src/main/res/values-fr-rFR/strings.xml b/common/src/main/res/values-fr-rFR/strings.xml index df5fbce85c..1a2d369044 100644 --- a/common/src/main/res/values-fr-rFR/strings.xml +++ b/common/src/main/res/values-fr-rFR/strings.xml @@ -173,6 +173,10 @@ Vous avez atteint la limite de %s proxies ajoutés dans %s. Supprimez des proxies pour en ajouter de nouveaux. Le nombre maximum de proxies a été atteint Les réseaux personnalisés ajoutés\napparaîtront ici + Coloré + Apparence + icônes des tokens + Blanc L\'adresse du contrat saisie est présente dans Nova sous la forme d\'un jeton %s. L\'adresse du contrat saisie est présente dans Nova comme un token %s. Êtes-vous sûr de vouloir le modifier ? Ce jeton existe déjà @@ -185,6 +189,8 @@ Entrez l\'adresse du contrat Entrez les décimales Entrez le symbole + Réseaux + Tokens Ajouter un jeton Adresse du contrat Décimales @@ -207,6 +213,7 @@ Ledger ne prend pas en charge cet actif Recherche par réseau ou actif Aucun réseau ou jeton avec\nnom saisi n\'a été trouvé + Rechercher par token Vos portefeuilles Vous n\'avez pas de tokens à envoyer.\nAchetez ou recevez des tokens sur votre\ncompte. Token à payer @@ -235,6 +242,7 @@ Sauvegarde Auth biométrique L\'achat est lancé ! Veuillez patienter jusqu\'à 60 minutes. Vous pouvez suivre l\'état d\'avancement sur l\'e-mail. + Sélectionner le réseau pour acheter %s Pour poursuivre l\'achat, vous serez redirigé de l\'application Nova Wallet vers %s Continuer dans le navigateur? Équilibrage automatique des nœuds @@ -968,6 +976,8 @@ En activant les notifications push, vous acceptez nos %s et %s Veuillez réessayer plus tard en accédant aux paramètres de notification depuis l\'onglet Paramètres Ne manquez rien ! + Sélectionner le réseau pour recevoir %s + Copier l\'adresse Collez le fichier JSON ou téléchargez le fichier... Télécharger le fichier Restaurer JSON @@ -1122,9 +1132,11 @@ Chemins non disponibles Nova a besoin que la localisation soit activée pour pouvoir effectuer un balayage Bluetooth afin de trouver votre appareil Ledger Veuillez activer la géolocalisation dans les paramètres de l\'appareil + Sélectionner un réseau Sélectionner des pistes pour %d sur %d Adresse ou w3n + Sélectionner le réseau pour envoyer %s Le destinataire est un compte système. Il n\'est contrôlé par aucune entreprise ou individu.\nÊtes-vous sûr de vouloir effectuer ce transfert ? Les tokens seront perdus Donner l\'autorité à @@ -1534,6 +1546,7 @@ Entrez un autre montant Pour payer les frais de réseau avec %s, Nova échangera automatiquement %s contre %s pour maintenir le solde minimum de %s sur votre compte. Des frais de réseau facturés par la blockchain pour traiter et valider toutes les transactions. Ils peuvent varier en fonction des conditions du réseau ou de la rapidité de la transaction. + Sélectionner le réseau pour échanger %s Le pool n\'a pas assez de liquidité pour échanger La différence de prix fait référence à la différence de prix entre deux actifs différents. Lors de l\'échange en crypto, la différence de prix est généralement la différence entre le prix de l\'actif que vous échangez et le prix de l\'actif avec lequel vous échangez. Différence de prix @@ -1626,6 +1639,7 @@ Acheter avec Recevoir Recevoir %s + Envoyez uniquement le token %1$s et les tokens dans le réseau %2$s à cette adresse, ou vous pourriez perdre vos fonds Envoyer Échanger Actifs diff --git a/common/src/main/res/values-in/strings.xml b/common/src/main/res/values-in/strings.xml index 2c83b7e843..d6b692fefb 100644 --- a/common/src/main/res/values-in/strings.xml +++ b/common/src/main/res/values-in/strings.xml @@ -173,6 +173,10 @@ Anda telah mencapai batas %s proxy yang ditambahkan dalam %s. Hapus proxy untuk menambahkan yang baru. Jumlah maksimum proxy telah tercapai Jaringan kustom yang ditambahkan\nakan muncul di sini + Berwarna + Penampilan + ikon token + Putih Alamat kontrak yang dimasukkan ada dalam Nova sebagai token %s. Alamat kontrak yang dimasukkan ada dalam Nova sebagai token %s. Apakah Anda yakin ingin memodifikasinya? Token ini sudah ada @@ -185,6 +189,8 @@ Masukkan alamat kontrak Masukkan desimal Masukkan simbol + Jaringan + Token Tambahkan token Alamat kontrak Desimal @@ -207,6 +213,7 @@ Ledger tidak mendukung token ini Cari berdasarkan jaringan atau token Tidak ada jaringan atau token dengan nama yang dimasukkan ditemukan + Cari berdasarkan token Dompet Anda Anda tidak memiliki token untuk dikirim. Beli atau Terima token ke akun Anda. Token untuk membayar @@ -235,6 +242,7 @@ Cadangan Otentikasi Biometrik Pembelian dimulai! Harap tunggu hingga 60 menit. Anda dapat melacak statusnya melalui email. + Pilih jaringan untuk membeli %s Untuk melanjutkan pembelian, Anda akan diarahkan dari aplikasi Nova Wallet ke %s Lanjut di browser? Node saldo otomatis @@ -961,6 +969,8 @@ Dengan mengaktifkan pemberitahuan push, Anda menyetujui %s dan %s kami Harap coba lagi nanti dengan mengakses pengaturan pemberitahuan dari tab Pengaturan Jangan lewatkan hal apa pun! + Pilih jaringan untuk menerima %s + Salin Alamat Tempelkan json atau unggah file... Unggah file Pulihkan JSON @@ -1114,9 +1124,11 @@ Unavailable tracks Nova needs location to be enabled to be able to perform bluetooth scanning to find your Ledger device Please enable geo-location in device settings + Pilih jaringan Select tracks for %d of %d Address or w3n + Pilih jaringan untuk mengirim %s Recipient is a system account. It is not controlled by any company or individual.\nAre you sure you still want to perform this transfer? Tokens will be lost Give authority to @@ -1524,6 +1536,7 @@ Masukkan jumlah lain Untuk membayar biaya jaringan dengan %s, Nova secara otomatis akan menukar %s dengan %s untuk mempertahankan saldo minimum %s akun Anda. Biaya jaringan yang dibebankan oleh blockchain untuk memproses dan memvalidasi transaksi. Bisa bervariasi tergantung pada kondisi jaringan atau kecepatan transaksi. + Pilih jaringan untuk menukar %s Pool tidak memiliki likuiditas yang cukup untuk swap Perbedaan harga merujuk pada perbedaan harga antara dua aset yang berbeda. Ketika melakukan swap dalam kripto, perbedaan harga biasanya adalah perbedaan antara harga aset yang Anda tukarkan dengan harga aset yang Anda tukar. Perbedaan harga @@ -1616,6 +1629,7 @@ Beli dengan Terima Terima %s + Hanya kirim token %1$s dan token di jaringan %2$s ke alamat ini, atau Anda bisa kehilangan dana Anda Kirim Tukar Aset diff --git a/common/src/main/res/values-it/strings.xml b/common/src/main/res/values-it/strings.xml index df030d8362..7ccfbe525a 100644 --- a/common/src/main/res/values-it/strings.xml +++ b/common/src/main/res/values-it/strings.xml @@ -173,6 +173,10 @@ Hai raggiunto il limite di %s proxy aggiunti in %s. Rimuovi i proxy per aggiungerne di nuovi Limite massimo di proxy raggiunto Le reti personalizzate aggiunte\nappariranno qui + Colorato + Aspetto + icone dei token + Bianco L\'indirizzo del contratto inserito è presente in Nova come token %s. L\'indirizzo del contratto inserito è presente in Nova come token %s. Sei sicuro di volerlo modificare? Questo token esiste già @@ -185,6 +189,8 @@ Inserisci l\'indirizzo del contratto Inserisci i decimali Inserisci il simbolo + Reti + Token Aggiungi token Indirizzo del contratto Decimali @@ -207,6 +213,7 @@ Ledger non supporta questo token Cerca per rete o token Nessuna rete o token con il nome inserito è stata trovata + Cerca per token I tuoi portafogli Non hai token da inviare. \nCompra o ricevi token sul tuo \nconto. Token da pagare @@ -235,6 +242,7 @@ Backup Autenticazione biometrica Acquisto iniziato! Attendi fino a 60 minuti. Puoi controllare lo stato sulla email. + Seleziona la rete per l\'acquisto di %s Per continuare l\'acquisto sarai reindirizzato dall\'app Nova Wallet a %s Continuare nel browser? Bilanciamento automatico dei nodi @@ -968,6 +976,8 @@ Abilitando le notifiche push, accetti i nostri %s e %s Si prega di riprovare più tardi accedendo alle impostazioni delle notifiche dalla scheda Impostazioni Non perdere nulla! + Seleziona la rete per ricevere %s + Copia indirizzo Incolla json o carica il file... Carica file Ripristina JSON per il recupero @@ -1122,9 +1132,11 @@ Tracce non disponibili Nova ha bisogno che la posizione sia attivata per poter effettuare la scansione Bluetooth per trovare il tuo dispositivo Ledger Si prega di abilitare la geolocalizzazione nelle impostazioni del dispositivo + Seleziona rete Seleziona brani per %d di %d Indirizzo o w3n + Seleziona la rete per inviare %s Il destinatario è un conto di sistema. Non è controllato da alcuna azienda o individuo.\nSei sicuro di voler comunque effettuare questo trasferimento? I token verranno persi Conferire autorità a @@ -1534,6 +1546,7 @@ Inserisci un altro importo Per pagare la tariffa di rete con %s, Nova effettuerà automaticamente lo scambio di %s per %s per mantenere il saldo minimo del tuo account di %s. Una tariffa di rete addebitata dal blockchain per elaborare e convalidare qualsiasi transazione. Può variare a seconda delle condizioni di rete o della velocità delle transazioni. + Seleziona la rete per scambiare %s Il pool non ha abbastanza liquidità per lo scambio La differenza di prezzo si riferisce alla differenza di prezzo tra due diversi asset. Quando si effettua uno scambio in criptovaluta, la differenza di prezzo è di solito la differenza tra il prezzo dell\'asset che stai scambiando e il prezzo dell\'asset con cui stai scambiando. Differenza di prezzo @@ -1626,6 +1639,7 @@ Acquista con Ricevi Ricevi %s + Invia solo token %1$s e token nella rete %2$s a questo indirizzo, altrimenti potresti perdere i tuoi fondi Invia Scambia Asset diff --git a/common/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml index afb5a07e86..2b31a4ecfd 100644 --- a/common/src/main/res/values-ja/strings.xml +++ b/common/src/main/res/values-ja/strings.xml @@ -173,6 +173,10 @@ %sで追加できるプロキシの上限に達しました。新しいものを追加するためには既存のプロキシを削除してください。 プロキシの最大数に達しました 追加されたカスタムネットワーク\nここに表示されます + カラー + 外観 + トークンアイコン + ホワイト 入力されたコントラクトアドレスは%sトークンとしてNovaに存在します。 入力されたコントラクトアドレスは%sトークンとしてNovaに存在します。本当にそれを変更しますか? このトークンはすでに存在します @@ -185,6 +189,8 @@ コントラクトアドレスを入力 小数を入力 シンボルを入力 + ネットワーク + トークン トークンを追加 コントラクトアドレス 小数 @@ -207,6 +213,7 @@ Ledgerはこのトークンをサポートしていません ネットワークまたはトークンで検索 入力した名前のネットワークやトークンが見つかりませんでした + トークンで検索 あなたのウォレット 送信するトークンがありません。\nトークンを購入または受け取ってください\nアカウントに。 支払い用トークン @@ -235,6 +242,7 @@ バックアップ 生体認証 購入が開始されました! 最大60分お待ちください。メールでステータスを追跡できます。 + 購入するネットワークを選択 %s 購入を続行するために、Nova Walletアプリから%sにリダイレクトされます。 ブラウザで続行しますか? 自動バランスノード @@ -961,6 +969,8 @@ プッシュ通知を有効にすることで、%sと%sに同意したことになります 設定タブから通知設定にアクセスして、後でもう一度お試しください お見逃しなく! + 受信するネットワークを選択 %s + 住所をコピー jsonを貼り付けるかファイルをアップロード... ファイルをアップロード JSONを復元 @@ -1114,9 +1124,11 @@ 利用不可のトラック Novaは、Ledgerデバイスを見つけるためにBluetoothスキャンを実行するために位置情報の有効化が必要です デバイス設定で位置情報を有効にしてください + ネットワークを選択 トラックを選択 %d / %d アドレスまたはw3n + 送信するネットワークを選択 %s 受信者はシステムアカウントです。どの会社や個人にも管理されていません。\nそれでもこの転送を実行してもよろしいですか? トークンが失われます 権限を与える @@ -1524,6 +1536,7 @@ 別の金額を入力 ネットワーク手数料を %s で支払うため、Novaは %s を %s に自動的にスワップして、アカウントの最低 %s 残高を維持します。 ブロックチェーンによって取引や検証を処理するためのネットワーク手数料。ネットワークの状況や取引速度によって異なる場合があります。 + スワップするネットワークを選択 %s プールにスワップするための十分な流動性がありません 価格差とは、二つの異なる資産間の価格差を指します。暗号通貨のスワップを行う際、価格差は通常、交換する資産の価格と交換される資産の価格の違いを意味します。 価格差 @@ -1616,6 +1629,7 @@ で購入 受け取る 受け取る %s + 送信するのは%1$sトークンと%2$sネットワークのトークンのみ可能です、それ以外の場合、資金を失う可能性があります 送信 スワップ 資産 diff --git a/common/src/main/res/values-ko/strings.xml b/common/src/main/res/values-ko/strings.xml index 4142aa26be..17b029a572 100644 --- a/common/src/main/res/values-ko/strings.xml +++ b/common/src/main/res/values-ko/strings.xml @@ -173,6 +173,10 @@ %s에서 추가된 프록시 수의 한도에 도달했습니다. 새로운 프록시를 추가하려면 기존 프록시를 제거하세요. 최대 프록시 수에 도달했습니다 추가된 사용자 정의 네트워크는 여기에 나타납니다 + 컬러 + 외관 + 토큰 아이콘 + 화이트 입력된 계약 주소는 Nova에서 %s 토큰으로 이미 존재합니다. 입력된 계약 주소는 Nova에서 %s 토큰으로 존재합니다. 수정하시겠습니까? 이 토큰은 이미 존재합니다 @@ -185,6 +189,8 @@ 계약 주소 입력 소수 자릿수 입력 심볼 입력 + 네트워크 + 토큰 토큰 추가 계약 주소 소수 자릿수 @@ -207,6 +213,7 @@ Ledger에서 이 토큰을 지원하지 않습니다. 네트워크 또는 토큰으로 검색 입력한 이름과 일치하는 네트워크 또는 토큰이 없습니다. + 토큰으로 검색 당신의 지갑 보낼 토큰이 없습니다.\n계정에 토큰을 구매하거나 수신하십시오. 지불할 토큰 @@ -235,6 +242,7 @@ 백업 생체 인증 구매가 시작되었습니다! 최대 60분까지 기다려주세요. 이메일에서 상태를 추적할 수 있습니다. + %s 구매를 위한 네트워크 선택 구매를 계속하려면 Nova Wallet 앱에서 %s로 리디렉션됩니다. 브라우저에서 계속할까요? 자동 균형 노드 @@ -961,6 +969,8 @@ 푸시 알림을 활성화함으로써 귀하는 우리의 %s 및 %s에 동의하게 됩니다 설정 탭에서 알림 설정에 접근하여 나중에 다시 시도해주세요 중요한 것을 놓치지 마세요! + %s 수신을 위한 네트워크 선택 + 주소 복사 json을 붙여넣거나 파일을 업로드하세요... 파일 업로드 JSON 복원 @@ -1114,9 +1124,11 @@ 사용할 수 없는 트랙 Ledger 장치를 찾기 위해 블루투스 스캔을 수행하려면 Nova는 위치 기능을 활성화해야 합니다. 기기 설정에서 지리적 위치를 활성화해주세요. + 네트워크 선택 트랙 선택 %d 중 %d 주소 또는 w3n + %s 전송을 위한 네트워크 선택 수신자가 시스템 계정입니다. 이 계정은 어떤 회사나 개인에 의해 제어되지 않습니다.\n여전히 이 전송을 수행하시겠습니까? 토큰이 소실됩니다 권한 부여 @@ -1524,6 +1536,7 @@ 다른 금액 입력 %s로 네트워크 수수료를 지불하기 위해, Nova는 계정의 최소 %s 잔액을 유지하기 위해 자동으로 %s를 %s로 교환합니다. 블록체인이 모든 거래를 처리하고 검증하기 위해 부과하는 네트워크 수수료입니다. 네트워크 상태나 거래 속도에 따라 다를 수 있습니다. + %s 교환을 위한 네트워크 선택 풀에 교환할 수 있는 유동성이 충분하지 않습니다 가격 차이는 두 자산 간의 가격 차이를 나타냅니다. 암호화폐 교환 시, 가격 차이는 일반적으로 교환하는 자산의 가격과 교환받는 자산의 가격 간의 차이를 의미합니다. 가격 차이 @@ -1616,6 +1629,7 @@ 구매 방법 받기 %s 수령 + 이 주소로 %1$s 토큰과 %2$s 네트워크에서의 토큰만 보내세요, 그렇지 않으면 자금을 잃을 수 있습니다. 보내기 스왑 자산 diff --git a/common/src/main/res/values-pl/strings.xml b/common/src/main/res/values-pl/strings.xml index 641ea2cf6e..0721f8cab6 100644 --- a/common/src/main/res/values-pl/strings.xml +++ b/common/src/main/res/values-pl/strings.xml @@ -173,6 +173,10 @@ Osiągnąłeś limit %s dodanych proxy w %s. Usuń proxy, aby dodać nowe. Osiągnięto maksymalną liczbę proxy Dodane niestandardowe sieci\npojawią się tutaj + Kolorowe + Wygląd + ikony tokenów + Białe Wprowadzony adres kontraktu jest obecny w Nova jako token %s. Wprowadzony adres kontraktu jest obecny w Nova jako token %s. Czy na pewno chcesz go zmodyfikować? Ten token już istnieje @@ -185,6 +189,8 @@ Wprowadź adres kontraktu Wprowadź liczbę miejsc dziesiętnych Wprowadź symbol + Sieci + Tokeny Dodaj token Adres kontraktu Liczba miejsc dziesiętnych @@ -207,6 +213,7 @@ Ledger nie obsługuje tego tokena Wyszukaj według sieci lub tokena Nie znaleziono sieci ani tokenów z\npodaną nazwą + Wyszukaj według tokena Twoje portfele Nie masz tokenów do wysłania.\nKup lub odbierz tokeny na swoje\nkonto. Token do zapłaty @@ -235,6 +242,7 @@ Kopia zapasowa Autoryzacja biometryczna Zakup zainicjowany! Proszę czekać do 60 minut. Możesz śledzić status na e-mail. + Wybierz sieć do kupna %s Aby kontynuować zakup, zostaniesz przekierowany z aplikacji Nova Wallet na %s Kontynuować w przeglądarce? Automatyczne równoważenie węzłów @@ -982,6 +990,8 @@ Włączając powiadomienia push, zgadzasz się na nasze %s i %s Spróbuj ponownie później, otwierając ustawienia powiadomień w zakładce Ustawienia Nie przegap niczego! + Wybierz sieć do otrzymania %s + Skopiuj adres Wklej json lub załaduj plik… Załaduj plik Przywróć JSON @@ -1138,9 +1148,11 @@ Niedostępne trasy Nova wymaga włączenia lokalizacji, aby móc przeprowadzać skanowanie Bluetooth w celu znalezienia urządzenia Ledger Proszę włączyć geolokalizację w ustawieniach urządzenia + Wybierz sieć Wybierz trasy dla %d z %d Adres lub w3n + Wybierz sieć do wysyłki %s Odbiorca jest kontem systemowym. Nie jest kontrolowany przez żadną firmę ani osobę prywatną.\nCzy na pewno chcesz wykonać ten transfer? Tokeny zostaną utracone Nadaj uprawnienia @@ -1554,6 +1566,7 @@ Wprowadź inną kwotę Aby zapłacić opłatę sieciową za pomocą %s, Nova automatycznie zamieni %s na %s, aby utrzymać minimalne saldo %s na twoim koncie. Opłaty sieciowe pobierane przez blockchain za przetwarzanie i walidację dowolnych transakcji. Mogą się różnić w zależności od warunków sieci lub szybkości transakcji. + Wybierz sieć do zamiany %s W puli brakuje płynności do wymiany Różnica cenowa odnosi się do różnicy w cenie pomiędzy dwoma różnymi aktywami. Podczas wymiany w kryptowalutach, różnica cenowa zazwyczaj oznacza różnicę między ceną aktywa, którą otrzymujesz, a ceną aktywa, którą płacisz. Różnica cen @@ -1646,6 +1659,7 @@ Kup za Otrzymaj Otrzymaj %s + Wysyłaj tylko token %1$s i tokeny w sieci %2$s na ten adres, w przeciwnym razie możesz stracić swoje środki Wyślij Wymiana Aktywa diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index f42fb5c89f..a6aab8484e 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -173,6 +173,10 @@ Você alcançou o limite de %s proxies adicionados em %s. Remova proxies para adicionar novos. Número máximo de proxies foi alcançado Redes personalizadas adicionadas\naparecerão aqui + Colorido + Aparência + ícones de token + Branco O endereço do contrato inserido já está presente na Nova como um token %s. O endereço do contrato inserido já está presente na Nova como um token %s. Tem certeza de que deseja modificá-lo? Este token já existe @@ -185,6 +189,8 @@ Insira o endereço do contrato Insira os decimais Insira o símbolo + Redes + Tokens Adicionar token Endereço do contrato Decimais @@ -207,6 +213,7 @@ Ledger não suporta este token Buscar por rede ou token Nenhuma rede ou token com o nome inserido foi encontrado + Pesquisar por token Suas carteiras Você não tem tokens para enviar. Compre ou Receba tokens na sua conta. Token para pagamento @@ -235,6 +242,7 @@ Backup Autenticação biométrica Compra iniciada! Por favor, aguarde até 60 minutos. Você pode rastrear o status pelo email. + Selecione a rede para comprar %s Para continuar a compra você será redirecionado do aplicativo Nova Wallet para %s Continuar no navegador? Equilibrar nós automaticamente @@ -968,6 +976,8 @@ Ao ativar as notificações push, você concorda com nossos %s e %s Por favor, tente novamente mais tarde acessando as configurações de notificação na aba Configurações Não perca nada! + Selecione a rede para receber %s + Copiar Endereço Cole o json ou faça upload do arquivo… Fazer upload de arquivo Restaurar JSON @@ -1122,9 +1132,11 @@ Faixas indisponíveis Nova precisa que a localização seja ativada para poder realizar a varredura Bluetooth para encontrar seu dispositivo Ledger Por favor, ative a localização geográfica nas configurações do dispositivo + Selecionar rede Selecione faixas para %d de %d Endereço ou w3n + Selecione a rede para enviar %s O destinatário é uma conta do sistema. Não é controlado por nenhuma empresa ou indivíduo.\nVocê ainda deseja realizar esta transferência? Os tokens serão perdidos Dar autoridade para @@ -1534,6 +1546,7 @@ Digite outra quantidade Para pagar a taxa de rede com %s, a Nova fará automaticamente a troca de %s por %s para manter o saldo mínimo de %s na sua conta. Uma taxa de rede cobrada pela blockchain para processar e validar quaisquer transações. Pode variar dependendo das condições da rede ou velocidade da transação. + Selecione a rede para trocar %s O pool não tem liquidez suficiente para a troca Diferença de preço refere-se à diferença de preço entre dois ativos diferentes. Ao fazer uma troca em cripto, a diferença de preço geralmente é entre o preço do ativo que você está trocando e o preço do ativo com o qual você está trocando. Diferença de preço @@ -1626,6 +1639,7 @@ Comprar com Receber Receber %s + Envie somente o token %1$s e tokens na rede %2$s para este endereço, ou você pode perder seus fundos Enviar Trocar Ativos diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index cd1f87d762..33dc180a91 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -173,6 +173,10 @@ Вы достигли лимита добавленных прокси (%s) в %s . Удалите прокси, чтобы добавить новые. Достигнуто максимальное количество прокси Добавленные пользовательские сети\nпоявятся здесь + Цветной + Внешний вид + иконки токенов + Белый Введенный адрес контракта уже добавлен в Nova как токен %s. Введенный адрес контракта присутствует в Nova как токен %s. Вы уверены, что хотите изменить его? Этот токен уже добавлен @@ -185,6 +189,8 @@ Введите адрес контракта Введите число десятичных знаков Введите символ + Сети + Токены Добавить токен Адрес контракта Число десятичных знаков @@ -207,6 +213,7 @@ Ledger не поддерживает этот токен Поиск по названию сети или токена Токены и сети с указанным именем\nне найдены + Поиск по токену Ваши кошельки У вас нет токенов для отправки.\nКупите или получите токены\nна свой аккаунт. Токен для оплаты @@ -235,6 +242,7 @@ Бэкап Биометрия Покупка совершена! Ожидайте до 60 минут. Вы можете отслеживать статус по электронной почте. + Выберите сеть для покупки %s Для продолжения покупки вы будете перенаправлены из приложения Nova Wallet на сайт %s Продолжить в браузере? Автоматическая балансировка нод @@ -982,6 +990,8 @@ Включая push-уведомления, вы соглашаетесь с нашими %s и %s Повторите попытку позже, открыв настройки уведомлений на вкладке «Настройки» Не пропустите ничего! + Выберите сеть для получения %s + Скопировать адрес Вставьте json строку или загрузите файл… Загрузите файл JSON для восстановления @@ -1138,9 +1148,11 @@ Недоступные треки Nova нуждается в включении местоположения, чтобы иметь возможность выполнять сканирование Bluetooth для поиска вашего устройства Ledger Пожалуйста, включите геолокацию в настройках устройства + Выберите сеть Выберите треки для %d из %d Адрес или w3n + Выберите сеть для отправки %s Получатель является системным аккаунтом. Этот аккаунт не контролируется какой-либо компанией или частным лицом. \nВы уверены, что все еще хотите выполнить данный перевод? Токены будут потеряны Выдать полномочия аккаунту @@ -1554,6 +1566,7 @@ Введите другую сумму Чтобы оплатить комиссию сети с помощью %s, Nova автоматически обменяет %s на %s, чтобы поддерживать минимальный %s баланс вашей учетной записи. Комиссия сети, взимается блокчейном за обработку и проверку транзакций. Может варьироваться в зависимости от условий сети или скорости транзакции. + Выберите сеть для обмена %s В пуле недостаточно ликвидности для обмена Разница в цене представляет собой разницу между двумя различными активами. При обмене криптовалюты под разницей в цене обычно имеется ввиду разница между ценой актива, которую вы получаете и ценой актива, которую вы платите. Разница в цене @@ -1646,6 +1659,7 @@ Купить с Получить Получить %s + Отправляйте только токен %1$s и токены в сети %2$s на этот адрес, иначе вы можете потерять свои средства Перевести Обменять Активы diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml index aa73d206fc..1775904096 100644 --- a/common/src/main/res/values-tr/strings.xml +++ b/common/src/main/res/values-tr/strings.xml @@ -173,6 +173,10 @@ %s\'da eklenen maksimum proxy (%s) limitine ulaştınız. Yeni eklemeler yapabilmek için proxy\'leri kaldırın. Maksimum proxy sayısına ulaşıldı Eklenen özel ağlar\nburada görünecek + Renkli + Görünüm + token simgeleri + Beyaz Girilen sözleşme adresi Nova\'da zaten %s tokeni olarak bulunmaktadır. Girilen sözleşme adresi Nova\'da zaten %s tokeni olarak bulunmaktadır. Değiştirmek istediğinize emin misiniz? Bu token zaten var @@ -185,6 +189,8 @@ Sözleşme adresi girin Ondalık sayısını girin Sembol girin + Ağlar + Tokenler Token ekle Sözleşme adresi Ondalıklar @@ -207,6 +213,7 @@ Ledger bu token\'i desteklemiyor Ağ veya token adına göre ara Girilen adla herhangi bir ağ veya token bulunamadı + Tokene göre ara Cüzdanlarınız Göndermek için token\'ınız yok.\nHesabınıza token alın veya alın. Ödeme yapılacak Token @@ -235,6 +242,7 @@ Yedekleme Biyometrik Doğrulama Satın alma başlatıldı! Lütfen 60 dakika kadar bekleyin. Durumu e-postadan takip edebilirsiniz. + Satın alma için ağ seçin %s Satın alma işlemine devam etmek için Nova Wallet uygulamasından %s\'a yönlendirileceksiniz Tarayıcıda devam et? Otomatik denge düğümleri @@ -968,6 +976,8 @@ Bildirimleri etkinleştirmekle, %s ve %s kabul etmiş olursunuz Lütfen, Ayarlar sekmesinden bildirim ayarlarına erişerek daha sonra tekrar deneyin Hiçbir şeyi kaçırmayın! + Alma için ağ seçin %s + Adresi Kopyala Json yapıştırın ya da dosya yükleyin... Dosya yükle JSON Kurtar @@ -1122,9 +1132,11 @@ Ulaşılamayan yollar Nova\'nın Bluetooth taraması yaparak Ledger cihazınızı bulabilmesi için konumun etkinleştirilmesi gerekiyor Lütfen cihaz ayarlarında coğrafi konumu etkinleştirin + Ağ seç İçin şarkıları seçin %d / %d Adres veya w3n + Gönderme için ağ seçin %s Alıcı, bir sistem hesabıdır. Bir şirket veya birey tarafından kontrol edilmez.\nBu transferi gerçekleştirmek istediğinize emin misiniz? Token\'lar kaybolacak Yetki ver @@ -1534,6 +1546,7 @@ Başka bir miktar girin %s ile ağ ücreti ödemek için, Nova otomatik olarak hesabınızın minimum %s bakiyesini korumak için %s\'i %s\'ye dönüştürecektir. Herhangi bir işlemi işlemek ve doğrulamak için blockchain tarafından alınan ağ ücretleri. Ağ koşullarına veya işlem hızına bağlı olarak değişebilir. + Takas için ağ seçin %s Havuzda takas için yeterli likidite yok Fiyat farkı, iki farklı varlık arasındaki fiyat farkını ifade eder. Kripto takas yaparken, fiyat farkı genellikle takas ettiğiniz varlığın fiyatı ile takas ettiğiniz varlığın fiyatı arasındaki farktır. Fiyat farkı @@ -1626,6 +1639,7 @@ ile Satın al Al %s al + Bu adrese yalnızca %1$s token ve %2$s ağındaki tokenleri gönderin, aksi takdirde paranızı kaybedebilirsiniz Gönder Takas et Varlıklar diff --git a/common/src/main/res/values-vi/strings.xml b/common/src/main/res/values-vi/strings.xml index 6ebc3b7831..7379a43bef 100644 --- a/common/src/main/res/values-vi/strings.xml +++ b/common/src/main/res/values-vi/strings.xml @@ -173,6 +173,10 @@ Bạn đã đạt đến giới hạn %s proxy được thêm vào %s. Xóa proxy để thêm proxy mới. Đã đạt đến số lượng proxy tối đa Các mạng tùy chỉnh đã thêm\nsẽ xuất hiện ở đây + Đã tô màu + Giao diện + biểu tượng token + Trắng Địa chỉ hợp đồng đã nhập có sẵn trong Nova là token %s. Địa chỉ hợp đồng đã nhập có sẵn trong Nova là token %s. Bạn có chắc chắn muốn thay đổi không? Token này đã tồn tại @@ -185,6 +189,8 @@ Nhập địa chỉ hợp đồng Nhập số thập phân Nhập ký hiệu + Mạng + Token Thêm token Địa chỉ hợp đồng Số thập phân @@ -207,6 +213,7 @@ Ledger không hỗ trợ token này Tìm kiếm theo mạng hoặc token Không tìm thấy mạng hoặc token nào với\ntên đã nhập + Tìm kiếm theo token Ví của bạn Bạn không có token để gửi.\nMua hoặc Nhận token vào\ntài khoản của bạn. Token để thanh toán @@ -235,6 +242,7 @@ Sao lưu Xác thực sinh trắc học Mua hàng đã được khởi tạo! Vui lòng đợi tối đa 60 phút. Bạn có thể theo dõi trạng thái trong email. + Chọn mạng để mua %s Để tiếp tục mua hàng, bạn sẽ được chuyển hướng từ ứng dụng Nova Wallet tới %s Tiếp tục trong trình duyệt? Tự động cân bằng nodes @@ -961,6 +969,8 @@ Bằng việc bật thông báo đẩy, bạn đồng ý với %s và %s của chúng tôi Vui lòng thử lại sau bằng cách truy cập cài đặt thông báo từ tab Cài đặt Đừng bỏ lỡ điều gì! + Chọn mạng để nhận %s + Sao chép Địa chỉ Dán mã JSON hoặc tải lên tệp... Tải lên tệp Khôi phục JSON @@ -1114,9 +1124,11 @@ Track không có sẵn Nova cần bật định vị để có thể thực hiện quét Bluetooth để tìm thiết bị Ledger của bạn Vui lòng bật định vị địa lý trong cài đặt thiết bị + Chọn mạng Chọn track cho %d trong %d Địa chỉ hoặc w3n + Chọn mạng để gửi %s Người nhận là một tài khoản hệ thống. Nó không được kiểm soát bởi bất kỳ công ty hoặc cá nhân nào.\nBạn có chắc chắn vẫn muốn thực hiện chuyển khoản này không? Token sẽ bị mất Cấp quyền cho @@ -1524,6 +1536,7 @@ Nhập số tiền khác Để thanh toán phí mạng bằng %s, Nova sẽ tự động hoán đổi %s thành %s để duy trì số dư tối thiểu %s trong tài khoản của bạn. Một khoản phí mạng được tính bởi blockchain để xử lý và xác nhận bất kỳ giao dịch nào. Có thể thay đổi tùy thuộc vào điều kiện mạng hoặc tốc độ giao dịch. + Chọn mạng để hoán đổi %s Pool không có đủ thanh khoản để thực hiện hoán đổi Sự khác biệt về giá đề cập đến sự chênh lệch giá giữa hai tài sản khác nhau. Khi thực hiện hoán đổi trong tiền điện tử, sự khác biệt về giá thường là sự chênh lệch giữa giá của tài sản mà bạn đang hoán đổi và giá của tài sản mà bạn đang hoán đổi. Sự khác biệt về giá @@ -1616,6 +1629,7 @@ Mua với Nhận Nhận %s + Chỉ gửi token %1$s và các token trong mạng %2$s tới địa chỉ này, nếu không bạn có thể mất tiền Gửi Hoán đổi Tài sản diff --git a/common/src/main/res/values-zh-rCN/strings.xml b/common/src/main/res/values-zh-rCN/strings.xml index cc1c960d19..8e720b42a4 100644 --- a/common/src/main/res/values-zh-rCN/strings.xml +++ b/common/src/main/res/values-zh-rCN/strings.xml @@ -173,6 +173,10 @@ 您已达到在%s中添加的代理的上限%s。移除代理以添加新代理。 已达到代理最大数量 添加的自定义网络会显示在这里 + 彩色图标 + 外观 + 代币图标 + 白色图标 输入的合约地址已作为%s代币存在于Nova中。 输入的合约地址已作为%s代币存在于Nova中。您确定要修改它吗? 该代币已存在 @@ -185,6 +189,8 @@ 输入合约地址 输入小数 输入符号 + 网络 + 代币 添加代币 合约地址 小数点 @@ -207,6 +213,7 @@ Ledger 不支持此代币 按网络或代币搜索 没有找到输入名称的网络或代币 + 按代币搜索 你的钱包 你没有要发送的代币。购买或接收代币到你的账户。 要支付的代币 @@ -235,6 +242,7 @@ 备份 生物认证 购买已启动!请等待最多 60 分钟。你可以在电子邮件上追踪状态。 + 选择购买%s的网络 为了继续购买,你将从 Nova Wallet 应用被重定向到 %s 在浏览器中继续? 自动平衡节点 @@ -961,6 +969,8 @@ 启用推送通知,即表示您同意我们的 %s 和 %s 请稍后再试,通过设置选项卡中的通知设置访问 不要错过任何事情! + 选择接收%s的网络 + 复制地址 粘贴 json 字符串或上传文件… 上传文件 恢复 JSON @@ -1114,9 +1124,11 @@ 不可用的轨道 Nova需要启用地理位置,以便能够进行蓝牙扫描查找您的Ledger设备 请在设备设置中启用地理位置 + 选择网络 选择曲目 %d / %d 地址或w3n + 选择发送%s的网络 收件人是一个系统账户。它不受任何公司或个人控制。您确定仍然要执行此转账吗? 代币将会丢失 授权给 @@ -1524,6 +1536,7 @@ 输入其他金额 为了支付网络费用,Nova将自动将%s兑换为%s,以保持您账户的最低%s余额。 区块链收取的网络费用,用于处理和验证任何交易。可能会根据网络状况或交易速度而变化。 + 选择交换%s的网络 池子没有足够的流动性进行交换 价格差异指的是两种不同资产之间的价格差。在加密货币交换中,价格差异通常是您要交换的资产的价格与您交换的资产的价格之间的差异。 价格差异 @@ -1616,6 +1629,7 @@ 用...购买 接收 接收 %s + 仅将%1$s代币和%2$s网络中的代币发送到此地址,否则您可能会失去资金 发送 兑换 资产 diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index 65457f8835..409cd3792e 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -3,6 +3,7 @@ #eeeeee #FFFFFF + #101014 #05081C @@ -13,8 +14,10 @@ #E0FFFFFF - #05081C + #000000 #7AFFFFFF + #8F000000 + #05081C #E0FFFFFF #A3FFFFFF #52FFFFFF @@ -61,6 +64,7 @@ #08090E #181920 #1A999EC7 + #3D999EC7 #5205081C #291F78FF diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 862440d046..b157bffc3d 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,6 +1,26 @@ + Select network + + Search by token + + Copy Address + Send only %1$s token and tokens in %2$s network to this address, or you might lose your funds + + token icons + Appearance + White + Colored + + Select network for buying %s + Select network for receiving %s + Select network for sending %s + Select network for swaping %s + + Tokens + Networks + Wiki & Help Center Get support via Email diff --git a/common/src/main/res/values/styles.xml b/common/src/main/res/values/styles.xml index c9f36fd1b8..63d87457d5 100644 --- a/common/src/main/res/values/styles.xml +++ b/common/src/main/res/values/styles.xml @@ -457,10 +457,14 @@ @color/selector_button_background_secondary - + + diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/chain/ChainUi.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/chain/ChainUi.kt index a5f61e4aa0..dc883f4cc6 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/chain/ChainUi.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/chain/ChainUi.kt @@ -6,8 +6,13 @@ import android.widget.ImageView import coil.ImageLoader import coil.load import coil.request.ImageRequest +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.presentation.fallbackIcon +import io.novafoundation.nova.common.presentation.getAssetIconOrFallback import io.novafoundation.nova.common.utils.images.Icon import io.novafoundation.nova.common.utils.images.asIcon +import io.novafoundation.nova.common.utils.images.setIcon import io.novafoundation.nova.feature_account_api.R import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -39,16 +44,12 @@ fun ImageLoader.loadChainIconToTarget(icon: String?, context: Context, target: ( this.enqueue(request) } -fun ImageView.loadTokenIcon(icon: String?, imageLoader: ImageLoader) { - load(icon, imageLoader) { +fun ImageView.setTokenIcon(icon: Icon, imageLoader: ImageLoader) { + setIcon(icon, imageLoader) { fallback(ASSET_ICON_PLACEHOLDER) } } -fun Chain.Asset.icon(): Icon { - return iconUrl?.asIcon() ?: ASSET_ICON_PLACEHOLDER.asIcon() -} - fun Chain.iconOrFallback(): Icon { return icon?.asIcon() ?: chainIconFallback() } @@ -60,3 +61,11 @@ fun String?.asIconOrFallback(): Icon { fun chainIconFallback(): Icon { return R.drawable.ic_fallback_network_icon.asIcon() } + +fun AssetIconProvider.getAssetIconOrFallback(asset: Chain.Asset, fallbackIcon: Icon = AssetIconProvider.fallbackIcon): Icon { + return this.getAssetIconOrFallback(asset.icon, fallbackIcon) +} + +fun AssetIconProvider.getAssetIconOrFallback(asset: Chain.Asset, iconMode: AssetIconMode, fallbackIcon: Icon = AssetIconProvider.fallbackIcon): Icon { + return this.getAssetIconOrFallback(asset.icon, iconMode, fallbackIcon) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureComponent.kt index a401b17e45..059d7d2f94 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureComponent.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureComponent.kt @@ -9,17 +9,20 @@ import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectAddress.SelectAddressCommunicator import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.detail.di.BalanceDetailComponent -import io.novafoundation.nova.feature_assets.presentation.balance.filters.di.AssetFiltersComponent import io.novafoundation.nova.feature_assets.presentation.balance.list.di.BalanceListComponent import io.novafoundation.nova.feature_assets.presentation.balance.list.view.GoToNftsView import io.novafoundation.nova.feature_assets.presentation.balance.search.di.AssetSearchComponent -import io.novafoundation.nova.feature_assets.presentation.buy.flow.di.AssetBuyFlowComponent +import io.novafoundation.nova.feature_assets.presentation.buy.flow.asset.di.AssetBuyFlowComponent +import io.novafoundation.nova.feature_assets.presentation.buy.flow.network.di.NetworkBuyFlowComponent import io.novafoundation.nova.feature_assets.presentation.receive.di.ReceiveComponent -import io.novafoundation.nova.feature_assets.presentation.receive.flow.di.AssetReceiveFlowComponent +import io.novafoundation.nova.feature_assets.presentation.receive.flow.asset.di.AssetReceiveFlowComponent +import io.novafoundation.nova.feature_assets.presentation.receive.flow.network.di.NetworkReceiveFlowComponent import io.novafoundation.nova.feature_assets.presentation.send.amount.di.SelectSendComponent import io.novafoundation.nova.feature_assets.presentation.send.confirm.di.ConfirmSendComponent -import io.novafoundation.nova.feature_assets.presentation.send.flow.di.AssetSendFlowComponent -import io.novafoundation.nova.feature_assets.presentation.swap.di.AssetSwapFlowComponent +import io.novafoundation.nova.feature_assets.presentation.send.flow.asset.di.AssetSendFlowComponent +import io.novafoundation.nova.feature_assets.presentation.send.flow.network.di.NetworkSendFlowComponent +import io.novafoundation.nova.feature_assets.presentation.swap.asset.di.AssetSwapFlowComponent +import io.novafoundation.nova.feature_assets.presentation.swap.network.di.NetworkSwapFlowComponent import io.novafoundation.nova.feature_assets.presentation.tokens.add.enterInfo.di.AddTokenEnterInfoComponent import io.novafoundation.nova.feature_assets.presentation.tokens.add.selectChain.di.AddTokenSelectChainComponent import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.di.ManageChainTokensComponent @@ -74,8 +77,6 @@ interface AssetsFeatureComponent : AssetsFeatureApi { fun receiveComponentFactory(): ReceiveComponent.Factory - fun assetFiltersComponentFactory(): AssetFiltersComponent.Factory - fun assetSearchComponentFactory(): AssetSearchComponent.Factory fun manageTokensComponentFactory(): ManageTokensComponent.Factory @@ -94,6 +95,14 @@ interface AssetsFeatureComponent : AssetsFeatureApi { fun buyFlowComponent(): AssetBuyFlowComponent.Factory + fun networkBuyFlowComponent(): NetworkBuyFlowComponent.Factory + + fun networkReceiveFlowComponent(): NetworkReceiveFlowComponent.Factory + + fun networkSendFlowComponent(): NetworkSendFlowComponent.Factory + + fun networkSwapFlowComponent(): NetworkSwapFlowComponent.Factory + fun inject(view: GoToNftsView) @Component.Factory diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt index 1ce1f7e787..86d943ecbe 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt @@ -10,12 +10,16 @@ import io.novafoundation.nova.common.data.network.AppLinksProvider import io.novafoundation.nova.common.data.network.HttpExceptionHandler import io.novafoundation.nova.common.data.network.NetworkApiCreator import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser +import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor import io.novafoundation.nova.common.interfaces.FileProvider import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.mixin.hints.ResourcesHintsMixinFactory +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ClipboardManager import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.QrCodeGenerator @@ -87,6 +91,88 @@ import javax.inject.Named interface AssetsFeatureDependencies { + val assetsSourceRegistry: AssetSourceRegistry + + val addressInputMixinFactory: AddressInputMixinFactory + + val multiChainQrSharingFactory: MultiChainQrSharingFactory + + val walletUiUseCase: WalletUiUseCase + + val computationalCache: ComputationalCache + + val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory + + val crossChainTraRepository: CrossChainTransfersRepository + + val crossChainWeigher: CrossChainWeigher + + val crossChainTransactor: CrossChainTransactor + + val resourcesHintsMixinFactory: ResourcesHintsMixinFactory + + val parachainInfoRepository: ParachainInfoRepository + + val watchOnlyMissingKeysPresenter: WatchOnlyMissingKeysPresenter + + val balanceLocksRepository: BalanceLocksRepository + + val chainAssetRepository: ChainAssetRepository + + val erc20Standard: Erc20Standard + + val externalBalanceRepository: ExternalBalanceRepository + + val pooledBalanceUpdaterFactory: PooledBalanceUpdaterFactory + + val paymentUpdaterFactory: PaymentUpdaterFactory + + val locksUpdaterFactory: BalanceLocksUpdaterFactory + + val accountUpdateScope: AccountUpdateScope + + val storageSharedRequestBuilderFactory: StorageSharedRequestsBuilderFactory + + val poolDisplayUseCase: PoolDisplayUseCase + + val poolAccountDerivation: PoolAccountDerivation + + val operationDao: OperationDao + + val coinPriceRepository: CoinPriceRepository + + val swapSettingsStateProvider: SwapSettingsStateProvider + + val swapService: SwapService + + val swapAvailabilityInteractor: SwapAvailabilityInteractor + + val bannerVisibilityRepository: BannerVisibilityRepository + + val buyMixinFactory: BuyMixin.Factory + + val buyMixinUi: BuyMixinUi + + val crossChainTransfersUseCase: CrossChainTransfersUseCase + + val arbitraryTokenUseCase: ArbitraryTokenUseCase + + val swapRateFormatter: SwapRateFormatter + + val bottomSheetLauncher: DescriptionBottomSheetLauncher + + val selectAddressMixinFactory: SelectAddressMixin.Factory + + val chainStateRepository: ChainStateRepository + + val holdsRepository: BalanceHoldsRepository + + val holdsDao: HoldsDao + + val coinGeckoLinkParser: CoinGeckoLinkParser + + val assetIconProvider: AssetIconProvider + fun web3NamesInteractor(): Web3NamesInteractor fun contributionsInteractor(): ContributionsInteractor @@ -169,85 +255,13 @@ interface AssetsFeatureDependencies { fun coingeckoApi(): CoingeckoApi + fun assetsViewModeRepository(): AssetsViewModeRepository + fun walletConnectSessionsUseCase(): WalletConnectSessionsUseCase - val assetsSourceRegistry: AssetSourceRegistry + fun assetsIconModeRepository(): AssetsIconModeRepository fun nftRepository(): NftRepository - val addressInputMixinFactory: AddressInputMixinFactory - - val multiChainQrSharingFactory: MultiChainQrSharingFactory - - val walletUiUseCase: WalletUiUseCase - - val computationalCache: ComputationalCache - - val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory - - val crossChainTraRepository: CrossChainTransfersRepository - val crossChainWeigher: CrossChainWeigher - val crossChainTransactor: CrossChainTransactor - - val resourcesHintsMixinFactory: ResourcesHintsMixinFactory - - val parachainInfoRepository: ParachainInfoRepository - - val watchOnlyMissingKeysPresenter: WatchOnlyMissingKeysPresenter - - val balanceLocksRepository: BalanceLocksRepository - - val chainAssetRepository: ChainAssetRepository - - val erc20Standard: Erc20Standard - - val externalBalanceRepository: ExternalBalanceRepository - - val pooledBalanceUpdaterFactory: PooledBalanceUpdaterFactory - - val paymentUpdaterFactory: PaymentUpdaterFactory - - val locksUpdaterFactory: BalanceLocksUpdaterFactory - - val accountUpdateScope: AccountUpdateScope - - val storageSharedRequestBuilderFactory: StorageSharedRequestsBuilderFactory - - val poolDisplayUseCase: PoolDisplayUseCase - - val poolAccountDerivation: PoolAccountDerivation - - val operationDao: OperationDao - - val coinPriceRepository: CoinPriceRepository - - val swapSettingsStateProvider: SwapSettingsStateProvider - - val swapService: SwapService - - val swapAvailabilityInteractor: SwapAvailabilityInteractor - - val bannerVisibilityRepository: BannerVisibilityRepository - - val buyMixinFactory: BuyMixin.Factory - - val buyMixinUi: BuyMixinUi - - val crossChainTransfersUseCase: CrossChainTransfersUseCase - - val arbitraryTokenUseCase: ArbitraryTokenUseCase - - val swapRateFormatter: SwapRateFormatter - - val bottomSheetLauncher: DescriptionBottomSheetLauncher - - val selectAddressMixinFactory: SelectAddressMixin.Factory - - val chainStateRepository: ChainStateRepository - - val holdsRepository: BalanceHoldsRepository - - val holdsDao: HoldsDao - - val coinGeckoLinkParser: CoinGeckoLinkParser + fun assetViewModeInteractor(): AssetViewModeInteractor } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureModule.kt index 3c99128536..1e7a4e6eb6 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureModule.kt @@ -3,9 +3,11 @@ package io.novafoundation.nova.feature_assets.di import dagger.Module import dagger.Provides import io.novafoundation.nova.common.data.memory.ComputationalCache +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository @@ -23,20 +25,31 @@ import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.WalletInteractorImpl import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor import io.novafoundation.nova.feature_assets.domain.assets.RealExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchUseCase +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetViewModeAssetSearchInteractorFactory +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.balance.common.ExpandableAssetsMixinFactory +import io.novafoundation.nova.feature_assets.presentation.swap.executor.InitialSwapFlowExecutor +import io.novafoundation.nova.feature_assets.presentation.swap.executor.SwapFlowExecutorFactory import io.novafoundation.nova.feature_assets.presentation.transaction.filter.HistoryFiltersProviderFactory +import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository import io.novafoundation.nova.feature_staking_api.data.network.blockhain.updaters.PooledBalanceUpdaterFactory import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService +import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.BalanceLocksUpdaterFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.PaymentUpdaterFactory import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CoinPriceRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.model.RealAmountFormatter import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -52,13 +65,28 @@ class AssetsFeatureModule { @Provides @FeatureScope - fun provideSearchInteractor( + fun provideAssetSearchUseCase( walletRepository: WalletRepository, accountRepository: AccountRepository, chainRegistry: ChainRegistry, - assetSourceRegistry: AssetSourceRegistry, swapService: SwapService - ) = AssetSearchInteractor(walletRepository, accountRepository, chainRegistry, assetSourceRegistry, swapService) + ) = AssetSearchUseCase(walletRepository, accountRepository, chainRegistry, swapService) + + @Provides + @FeatureScope + fun provideSearchInteractorFactory( + assetViewModeRepository: AssetsViewModeRepository, + assetSearchUseCase: AssetSearchUseCase, + chainRegistry: ChainRegistry + ): AssetSearchInteractorFactory = AssetViewModeAssetSearchInteractorFactory(assetViewModeRepository, assetSearchUseCase, chainRegistry) + + @Provides + @FeatureScope + fun provideAssetNetworksInteractor( + chainRegistry: ChainRegistry, + swapService: SwapService, + assetSearchUseCase: AssetSearchUseCase + ) = AssetNetworksInteractor(chainRegistry, swapService, assetSearchUseCase) @Provides @FeatureScope @@ -141,4 +169,44 @@ class AssetsFeatureModule { coinPriceRepository = coinPriceRepository, poolAccountDerivation = poolAccountDerivation ) + + @Provides + @FeatureScope + fun provideInitialSwapFlowExecutor( + assetsRouter: AssetsRouter + ): InitialSwapFlowExecutor { + return InitialSwapFlowExecutor(assetsRouter) + } + + @Provides + @FeatureScope + fun provideSwapExecutor( + initialSwapFlowExecutor: InitialSwapFlowExecutor, + assetsRouter: AssetsRouter, + swapSettingsStateProvider: SwapSettingsStateProvider + ): SwapFlowExecutorFactory { + return SwapFlowExecutorFactory(initialSwapFlowExecutor, assetsRouter, swapSettingsStateProvider) + } + + @Provides + @FeatureScope + fun provideAmountFormatter(resourceManager: ResourceManager): AmountFormatter { + return RealAmountFormatter(resourceManager) + } + + @Provides + @FeatureScope + fun provideExpandableAssetsMixinFactory( + assetIconProvider: AssetIconProvider, + currencyInteractor: CurrencyInteractor, + assetsViewModeRepository: AssetsViewModeRepository, + amountFormatter: AmountFormatter + ): ExpandableAssetsMixinFactory { + return ExpandableAssetsMixinFactory( + assetIconProvider, + currencyInteractor, + assetsViewModeRepository, + amountFormatter + ) + } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/ManageTokensCommonModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/ManageTokensCommonModule.kt index 00bfa90407..d4244715e4 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/ManageTokensCommonModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/ManageTokensCommonModule.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_assets.di.modules import dagger.Module import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_assets.domain.tokens.AssetsDataCleaner import io.novafoundation.nova.feature_assets.domain.tokens.RealAssetsDataCleaner @@ -21,8 +22,9 @@ class ManageTokensCommonModule { @Provides @FeatureScope fun provideMultiChainTokenUiMapper( + assetIconProvider: AssetIconProvider, resourceManager: ResourceManager - ) = MultiChainTokenMapper(resourceManager) + ) = MultiChainTokenMapper(assetIconProvider, resourceManager) @Provides @FeatureScope diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/WalletInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/WalletInteractor.kt index 3cf88388af..a4f6aa08c6 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/WalletInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/WalletInteractor.kt @@ -3,8 +3,10 @@ package io.novafoundation.nova.feature_assets.domain import io.novafoundation.nova.common.data.model.DataPage import io.novafoundation.nova.common.data.model.PageOffset import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup import io.novafoundation.nova.feature_currency_api.domain.model.Currency import io.novafoundation.nova.feature_nft_api.data.repository.NftSyncTrigger import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter @@ -57,8 +59,13 @@ interface WalletInteractor { filters: Set ): Result> - suspend fun groupAssets( + suspend fun groupAssetsByNetwork( assets: List, externalBalances: List - ): Map> + ): Map> + + suspend fun groupAssetsByToken( + assets: List, + externalBalances: List + ): Map> } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/WalletInteractorImpl.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/WalletInteractorImpl.kt index 2113a506ce..58c03b6064 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/WalletInteractorImpl.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/WalletInteractorImpl.kt @@ -8,9 +8,12 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.feature_assets.data.repository.TransactionHistoryRepository import io.novafoundation.nova.feature_assets.data.repository.assetFilters.AssetFiltersRepository -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByNetwork +import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByToken import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository import io.novafoundation.nova.feature_currency_api.domain.model.Currency import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository @@ -168,12 +171,21 @@ class WalletInteractorImpl( } } - override suspend fun groupAssets( + override suspend fun groupAssetsByNetwork( assets: List, externalBalances: List - ): Map> { + ): Map> { val chains = chainRegistry.enabledChainByIdFlow().first() return groupAndSortAssetsByNetwork(assets, externalBalances.aggregatedBalanceByAsset(), chains) } + + override suspend fun groupAssetsByToken( + assets: List, + externalBalances: List + ): Map> { + val chains = chainRegistry.enabledChainByIdFlow().first() + + return groupAndSortAssetsByToken(assets, externalBalances.aggregatedBalanceByAsset(), chains) + } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/list/AssetsListInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/list/AssetsListInteractor.kt index f48ca7d1fb..13388f4e1a 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/list/AssetsListInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/list/AssetsListInteractor.kt @@ -1,5 +1,7 @@ package io.novafoundation.nova.feature_assets.domain.assets.list +import io.novafoundation.nova.common.data.model.AssetViewMode +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_nft_api.data.model.Nft @@ -15,9 +17,16 @@ private const val BANNER_TAG = "CROWDLOAN_UNLOCK_BANNER" class AssetsListInteractor( private val accountRepository: AccountRepository, private val nftRepository: NftRepository, - private val bannerVisibilityRepository: BannerVisibilityRepository + private val bannerVisibilityRepository: BannerVisibilityRepository, + private val assetsViewModeRepository: AssetsViewModeRepository ) { + fun assetsViewModeFlow() = assetsViewModeRepository.assetsViewModeFlow() + + suspend fun setAssetViewMode(assetViewModel: AssetViewMode) { + assetsViewModeRepository.setAssetsViewMode(assetViewModel) + } + suspend fun fullSyncNft(nft: Nft) = nftRepository.fullNftSync(nft) fun observeNftPreviews(): Flow { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/models/AssetsByViewModeResult.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/models/AssetsByViewModeResult.kt new file mode 100644 index 0000000000..a51611a4a8 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/models/AssetsByViewModeResult.kt @@ -0,0 +1,29 @@ +package io.novafoundation.nova.feature_assets.domain.assets.models + +import io.novafoundation.nova.common.utils.MultiMapList +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup + +sealed interface AssetsByViewModeResult { + + class ByNetworks(val assets: MultiMapList) : AssetsByViewModeResult + + class ByTokens(val tokens: MultiMapList) : AssetsByViewModeResult +} + +fun AssetsByViewModeResult.groupList(): List { + return when (this) { + is AssetsByViewModeResult.ByNetworks -> assets.keys.toList() + is AssetsByViewModeResult.ByTokens -> tokens.keys.toList() + } +} + +fun MultiMapList.byNetworks(): AssetsByViewModeResult { + return AssetsByViewModeResult.ByNetworks(this) +} + +fun MultiMapList.byTokens(): AssetsByViewModeResult { + return AssetsByViewModeResult.ByTokens(this) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchInteractor.kt index 1817c6eecd..3c4ec20485 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchInteractor.kt @@ -1,148 +1,45 @@ package io.novafoundation.nova.feature_assets.domain.assets.search -import io.novafoundation.nova.common.utils.flowOfAll -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup -import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance -import io.novafoundation.nova.feature_assets.domain.common.getAssetBaseComparator -import io.novafoundation.nova.feature_assets.domain.common.getAssetGroupBaseComparator -import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByNetwork -import io.novafoundation.nova.feature_assets.domain.common.searchTokens -import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry -import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance -import io.novafoundation.nova.feature_wallet_api.domain.model.aggregatedBalanceByAsset -import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.ChainsById -import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import io.novafoundation.nova.runtime.multiNetwork.enabledChainById -import io.novasama.substrate_sdk_android.hash.isPositive import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -private typealias AssetSearchFilter = suspend (Asset) -> Boolean +interface AssetSearchInteractorFactory { -class AssetSearchInteractor( - private val walletRepository: WalletRepository, - private val accountRepository: AccountRepository, - private val chainRegistry: ChainRegistry, - private val assetSourceRegistry: AssetSourceRegistry, - private val swapService: SwapService -) { + fun createByAssetViewMode(): AssetSearchInteractor +} + +typealias AssetSearchFilter = suspend (Asset) -> Boolean + +interface AssetSearchInteractor { fun buyAssetSearch( queryFlow: Flow, externalBalancesFlow: Flow>, - ): Flow>> { - return searchAssetsInternalFlow(queryFlow, externalBalancesFlow) { - it.token.configuration.buyProviders.isNotEmpty() - } - } + ): Flow fun sendAssetSearch( queryFlow: Flow, externalBalancesFlow: Flow>, - ): Flow>> { - val groupComparator = getAssetGroupBaseComparator { it.groupTransferableBalanceFiat } - val assetsComparator = getAssetBaseComparator { it.balanceWithOffchain.transferable.fiat } - - return searchAssetsInternalFlow(queryFlow, externalBalancesFlow, groupComparator, assetsComparator) { asset -> - asset.transferableInPlanks.isPositive() - } - } + ): Flow fun searchSwapAssetsFlow( forAsset: FullChainAssetId?, queryFlow: Flow, externalBalancesFlow: Flow>, coroutineScope: CoroutineScope - ): Flow>> { - val filterFlow = getAvailableSwapAssets(forAsset, coroutineScope).map { availableAssetsForSwap -> - val filter: AssetSearchFilter = { asset -> - val chainAsset = asset.token.configuration - - chainAsset.fullId in availableAssetsForSwap - } - - filter - } - - return searchAssetsInternalFlow(queryFlow, externalBalancesFlow, filterFlow = filterFlow) - } - - fun searchAssetsFlow( - queryFlow: Flow, - externalBalancesFlow: Flow>, - ): Flow>> { - return searchAssetsInternalFlow(queryFlow, externalBalancesFlow, filter = null) - } - - private fun getAvailableSwapAssets(asset: FullChainAssetId?, coroutineScope: CoroutineScope): Flow> { - return flowOfAll { - val chainAsset = asset?.let { chainRegistry.asset(it) } - - if (chainAsset == null) { - swapService.assetsAvailableForSwap(coroutineScope) - } else { - swapService.availableSwapDirectionsFor(chainAsset, coroutineScope) - } - } - } + ): Flow - private fun searchAssetsInternalFlow( + fun searchReceiveAssetsFlow( queryFlow: Flow, externalBalancesFlow: Flow>, - assetGroupComparator: Comparator = getAssetGroupBaseComparator(), - assetsComparator: Comparator = getAssetBaseComparator(), - filter: AssetSearchFilter?, - ): Flow>> { - val filterFlow = flowOf(filter) + ): Flow - return searchAssetsInternalFlow(queryFlow, externalBalancesFlow, assetGroupComparator, assetsComparator, filterFlow) - } - - private fun searchAssetsInternalFlow( + fun searchAssetsFlow( queryFlow: Flow, externalBalancesFlow: Flow>, - assetGroupComparator: Comparator = getAssetGroupBaseComparator(), - assetsComparator: Comparator = getAssetBaseComparator(), - filterFlow: Flow, - ): Flow>> { - var assetsFlow = accountRepository.selectedMetaAccountFlow() - .flatMapLatest { walletRepository.syncedAssetsFlow(it.id) } - - assetsFlow = combine(assetsFlow, filterFlow) { assets, filter -> - if (filter == null) { - assets - } else { - assets.filter { filter(it) } - } - } - - val aggregatedExternalBalances = externalBalancesFlow.map { it.aggregatedBalanceByAsset() } - - return combine(assetsFlow, aggregatedExternalBalances, queryFlow) { assets, externalBalances, query -> - val chainsById = chainRegistry.enabledChainById() - val filtered = assets.filterBy(query, chainsById) - - groupAndSortAssetsByNetwork(filtered, externalBalances, chainsById, assetGroupComparator, assetsComparator) - } - } - - private fun List.filterBy(query: String, chainsById: ChainsById): List { - return searchTokens( - query = query, - chainsById = chainsById, - tokenSymbol = { it.token.configuration.symbol.value }, - relevantToChains = { asset, chainIds -> asset.token.configuration.chainId in chainIds } - ) - } + ): Flow } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchUseCase.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchUseCase.kt new file mode 100644 index 0000000000..677231e93f --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchUseCase.kt @@ -0,0 +1,58 @@ +package io.novafoundation.nova.feature_assets.domain.assets.search + +import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_assets.domain.common.searchTokens +import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService +import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.ChainsById +import io.novafoundation.nova.runtime.multiNetwork.asset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest + +class AssetSearchUseCase( + private val walletRepository: WalletRepository, + private val accountRepository: AccountRepository, + private val chainRegistry: ChainRegistry, + private val swapService: SwapService +) { + + fun filteredAssetFlow(filterFlow: Flow): Flow> { + val assetsFlow = accountRepository.selectedMetaAccountFlow() + .flatMapLatest { walletRepository.syncedAssetsFlow(it.id) } + + return combine(assetsFlow, filterFlow) { assets, filter -> + if (filter == null) { + assets + } else { + assets.filter { filter(it) } + } + } + } + + fun filterAssetsByQuery(query: String, assets: List, chainsById: ChainsById): List { + return assets.searchTokens( + query = query, + chainsById = chainsById, + tokenSymbol = { it.token.configuration.symbol.value }, + relevantToChains = { asset, chainIds -> asset.token.configuration.chainId in chainIds } + ) + } + + fun getAvailableSwapAssets(asset: FullChainAssetId?, coroutineScope: CoroutineScope): Flow> { + return flowOfAll { + val chainAsset = asset?.let { chainRegistry.asset(it) } + + if (chainAsset == null) { + swapService.assetsAvailableForSwap(coroutineScope) + } else { + swapService.availableSwapDirectionsFor(chainAsset, coroutineScope) + } + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetViewModeAssetSearchInteractorFactory.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetViewModeAssetSearchInteractorFactory.kt new file mode 100644 index 0000000000..1617517d26 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetViewModeAssetSearchInteractorFactory.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_assets.domain.assets.search + +import io.novafoundation.nova.common.data.model.AssetViewMode +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +class AssetViewModeAssetSearchInteractorFactory( + private val assetViewModeRepository: AssetsViewModeRepository, + private val assetSearchUseCase: AssetSearchUseCase, + private val chainRegistry: ChainRegistry +) : AssetSearchInteractorFactory { + + override fun createByAssetViewMode(): AssetSearchInteractor { + return when (assetViewModeRepository.getAssetViewMode()) { + AssetViewMode.TOKENS -> ByTokensAssetSearchInteractor( + assetSearchUseCase, + chainRegistry + ) + + AssetViewMode.NETWORKS -> ByNetworkAssetSearchInteractor( + assetSearchUseCase, + chainRegistry + ) + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/ByNetworkAssetSearchInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/ByNetworkAssetSearchInteractor.kt new file mode 100644 index 0000000000..88341f2cff --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/ByNetworkAssetSearchInteractor.kt @@ -0,0 +1,116 @@ +package io.novafoundation.nova.feature_assets.domain.assets.search + +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.getAssetBaseComparator +import io.novafoundation.nova.feature_assets.domain.common.getAssetGroupBaseComparator +import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByNetwork +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance +import io.novafoundation.nova.feature_wallet_api.domain.model.aggregatedBalanceByAsset +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.enabledChainById +import io.novasama.substrate_sdk_android.hash.isPositive +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +class ByNetworkAssetSearchInteractor( + private val assetSearchUseCase: AssetSearchUseCase, + private val chainRegistry: ChainRegistry +) : AssetSearchInteractor { + + override fun buyAssetSearch( + queryFlow: Flow, + externalBalancesFlow: Flow>, + ): Flow { + val filter = { asset: Asset -> asset.token.configuration.buyProviders.isNotEmpty() } + + return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filter = filter) + } + + override fun sendAssetSearch( + queryFlow: Flow, + externalBalancesFlow: Flow>, + ): Flow { + val filter = { asset: Asset -> asset.transferableInPlanks.isPositive() } + + return searchAssetsByNetworksInternalFlow( + queryFlow, + externalBalancesFlow, + assetGroupComparator = getAssetGroupBaseComparator { it.groupTransferableBalanceFiat }, + assetsComparator = getAssetBaseComparator { it.balanceWithOffchain.transferable.fiat }, + filter = filter + ) + } + + override fun searchSwapAssetsFlow( + forAsset: FullChainAssetId?, + queryFlow: Flow, + externalBalancesFlow: Flow>, + coroutineScope: CoroutineScope + ): Flow { + val filterFlow = assetSearchUseCase.getAvailableSwapAssets(forAsset, coroutineScope).map { availableAssetsForSwap -> + val filter: AssetSearchFilter = { asset -> + val chainAsset = asset.token.configuration + + chainAsset.fullId in availableAssetsForSwap + } + + filter + } + + return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filterFlow = filterFlow) + } + + override fun searchReceiveAssetsFlow( + queryFlow: Flow, + externalBalancesFlow: Flow>, + ): Flow { + return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filter = null) + } + + override fun searchAssetsFlow( + queryFlow: Flow, + externalBalancesFlow: Flow>, + ): Flow { + return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filter = null) + } + + private fun ByNetworkAssetSearchInteractor.searchAssetsByNetworksInternalFlow( + queryFlow: Flow, + externalBalancesFlow: Flow>, + assetGroupComparator: Comparator = getAssetGroupBaseComparator(), + assetsComparator: Comparator = getAssetBaseComparator(), + filter: AssetSearchFilter?, + ): Flow { + val filterFlow = flowOf(filter) + + return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, assetGroupComparator, assetsComparator, filterFlow) + } + + private fun searchAssetsByNetworksInternalFlow( + queryFlow: Flow, + externalBalancesFlow: Flow>, + assetGroupComparator: Comparator = getAssetGroupBaseComparator(), + assetsComparator: Comparator = getAssetBaseComparator(), + filterFlow: Flow, + ): Flow { + val assetsFlow = assetSearchUseCase.filteredAssetFlow(filterFlow) + + val aggregatedExternalBalances = externalBalancesFlow.map { it.aggregatedBalanceByAsset() } + + return combine(assetsFlow, aggregatedExternalBalances, queryFlow) { assets, externalBalances, query -> + val chainsById = chainRegistry.enabledChainById() + val filtered = assetSearchUseCase.filterAssetsByQuery(query, assets, chainsById) + + val assetGroups = groupAndSortAssetsByNetwork(filtered, externalBalances, chainsById, assetGroupComparator, assetsComparator) + AssetsByViewModeResult.ByNetworks(assetGroups) + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/ByTokensAssetSearchInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/ByTokensAssetSearchInteractor.kt new file mode 100644 index 0000000000..5755da513a --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/ByTokensAssetSearchInteractor.kt @@ -0,0 +1,118 @@ +package io.novafoundation.nova.feature_assets.domain.assets.search + +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.getTokenAssetBaseComparator +import io.novafoundation.nova.feature_assets.domain.common.getTokenAssetGroupBaseComparator +import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByToken +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance +import io.novafoundation.nova.feature_wallet_api.domain.model.aggregatedBalanceByAsset +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.enabledChainById +import io.novasama.substrate_sdk_android.hash.isPositive +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +class ByTokensAssetSearchInteractor( + private val assetSearchUseCase: AssetSearchUseCase, + private val chainRegistry: ChainRegistry +) : AssetSearchInteractor { + + override fun buyAssetSearch( + queryFlow: Flow, + externalBalancesFlow: Flow>, + ): Flow { + val filter = { asset: Asset -> asset.token.configuration.buyProviders.isNotEmpty() } + + return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filter = filter) + } + + override fun sendAssetSearch( + queryFlow: Flow, + externalBalancesFlow: Flow>, + ): Flow { + val filter = { asset: Asset -> asset.transferableInPlanks.isPositive() } + + return searchAssetsByTokensInternalFlow( + queryFlow, + externalBalancesFlow, + assetGroupComparator = getTokenAssetGroupBaseComparator { it.groupBalance.transferable.fiat }, + assetsComparator = getTokenAssetBaseComparator { it.balanceWithOffChain.transferable.fiat }, + filter = filter + ) + } + + override fun searchSwapAssetsFlow( + forAsset: FullChainAssetId?, + queryFlow: Flow, + externalBalancesFlow: Flow>, + coroutineScope: CoroutineScope + ): Flow { + val filterFlow = assetSearchUseCase.getAvailableSwapAssets(forAsset, coroutineScope) + .map { availableAssetsForSwap -> + val filter: AssetSearchFilter = { asset -> + val chainAsset = asset.token.configuration + + chainAsset.fullId in availableAssetsForSwap + } + + filter + } + + return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filterFlow = filterFlow) + } + + override fun searchReceiveAssetsFlow( + queryFlow: Flow, + externalBalancesFlow: Flow>, + ): Flow { + return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filter = null) + } + + override fun searchAssetsFlow( + queryFlow: Flow, + externalBalancesFlow: Flow>, + ): Flow { + return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filter = null) + } + + private fun searchAssetsByTokensInternalFlow( + queryFlow: Flow, + externalBalancesFlow: Flow>, + assetGroupComparator: Comparator = getTokenAssetGroupBaseComparator(), + assetsComparator: Comparator = getTokenAssetBaseComparator(), + filter: AssetSearchFilter?, + ): Flow { + val filterFlow = flowOf(filter) + + return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, assetGroupComparator, assetsComparator, filterFlow) + } + + private fun searchAssetsByTokensInternalFlow( + queryFlow: Flow, + externalBalancesFlow: Flow>, + assetGroupComparator: Comparator = getTokenAssetGroupBaseComparator(), + assetsComparator: Comparator = getTokenAssetBaseComparator(), + filterFlow: Flow, + ): Flow { + val assetsFlow = assetSearchUseCase.filteredAssetFlow(filterFlow) + + val aggregatedExternalBalances = externalBalancesFlow.map { it.aggregatedBalanceByAsset() } + + return combine(assetsFlow, aggregatedExternalBalances, queryFlow) { assets, externalBalances, query -> + val chainsById = chainRegistry.enabledChainById() + val filtered = assetSearchUseCase.filterAssetsByQuery(query, assets, chainsById) + + val assetGroups = groupAndSortAssetsByToken(filtered, externalBalances, chainsById, assetGroupComparator, assetsComparator) + + AssetsByViewModeResult.ByTokens(assetGroups) + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/AssetBalance.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/AssetBalance.kt new file mode 100644 index 0000000000..adb2947bdb --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/AssetBalance.kt @@ -0,0 +1,33 @@ +package io.novafoundation.nova.feature_assets.domain.common + +import java.math.BigDecimal + +class AssetBalance( + val total: PricedAmount, + val transferable: PricedAmount +) { + + companion object { + val ZERO = AssetBalance(PricedAmount(BigDecimal.ZERO, BigDecimal.ZERO), PricedAmount(BigDecimal.ZERO, BigDecimal.ZERO)) + } + + operator fun plus(other: AssetBalance): AssetBalance { + return AssetBalance( + total + other.total, + transferable + other.transferable + ) + } +} + +class PricedAmount( + val amount: BigDecimal, + val fiat: BigDecimal +) { + + operator fun plus(other: PricedAmount): PricedAmount { + return PricedAmount( + amount + other.amount, + fiat + other.fiat + ) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/AssetSorting.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/NetworkAssetSorting.kt similarity index 77% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/AssetSorting.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/NetworkAssetSorting.kt index 58458688e2..e4b816da01 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/AssetSorting.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/common/NetworkAssetSorting.kt @@ -12,7 +12,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import java.math.BigDecimal -class AssetGroup( +class NetworkAssetGroup( val chain: Chain, val groupTotalBalanceFiat: BigDecimal, val groupTransferableBalanceFiat: BigDecimal, @@ -21,34 +21,23 @@ class AssetGroup( class AssetWithOffChainBalance( val asset: Asset, - val balanceWithOffchain: Balance, -) { - - class Balance( - val total: Amount, - val transferable: Amount - ) -} - -class Amount( - val amount: BigDecimal, - val fiat: BigDecimal + val balanceWithOffchain: AssetBalance, ) fun groupAndSortAssetsByNetwork( assets: List, externalBalances: Map, chainsById: Map, - assetGroupComparator: Comparator = getAssetGroupBaseComparator(), + assetGroupComparator: Comparator = getAssetGroupBaseComparator(), assetComparator: Comparator = getAssetBaseComparator() -): Map> { +): Map> { return assets .map { asset -> AssetWithOffChainBalance(asset, asset.totalWithOffChain(externalBalances)) } .filter { chainsById.containsKey(it.asset.token.configuration.chainId) } .groupBy { chainsById.getValue(it.asset.token.configuration.chainId) } .mapValues { (_, assets) -> assets.sortedWith(assetComparator) } .mapKeys { (chain, assets) -> - AssetGroup( + NetworkAssetGroup( chain = chain, groupTotalBalanceFiat = assets.sumByBigDecimal { it.balanceWithOffchain.total.fiat }, groupTransferableBalanceFiat = assets.sumByBigDecimal { it.balanceWithOffchain.transferable.fiat }, @@ -67,14 +56,14 @@ fun getAssetBaseComparator( } fun getAssetGroupBaseComparator( - comparing: (AssetGroup) -> Comparable<*> = AssetGroup::groupTotalBalanceFiat -): Comparator { + comparing: (NetworkAssetGroup) -> Comparable<*> = NetworkAssetGroup::groupTotalBalanceFiat +): Comparator { return compareByDescending(comparing) .thenByDescending { it.zeroBalance } // non-zero balances first - .then(Chain.defaultComparatorFrom(AssetGroup::chain)) + .then(Chain.defaultComparatorFrom(NetworkAssetGroup::chain)) } -private fun Asset.totalWithOffChain(externalBalances: Map): AssetWithOffChainBalance.Balance { +fun Asset.totalWithOffChain(externalBalances: Map): AssetBalance { val onChainTotal = total val offChainTotal = externalBalances[token.configuration.fullId] ?.let(token::amountFromPlanks) @@ -83,8 +72,8 @@ private fun Asset.totalWithOffChain(externalBalances: Map, + externalBalances: Map, + chainsById: Map, + assetGroupComparator: Comparator = getTokenAssetGroupBaseComparator(), + assetComparator: Comparator = getTokenAssetBaseComparator() +): Map> { + return assets + .filter { chainsById.containsKey(it.token.configuration.chainId) } + .map { asset -> AssetWithNetwork(chainsById.getValue(asset.token.configuration.chainId), asset, asset.totalWithOffChain(externalBalances)) } + .groupBy { mapToTokenGroup(it) } + .mapValues { (_, assets) -> assets.sortedWith(assetComparator) } + .mapKeys { (tokenWrapper, assets) -> + TokenAssetGroup( + token = tokenWrapper.token, + groupBalance = assets.fold(AssetBalance.ZERO) { acc, element -> acc + element.balanceWithOffChain }, + itemsCount = assets.size + ) + }.toSortedMap(assetGroupComparator) +} + +fun getTokenAssetBaseComparator( + comparing: (AssetWithNetwork) -> Comparable<*> = { it.balanceWithOffChain.total.amount } +): Comparator { + return compareByDescending(comparing) + .thenByDescending { it.asset.token.configuration.isUtilityAsset } // utility assets first + .thenBy { it.asset.token.configuration.symbol.value } + .then(Chain.defaultComparatorFrom(AssetWithNetwork::chain)) +} + +fun getTokenAssetGroupBaseComparator( + comparing: (TokenAssetGroup) -> Comparable<*> = { it.groupBalance.total.fiat } +): Comparator { + return compareByDescending(comparing) + .thenByDescending { it.groupBalance.total.amount > BigDecimal.ZERO } // non-zero balances first + .then(TokenSymbol.defaultComparatorFrom { it.token.symbol }) +} + +private fun mapToTokenGroup(it: AssetWithNetwork) = TokenGroupWrapper( + TokenAssetGroup.Token( + it.asset.token.configuration.icon, + it.asset.token.configuration.symbol.normalize(), + it.asset.token.currency, + it.asset.token.coinRate + ) +) + +// Helper class to group items by symbol only +private class TokenGroupWrapper(val token: TokenAssetGroup.Token) { + + override fun equals(other: Any?): Boolean { + return other is TokenGroupWrapper && token.symbol == other.token.symbol + } + + override fun hashCode(): Int { + return token.symbol.hashCode() + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/networks/AssetNetworksInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/networks/AssetNetworksInteractor.kt new file mode 100644 index 0000000000..db36fa1921 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/networks/AssetNetworksInteractor.kt @@ -0,0 +1,133 @@ +package io.novafoundation.nova.feature_assets.domain.networks + +import io.novafoundation.nova.common.utils.TokenSymbol +import io.novafoundation.nova.common.utils.filterList +import io.novafoundation.nova.common.utils.filterSet +import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchFilter +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchUseCase +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.getTokenAssetBaseComparator +import io.novafoundation.nova.feature_assets.domain.common.getTokenAssetGroupBaseComparator +import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByToken +import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance +import io.novafoundation.nova.feature_wallet_api.domain.model.aggregatedBalanceByAsset +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.ext.normalize +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.enabledChainById +import io.novafoundation.nova.runtime.multiNetwork.enabledChains +import io.novasama.substrate_sdk_android.hash.isPositive +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +class AssetNetworksInteractor( + private val chainRegistry: ChainRegistry, + private val swapService: SwapService, + private val assetSearchUseCase: AssetSearchUseCase +) { + + fun buyAssetFlow( + tokenSymbol: TokenSymbol, + externalBalancesFlow: Flow>, + ): Flow> { + val filter = { asset: Asset -> asset.token.configuration.buyProviders.isNotEmpty() } + + return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, filter = filter) + } + + fun sendAssetFlow( + tokenSymbol: TokenSymbol, + externalBalancesFlow: Flow>, + ): Flow> { + val filter = { asset: Asset -> asset.transferableInPlanks.isPositive() } + + return searchAssetsByTokenSymbolInternalFlow( + tokenSymbol, + externalBalancesFlow, + assetGroupComparator = getTokenAssetGroupBaseComparator { it.groupBalance.transferable.fiat }, + assetsComparator = getTokenAssetBaseComparator { it.balanceWithOffChain.transferable.fiat }, + filter = filter + ) + } + + fun swapAssetsFlow( + tokenSymbol: TokenSymbol, + externalBalancesFlow: Flow>, + coroutineScope: CoroutineScope + ): Flow> { + val filterFlow = getSwapAssetsFilter(tokenSymbol, coroutineScope) + + return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, filterFlow = filterFlow) + } + + fun receiveAssetFlow( + tokenSymbol: TokenSymbol, + externalBalancesFlow: Flow>, + ): Flow> { + return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, filter = null) + } + + fun searchAssetsByTokenSymbolInternalFlow( + tokenSymbol: TokenSymbol, + externalBalancesFlow: Flow>, + assetGroupComparator: Comparator = getTokenAssetGroupBaseComparator(), + assetsComparator: Comparator = getTokenAssetBaseComparator(), + filterFlow: Flow, + ): Flow> { + val assetsFlow = assetSearchUseCase.filteredAssetFlow(filterFlow) + .filterList { it.token.configuration.symbol.normalize() == tokenSymbol } + + val aggregatedExternalBalances = externalBalancesFlow.map { it.aggregatedBalanceByAsset() } + + return combine(assetsFlow, aggregatedExternalBalances) { assets, externalBalances -> + val chainsById = chainRegistry.enabledChainById() + + groupAndSortAssetsByToken(assets, externalBalances, chainsById, assetGroupComparator, assetsComparator) + .flatMap { it.value } + } + } + + private fun getSwapAssetsFilter(tokenSymbol: TokenSymbol, coroutineScope: CoroutineScope): Flow { + return getAvailableSwapAssets(tokenSymbol, coroutineScope) + .map { availableAssetsForSwap -> + val assetFilter: suspend (Asset) -> Boolean = { asset: Asset -> + asset.token.configuration.fullId in availableAssetsForSwap + } + + assetFilter + } + } + + private fun getAvailableSwapAssets(tokenSymbol: TokenSymbol, coroutineScope: CoroutineScope): Flow> { + return flowOfAll { + val assetsSupportedTokenSymbol = chainRegistry.enabledChains() + .flatMap { chain -> + chain.assets.filter { it.symbol.normalize() == tokenSymbol } + .map { it.fullId } + } + + swapService.assetsAvailableForSwap(coroutineScope) + .filterSet { fullAssetId -> fullAssetId in assetsSupportedTokenSymbol } + } + } +} + +private fun AssetNetworksInteractor.searchAssetsByTokenSymbolInternalFlow( + tokenSymbol: TokenSymbol, + externalBalancesFlow: Flow>, + assetGroupComparator: Comparator = getTokenAssetGroupBaseComparator(), + assetsComparator: Comparator = getTokenAssetBaseComparator(), + filter: AssetSearchFilter?, +): Flow> { + val filterFlow = flowOf(filter) + + return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, assetGroupComparator, assetsComparator, filterFlow) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/receive/ReceiveInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/receive/ReceiveInteractor.kt index a5638bd8a1..43aa06f273 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/receive/ReceiveInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/receive/ReceiveInteractor.kt @@ -2,6 +2,8 @@ package io.novafoundation.nova.feature_assets.domain.receive import android.graphics.Bitmap import android.net.Uri +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository import io.novafoundation.nova.common.interfaces.FileProvider import io.novafoundation.nova.common.utils.write import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository @@ -16,6 +18,7 @@ class ReceiveInteractor( private val fileProvider: FileProvider, private val chainRegistry: ChainRegistry, private val accountRepository: AccountRepository, + private val assetsIconModeRepository: AssetsIconModeRepository ) { suspend fun getQrCodeSharingString(chainId: ChainId): String = withContext(Dispatchers.Default) { @@ -25,6 +28,8 @@ class ReceiveInteractor( accountRepository.createQrAccountContent(chain, account) } + fun getAssetIconMode(): AssetIconMode = assetsIconModeRepository.getIconMode() + suspend fun generateTempQrFile(qrCode: Bitmap): Result = withContext(Dispatchers.IO) { runCatching { val file = fileProvider.generateTempFile(fixedName = QR_FILE_NAME) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/AddTokensInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/AddTokensInteractor.kt index 06b9c5f640..170b42b074 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/AddTokensInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/AddTokensInteractor.kt @@ -77,7 +77,7 @@ class RealAddTokensInteractor( val priceId = coinGeckoLinkParser.parse(customErc20Token.priceLink).getOrNull()?.priceId val asset = Chain.Asset( - iconUrl = null, + icon = null, id = chainAssetIdOfErc20Token(customErc20Token.contract), priceId = priceId, chainId = customErc20Token.chainId, diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/manage/ManageTokenInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/manage/ManageTokenInteractor.kt index bb838e4e15..93f79b809a 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/manage/ManageTokenInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/manage/ManageTokenInteractor.kt @@ -6,7 +6,7 @@ import io.novafoundation.nova.feature_assets.domain.tokens.AssetsDataCleaner import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository import io.novafoundation.nova.runtime.ext.defaultComparator import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.ext.unifiedSymbol +import io.novafoundation.nova.runtime.ext.normalizeSymbol import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -92,7 +92,7 @@ class RealManageTokenInteractor( val enabledAssets = assetsWithChains.filter { it.asset.enabled } .map { it.asset.fullId } - return assetsWithChains.groupBy { (_, asset) -> asset.unifiedSymbol() } + return assetsWithChains.groupBy { (_, asset) -> asset.normalizeSymbol() } .map { (symbol, chainsWithAssets) -> val (_, firstAsset) = chainsWithAssets.first() val tokenAssets = chainsWithAssets.filter { it.asset.enabled } @@ -103,7 +103,7 @@ class RealManageTokenInteractor( MultiChainToken( id = symbol, symbol = symbol, - icon = firstAsset.iconUrl, + icon = firstAsset.icon, isSwitchable = !isLastTokenEnabled, instances = chainsWithAssets.map { (chain, asset) -> MultiChainToken.ChainTokenInstance( diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt index ac6f6d3a53..8f079be64e 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt @@ -1,9 +1,11 @@ package io.novafoundation.nova.feature_assets.presentation import android.os.Bundle +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload import io.novafoundation.nova.feature_assets.presentation.model.OperationParcelizeModel import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload +import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowPayload import io.novafoundation.nova.feature_assets.presentation.tokens.add.enterInfo.AddTokenEnterInfoPayload import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.ManageChainTokensPayload import io.novafoundation.nova.feature_assets.presentation.transaction.filter.TransactionHistoryFilterPayload @@ -40,8 +42,6 @@ interface AssetsRouter { fun openReceive(assetPayload: AssetPayload) - fun openAssetFilters() - fun openAssetSearch() fun openManageTokens() @@ -75,4 +75,14 @@ interface AssetsRouter { fun openStaking() fun closeSendFlow() + + fun openSendNetworks(payload: NetworkFlowPayload) + + fun openReceiveNetworks(payload: NetworkFlowPayload) + + fun openSwapNetworks(payload: NetworkSwapFlowPayload) + + fun openBuyNetworks(payload: NetworkFlowPayload) + + fun returnToMainSwapScreen() } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetExpandableAssetDecorationSettings.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetExpandableAssetDecorationSettings.kt new file mode 100644 index 0000000000..89a22476b4 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetExpandableAssetDecorationSettings.kt @@ -0,0 +1,6 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common + +import android.view.animation.AccelerateDecelerateInterpolator +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings + +fun ExpandableAnimationSettings.Companion.createForAssets() = ExpandableAnimationSettings(400, AccelerateDecelerateInterpolator()) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetListMixin.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetListMixin.kt new file mode 100644 index 0000000000..a7f40bbcb1 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetListMixin.kt @@ -0,0 +1,98 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common + +import io.novafoundation.nova.common.data.model.AssetViewMode +import io.novafoundation.nova.common.utils.combineToPair +import io.novafoundation.nova.common.utils.shareInBackground +import io.novafoundation.nova.common.utils.throttleLast +import io.novafoundation.nova.feature_assets.domain.WalletInteractor +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor +import io.novafoundation.nova.feature_assets.domain.assets.models.byNetworks +import io.novafoundation.nova.feature_assets.domain.assets.models.byTokens +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.combine +import kotlin.time.Duration.Companion.milliseconds + +class AssetListMixinFactory( + private val walletInteractor: WalletInteractor, + private val assetsListInteractor: AssetsListInteractor, + private val externalBalancesInteractor: ExternalBalancesInteractor, + private val expandableAssetsMixinFactory: ExpandableAssetsMixinFactory +) { + + fun create(coroutineScope: CoroutineScope): AssetListMixin = RealAssetListMixin( + walletInteractor, + assetsListInteractor, + externalBalancesInteractor, + expandableAssetsMixinFactory, + coroutineScope + ) +} + +interface AssetListMixin { + + val assetsViewModeFlow: Flow + + val externalBalancesFlow: SharedFlow> + + val assetsFlow: Flow> + + val assetModelsFlow: Flow> + + fun expandToken(tokenGroupUi: TokenGroupUi) + + suspend fun switchViewMode() +} + +class RealAssetListMixin( + private val walletInteractor: WalletInteractor, + private val assetsListInteractor: AssetsListInteractor, + private val externalBalancesInteractor: ExternalBalancesInteractor, + private val expandableAssetsMixinFactory: ExpandableAssetsMixinFactory, + private val coroutineScope: CoroutineScope +) : AssetListMixin, CoroutineScope by coroutineScope { + + override val assetsFlow = walletInteractor.assetsFlow() + .shareInBackground() + + private val filteredAssetsFlow = walletInteractor.filterAssets(assetsFlow) + .shareInBackground() + + override val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances() + .shareInBackground() + + override val assetsViewModeFlow = assetsListInteractor.assetsViewModeFlow() + .shareInBackground() + + private val throttledBalance = combineToPair(filteredAssetsFlow, externalBalancesFlow) + .throttleLast(300.milliseconds) + + private val assetsByViewMode = combine( + throttledBalance, + assetsViewModeFlow + ) { (assets, externalBalances), viewMode -> + when (viewMode) { + AssetViewMode.NETWORKS -> walletInteractor.groupAssetsByNetwork(assets, externalBalances).byNetworks() + AssetViewMode.TOKENS -> walletInteractor.groupAssetsByToken(assets, externalBalances).byTokens() + } + }.shareInBackground() + + private val expandableAssetsMixin = expandableAssetsMixinFactory.create(assetsByViewMode) + + override val assetModelsFlow = expandableAssetsMixin.assetModelsFlow + .shareInBackground() + + override fun expandToken(tokenGroupUi: TokenGroupUi) { + expandableAssetsMixin.expandToken(tokenGroupUi) + } + + override suspend fun switchViewMode() { + expandableAssetsMixin.switchViewMode() + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetMappers.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetMappers.kt deleted file mode 100644 index 6799b92fd1..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetMappers.kt +++ /dev/null @@ -1,84 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.balance.common - -import io.novafoundation.nova.common.list.GroupedList -import io.novafoundation.nova.common.list.toListWithHeaders -import io.novafoundation.nova.common.utils.formatting.formatAsChange -import io.novafoundation.nova.common.utils.isNonNegative -import io.novafoundation.nova.common.utils.isZero -import io.novafoundation.nova.common.utils.orZero -import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi -import io.novafoundation.nova.feature_assets.R -import io.novafoundation.nova.feature_assets.domain.common.Amount -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup -import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance -import io.novafoundation.nova.feature_assets.presentation.balance.list.model.AssetGroupUi -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel -import io.novafoundation.nova.feature_assets.presentation.model.TokenModel -import io.novafoundation.nova.feature_currency_api.domain.model.Currency -import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency -import io.novafoundation.nova.feature_wallet_api.domain.model.Token -import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel -import java.math.BigDecimal - -fun GroupedList.mapGroupedAssetsToUi( - currency: Currency, - groupBalance: (AssetGroup) -> BigDecimal = AssetGroup::groupTotalBalanceFiat, - balance: (AssetWithOffChainBalance.Balance) -> Amount = AssetWithOffChainBalance.Balance::total, -): List { - return mapKeys { (assetGroup, _) -> mapAssetGroupToUi(assetGroup, currency, groupBalance) } - .mapValues { (_, assets) -> mapAssetsToAssetModels(assets, balance) } - .toListWithHeaders() -} - -fun mapTokenToTokenModel(token: Token): TokenModel { - return with(token) { - val rateChange = token.coinRate?.recentRateChange - - val changeColorRes = when { - rateChange == null || rateChange.isZero -> R.color.text_secondary - rateChange.isNonNegative -> R.color.text_positive - else -> R.color.text_negative - } - - TokenModel( - configuration = configuration, - rate = coinRate?.rate.orZero().formatAsCurrency(token.currency), - recentRateChange = (coinRate?.recentRateChange ?: BigDecimal.ZERO).formatAsChange(), - rateChangeColorRes = changeColorRes - ) - } -} - -private fun mapAssetsToAssetModels( - assets: List, - balance: (AssetWithOffChainBalance.Balance) -> Amount -): List { - return assets.map { mapAssetToAssetModel(it, balance) } -} - -private fun mapAssetGroupToUi( - assetGroup: AssetGroup, - currency: Currency, - groupBalance: (AssetGroup) -> BigDecimal -): AssetGroupUi { - return AssetGroupUi( - chainUi = mapChainToUi(assetGroup.chain), - groupBalanceFiat = groupBalance(assetGroup).formatAsCurrency(currency) - ) -} - -private fun mapAssetToAssetModel( - assetWithOffChainBalance: AssetWithOffChainBalance, - balance: (AssetWithOffChainBalance.Balance) -> Amount -): AssetModel { - return with(assetWithOffChainBalance) { - AssetModel( - token = mapTokenToTokenModel(asset.token), - amount = mapAmountToAmountModel( - amount = balance(balanceWithOffchain).amount, - asset = asset, - includeAssetTicker = false - ) - ) - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetTokensDecoration.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetTokensDecoration.kt new file mode 100644 index 0000000000..1be9269edd --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetTokensDecoration.kt @@ -0,0 +1,200 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common + +import android.animation.ArgbEvaluator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Rect +import android.graphics.RectF +import android.view.View +import androidx.core.graphics.toRect +import androidx.recyclerview.widget.RecyclerView +import io.novafoundation.nova.common.utils.dp +import io.novafoundation.nova.common.utils.dpF +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAdapter +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableItemDecoration +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimationItemState +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator +import io.novafoundation.nova.common.utils.recyclerView.expandable.expandingFraction +import io.novafoundation.nova.common.utils.recyclerView.expandable.flippedFraction +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableParentItem +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.TokenAssetGroupViewHolder +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import kotlin.math.roundToInt + +class AssetTokensDecoration( + private val context: Context, + private val adapter: ExpandableAdapter, + animator: ExpandableAnimator +) : ExpandableItemDecoration( + adapter, + animator +) { + private val argbEvaluator = ArgbEvaluator() + + private val childrenBlockCollapsedHorizontalMargin = 16.dp(context) + private val childrenBlockCollapsedHeight = 4.dp(context) + + private val blockRadiusCollapsed = 4.dpF(context) + private val blockRadiusExpanded = 12.dpF(context) + private val blockRadiusDelta = blockRadiusExpanded - blockRadiusCollapsed + + private val blockColor = context.getColor(R.color.block_background) + private val hidedBlockColor = context.getColor(R.color.hided_networks_block_background) + private val transparentColor = Color.TRANSPARENT + private val dividerColor = context.getColor(R.color.divider) + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + } + + private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + } + + private var drawingPath = Path() + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val viewHolder = parent.getChildViewHolder(view) + + if (viewHolder.bindingAdapterPosition == 0) return + + if (viewHolder is TokenAssetGroupViewHolder) { + if (viewHolder.bindingAdapterPosition == adapter.getItems().size - 1) { + outRect.set(0, 12.dp(context), 0, 12.dp(context)) + } else { + outRect.set(0, 12.dp(context), 0, 0) + } + } + } + + override fun onDrawGroup( + canvas: Canvas, + animationState: ExpandableAnimationItemState, + recyclerView: RecyclerView, + parentItem: ExpandableParentItem, + parent: RecyclerView.ViewHolder?, + children: List + ) { + val expandingFraction = animationState.expandingFraction() + + val parentBounds = parentBounds(parent) + if (parentBounds != null) { + drawParentBlock(parentBounds, canvas, expandingFraction) + } + + // Don't draw children background if it's a single item + if (parentItem is TokenGroupUi && parentItem.singleItemGroup) return + + val childrenBlockBounds = getChildrenBlockBounds(animationState, recyclerView, parent, children) + drawChildrenBlock(expandingFraction, childrenBlockBounds, canvas) + clipChildren(children, childrenBlockBounds) + } + + private fun clipChildren(children: List, childrenBlockBounds: RectF) { + val childrenBlock = childrenBlockBounds.toRect() + children.forEach { + val childrenBottomClipInset = (it.itemView.bottom + it.itemView.translationY.roundToInt()) - childrenBlock.bottom + val childrenTopClipInset = childrenBlock.top - (it.itemView.top + it.itemView.translationY.roundToInt()) + if (childrenTopClipInset > 0 || childrenBottomClipInset > 0) { + it.itemView.clipBounds = Rect( + 0, + childrenTopClipInset, + it.itemView.width, + it.itemView.height - childrenBottomClipInset + ) + } else { + it.itemView.clipBounds = null + } + } + } + + private fun drawChildrenBlock(expandingFraction: Float, childrenBlockBounds: RectF, canvas: Canvas) { + val animatedBlockRadius = blockRadiusDelta * expandingFraction + childrenBlockBounds.toPath(drawingPath, topRadius = 0f, bottomRadius = blockRadiusCollapsed + animatedBlockRadius * expandingFraction) + paint.color = argbEvaluator.evaluate(expandingFraction, hidedBlockColor, blockColor) as Int + canvas.drawPath(drawingPath, paint) + } + + private fun drawParentBlock( + parentBounds: RectF, + canvas: Canvas, + expandingFraction: Float + ) { + val path = Path() + val bottomRadius = blockRadiusExpanded * expandingFraction.flippedFraction() + parentBounds.toPath(path, topRadius = blockRadiusExpanded, bottomRadius = bottomRadius) + paint.color = blockColor + canvas.drawPath(path, paint) + + drawParentDivider(expandingFraction, bottomRadius, canvas, parentBounds) + } + + private fun drawParentDivider( + expandingFraction: Float, + dividerHorizontalMargin: Float, + canvas: Canvas, + parentBounds: RectF + ) { + linePaint.color = argbEvaluator.evaluate(expandingFraction, transparentColor, dividerColor) as Int + canvas.drawLine( + parentBounds.left + dividerHorizontalMargin, + parentBounds.bottom, + parentBounds.right - dividerHorizontalMargin, + parentBounds.bottom, + linePaint + ) + } + + private fun parentBounds(parent: RecyclerView.ViewHolder?): RectF? { + if (parent == null) return null + + return parent.itemView.let { + RectF( + it.left.toFloat(), + it.top.toFloat() + it.translationY, + it.right.toFloat(), + it.bottom.toFloat() + it.translationY + ) + } + } + + private fun getChildrenBlockBounds( + animationState: ExpandableAnimationItemState, + recyclerView: RecyclerView, + parent: RecyclerView.ViewHolder?, + children: List + ): RectF { + val lastChild = children.maxByOrNull { it.itemView.bottom } + + val parentTranslationY = parent?.itemView?.translationY ?: 0f + val childTranslationY = lastChild?.itemView?.translationY ?: 0f + + val top = (parent?.itemView?.bottom ?: 0) + parentTranslationY + val bottom = (lastChild?.itemView?.bottom?.toFloat() ?: top).coerceAtLeast(top) + val left = parent?.itemView?.left ?: lastChild?.itemView?.left ?: recyclerView.left + val right = parent?.itemView?.right ?: lastChild?.itemView?.right ?: recyclerView.right + + val expandingFraction = animationState.expandingFraction() + val flippedExpandingFraction = expandingFraction.flippedFraction() + val heightDelta = (bottom - top) + return RectF( + left + childrenBlockCollapsedHorizontalMargin * flippedExpandingFraction, + top, + right - childrenBlockCollapsedHorizontalMargin * flippedExpandingFraction, + top + childrenBlockCollapsedHeight + heightDelta * expandingFraction + childTranslationY + ) + } + + private fun RectF.toPath(path: Path, topRadius: Float, bottomRadius: Float) { + path.reset() + path.addRoundRect( + this, + floatArrayOf(topRadius, topRadius, topRadius, topRadius, bottomRadius, bottomRadius, bottomRadius, bottomRadius), + Path.Direction.CW + ) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetTokensItemAnimator.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetTokensItemAnimator.kt new file mode 100644 index 0000000000..00176627b2 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetTokensItemAnimator.kt @@ -0,0 +1,77 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common + +import android.view.ViewPropertyAnimator +import androidx.recyclerview.widget.RecyclerView +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableItemAnimator +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator + +private const val REMOVE_SCALE = 0.9f + +class AssetTokensItemAnimator( + settings: ExpandableAnimationSettings, + expandableAnimator: ExpandableAnimator +) : ExpandableItemAnimator( + settings, + expandableAnimator +) { + + override fun preAddImpl(holder: RecyclerView.ViewHolder) { + holder.itemView.alpha = 0f + holder.itemView.scaleX = REMOVE_SCALE + holder.itemView.scaleY = REMOVE_SCALE + } + + override fun getAddAnimator(holder: RecyclerView.ViewHolder): ViewPropertyAnimator { + return holder.itemView.animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + } + + override fun preRemoveImpl(holder: RecyclerView.ViewHolder) { + resetAddState(holder) + } + + override fun getRemoveAnimator(holder: RecyclerView.ViewHolder): ViewPropertyAnimator { + return holder.itemView.animate() + .alpha(0f) + .scaleX(REMOVE_SCALE) + .scaleY(REMOVE_SCALE) + } + + override fun preMoveImpl(holder: RecyclerView.ViewHolder, fromY: Int, toY: Int) { + val yDelta = toY - fromY + holder.itemView.translationY += -yDelta + } + + override fun getMoveAnimator(holder: RecyclerView.ViewHolder): ViewPropertyAnimator { + return holder.itemView.animate() + .translationY(0f) + } + + override fun endAnimation(viewHolder: RecyclerView.ViewHolder) { + super.endAnimation(viewHolder) + + viewHolder.itemView.translationY = 0f + viewHolder.itemView.alpha = 1f + viewHolder.itemView.scaleX = 1f + viewHolder.itemView.scaleY = 1f + } + + override fun resetAddState(holder: RecyclerView.ViewHolder) { + holder.itemView.alpha = 1f + holder.itemView.scaleX = 1f + holder.itemView.scaleY = 1f + } + + override fun resetRemoveState(holder: RecyclerView.ViewHolder) { + holder.itemView.alpha = 1f + holder.itemView.scaleX = 1f + holder.itemView.scaleY = 1f + } + + override fun resetMoveState(holder: RecyclerView.ViewHolder) { + holder.itemView.translationY = 0f + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/BalanceListAdapter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/BalanceListAdapter.kt index 636dd1e8d0..c6ab404aa6 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/BalanceListAdapter.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/BalanceListAdapter.kt @@ -1,146 +1,165 @@ package io.novafoundation.nova.feature_assets.presentation.balance.common -import android.view.View +import android.annotation.SuppressLint import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder import coil.ImageLoader -import io.novafoundation.nova.common.list.BaseGroupedDiffCallback -import io.novafoundation.nova.common.list.GroupedListAdapter -import io.novafoundation.nova.common.list.GroupedListHolder import io.novafoundation.nova.common.list.PayloadGenerator import io.novafoundation.nova.common.list.resolvePayload import io.novafoundation.nova.common.utils.inflateChild -import io.novafoundation.nova.common.utils.setTextColorRes -import io.novafoundation.nova.feature_account_api.presenatation.chain.loadTokenIcon +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAdapter +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableBaseItem import io.novafoundation.nova.feature_assets.R -import io.novafoundation.nova.feature_assets.presentation.balance.list.model.AssetGroupUi +import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.NetworkAssetGroupViewHolder +import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.NetworkAssetViewHolder +import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.TokenAssetGroupViewHolder +import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.TokenAssetViewHolder +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkAssetUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkGroupUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenAssetUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi import io.novafoundation.nova.feature_assets.presentation.model.AssetModel -import kotlinx.android.synthetic.main.item_asset.view.itemAssetBalance -import kotlinx.android.synthetic.main.item_asset.view.itemAssetImage -import kotlinx.android.synthetic.main.item_asset.view.itemAssetPriceAmount -import kotlinx.android.synthetic.main.item_asset.view.itemAssetRate -import kotlinx.android.synthetic.main.item_asset.view.itemAssetRateChange -import kotlinx.android.synthetic.main.item_asset.view.itemAssetToken -import kotlinx.android.synthetic.main.item_asset_group.view.itemAssetGroupBalance -import kotlinx.android.synthetic.main.item_asset_group.view.itemAssetGroupChain - -val priceRateExtractor = { assetModel: AssetModel -> assetModel.token.rate } -val recentChangeExtractor = { assetModel: AssetModel -> assetModel.token.recentRateChange } +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +private val priceRateExtractor = { asset: AssetModel -> asset.token.rate } +private val recentChangeExtractor = { asset: AssetModel -> asset.token.recentRateChange } +private val amountExtractor = { asset: AssetModel -> asset.amount } + +private val tokenGroupPriceRateExtractor = { group: TokenGroupUi -> group.rate } +private val tokenGroupRecentChangeExtractor = { group: TokenGroupUi -> group.recentRateChange } +private val tokenGroupAmountExtractor = { group: TokenGroupUi -> group.balance } +private val tokenGroupTypeExtractor = { group: TokenGroupUi -> group.groupType } + +const val TYPE_NETWORK_GROUP = 0 +const val TYPE_NETWORK_ASSET = 1 +const val TYPE_TOKEN_GROUP = 2 +const val TYPE_TOKEN_ASSET = 3 class BalanceListAdapter( private val imageLoader: ImageLoader, private val itemHandler: ItemAssetHandler, -) : GroupedListAdapter(DiffCallback) { +) : ListAdapter(DiffCallback), ExpandableAdapter { interface ItemAssetHandler { - fun assetClicked(asset: AssetModel) - } - - override fun createGroupViewHolder(parent: ViewGroup): GroupedListHolder { - val view = parent.inflateChild(R.layout.item_asset_group) - - return AssetGroupViewHolder(view) - } - - override fun createChildViewHolder(parent: ViewGroup): GroupedListHolder { - val view = parent.inflateChild(R.layout.item_asset) - - return AssetViewHolder(view, imageLoader) - } + fun assetClicked(asset: Chain.Asset) - override fun bindGroup(holder: GroupedListHolder, group: AssetGroupUi) { - require(holder is AssetGroupViewHolder) - - holder.bind(group) + fun tokenGroupClicked(tokenGroup: TokenGroupUi) } - override fun bindChild(holder: GroupedListHolder, position: Int, child: AssetModel, payloads: List) { - require(holder is AssetViewHolder) - - resolvePayload(holder, position, payloads) { - when (it) { - priceRateExtractor -> holder.bindPriceInfo(child) - recentChangeExtractor -> holder.bindRecentChange(child) - AssetModel::amount -> holder.bindTotal(child) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return when (viewType) { + TYPE_NETWORK_GROUP -> NetworkAssetGroupViewHolder(parent.inflateChild(R.layout.item_network_asset_group)) + TYPE_NETWORK_ASSET -> NetworkAssetViewHolder(parent.inflateChild(R.layout.item_network_asset), imageLoader) + TYPE_TOKEN_GROUP -> TokenAssetGroupViewHolder(parent.inflateChild(R.layout.item_token_asset_group), imageLoader, itemHandler) + TYPE_TOKEN_ASSET -> TokenAssetViewHolder(parent.inflateChild(R.layout.item_token_asset), imageLoader) + else -> error("Unknown view type") } } - override fun bindChild(holder: GroupedListHolder, child: AssetModel) { - require(holder is AssetViewHolder) - - holder.bind(child, itemHandler) - } -} - -class AssetGroupViewHolder( - containerView: View, -) : GroupedListHolder(containerView) { - - fun bind(assetGroup: AssetGroupUi) = with(containerView) { - itemAssetGroupChain.setChain(assetGroup.chainUi) - itemAssetGroupBalance.text = assetGroup.groupBalanceFiat + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + return when (holder) { + is NetworkAssetGroupViewHolder -> holder.bind(getItem(position) as NetworkGroupUi) + is NetworkAssetViewHolder -> holder.bind(getItem(position) as NetworkAssetUi, itemHandler) + is TokenAssetGroupViewHolder -> holder.bind(getItem(position) as TokenGroupUi) + is TokenAssetViewHolder -> holder.bind(getItem(position) as TokenAssetUi, itemHandler) + else -> error("Unknown holder") + } } -} -class AssetViewHolder( - containerView: View, - private val imageLoader: ImageLoader, -) : GroupedListHolder(containerView) { - - fun bind(asset: AssetModel, itemHandler: BalanceListAdapter.ItemAssetHandler) = with(containerView) { - itemAssetImage.loadTokenIcon(asset.token.configuration.iconUrl, imageLoader) - - bindPriceInfo(asset) - - bindRecentChange(asset) - - bindTotal(asset) + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + when (holder) { + is NetworkAssetViewHolder -> { + val item = getItem(position) as NetworkAssetUi + resolvePayload(holder, position, payloads) { + when (it) { + priceRateExtractor -> holder.bindPriceInfo(item.asset) + recentChangeExtractor -> holder.bindRecentChange(item.asset) + amountExtractor -> holder.bindTotal(item.asset) + } + } + } - itemAssetToken.text = asset.token.configuration.symbol.value + is TokenAssetViewHolder -> { + val item = getItem(position) as TokenAssetUi + resolvePayload(holder, position, payloads) { + when (it) { + AssetModel::amount -> holder.bindTotal(item.asset) + } + } + } - setOnClickListener { itemHandler.assetClicked(asset) } - } + is TokenAssetGroupViewHolder -> { + val item = getItem(position) as TokenGroupUi + resolvePayload(holder, position, payloads) { + when (it) { + tokenGroupPriceRateExtractor -> holder.bindPriceRate(item) + tokenGroupRecentChangeExtractor -> holder.bindRecentChange(item) + tokenGroupAmountExtractor -> holder.bindTotal(item) + tokenGroupTypeExtractor -> holder.bindGroupType(item) + } + } + } - fun bindTotal(asset: AssetModel) { - containerView.itemAssetBalance.text = asset.amount.token - containerView.itemAssetPriceAmount.text = asset.amount.fiat + else -> super.onBindViewHolder(holder, position, payloads) + } } - fun bindRecentChange(asset: AssetModel) = with(containerView) { - itemAssetRateChange.setTextColorRes(asset.token.rateChangeColorRes) - itemAssetRateChange.text = asset.token.recentRateChange + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is NetworkGroupUi -> TYPE_NETWORK_GROUP + is NetworkAssetUi -> TYPE_NETWORK_ASSET + is TokenGroupUi -> TYPE_TOKEN_GROUP + is TokenAssetUi -> TYPE_TOKEN_ASSET + else -> error("Unknown item type") + } } - fun bindPriceInfo(asset: AssetModel) = with(containerView) { - itemAssetRate.text = asset.token.rate + override fun getItems(): List { + return currentList } } -private object DiffCallback : BaseGroupedDiffCallback(AssetGroupUi::class.java) { +private object DiffCallback : DiffUtil.ItemCallback() { - override fun areGroupItemsTheSame(oldItem: AssetGroupUi, newItem: AssetGroupUi): Boolean { - return oldItem.chainUi.id == newItem.chainUi.id + override fun areItemsTheSame(oldItem: BalanceListRvItem, newItem: BalanceListRvItem): Boolean { + return oldItem.itemId == newItem.itemId } - override fun areGroupContentsTheSame(oldItem: AssetGroupUi, newItem: AssetGroupUi): Boolean { + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: BalanceListRvItem, newItem: BalanceListRvItem): Boolean { return oldItem == newItem } - override fun areChildItemsTheSame(oldItem: AssetModel, newItem: AssetModel): Boolean { - return oldItem.token.configuration == newItem.token.configuration - } + override fun getChangePayload(oldItem: BalanceListRvItem, newItem: BalanceListRvItem): Any? { + return when { + oldItem is NetworkAssetUi && newItem is NetworkAssetUi -> NetworkAssetPayloadGenerator.diff(oldItem.asset, newItem.asset) - override fun areChildContentsTheSame(oldItem: AssetModel, newItem: AssetModel): Boolean { - return oldItem == newItem - } + oldItem is TokenAssetUi && newItem is TokenAssetUi -> TokenAssetPayloadGenerator.diff(oldItem.asset, newItem.asset) + + oldItem is TokenGroupUi && newItem is TokenGroupUi -> TokenGroupAssetPayloadGenerator.diff(oldItem, newItem) - override fun getChildChangePayload(oldItem: AssetModel, newItem: AssetModel): Any? { - return AssetPayloadGenerator.diff(oldItem, newItem) + else -> null + } } } -private object AssetPayloadGenerator : PayloadGenerator( +private object NetworkAssetPayloadGenerator : PayloadGenerator( priceRateExtractor, recentChangeExtractor, - AssetModel::amount + amountExtractor +) + +private object TokenAssetPayloadGenerator : PayloadGenerator( + amountExtractor +) + +private object TokenGroupAssetPayloadGenerator : PayloadGenerator( + tokenGroupPriceRateExtractor, + tokenGroupRecentChangeExtractor, + tokenGroupAmountExtractor, + tokenGroupTypeExtractor ) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/ExpandableAssetsMixin.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/ExpandableAssetsMixin.kt new file mode 100644 index 0000000000..71f74fc753 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/ExpandableAssetsMixin.kt @@ -0,0 +1,96 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common + +import io.novafoundation.nova.common.data.model.switch +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.utils.combineToTriple +import io.novafoundation.nova.common.utils.toggle +import io.novafoundation.nova.common.utils.updateValue +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapGroupedAssetsToUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenAssetUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest + +class ExpandableAssetsMixinFactory( + private val assetIconProvider: AssetIconProvider, + private val currencyInteractor: CurrencyInteractor, + private val assetsViewModeRepository: AssetsViewModeRepository, + private val amountFormatter: AmountFormatter +) { + + fun create(assetsFlow: Flow): ExpandableAssetsMixin { + return RealExpandableAssetsMixin(assetsFlow, currencyInteractor, assetIconProvider, assetsViewModeRepository, amountFormatter) + } +} + +interface ExpandableAssetsMixin { + + val assetModelsFlow: Flow> + + fun expandToken(tokenGroupUi: TokenGroupUi) + + suspend fun switchViewMode() +} + +class RealExpandableAssetsMixin( + assetsFlow: Flow, + currencyInteractor: CurrencyInteractor, + private val assetIconProvider: AssetIconProvider, + private val assetsViewModeRepository: AssetsViewModeRepository, + private val amountFormatter: AmountFormatter +) : ExpandableAssetsMixin { + + private val selectedCurrency = currencyInteractor.observeSelectCurrency() + + private val expandedTokenIdsFlow = MutableStateFlow(setOf()) + + override val assetModelsFlow: Flow> = combineToTriple( + assetsFlow, + expandedTokenIdsFlow, + selectedCurrency + ).mapLatest { (assetsByViewMode, expandedTokens, currency) -> + when (assetsByViewMode) { + is AssetsByViewModeResult.ByNetworks -> assetsByViewMode.assets.mapGroupedAssetsToUi(amountFormatter, assetIconProvider, currency) + is AssetsByViewModeResult.ByTokens -> assetsByViewMode.tokens.mapGroupedAssetsToUi( + amountFormatter = amountFormatter, + assetIconProvider = assetIconProvider, + assetFilter = { groupId, assetsInGroup -> filterTokens(groupId, assetsInGroup, expandedTokens) } + ) + } + } + .distinctUntilChanged() + + override fun expandToken(tokenGroupUi: TokenGroupUi) { + expandedTokenIdsFlow.updateValue { it.toggle(tokenGroupUi.itemId) } + } + + override suspend fun switchViewMode() { + expandedTokenIdsFlow.value = emptySet() + + val assetViewMode = assetsViewModeRepository.getAssetViewMode() + assetsViewModeRepository.setAssetsViewMode(assetViewMode.switch()) + } + + private fun filterTokens(groupId: String, assets: List, expandedGroups: Set): List { + if (groupId in expandedGroups) { + return filterIfSingleItem(assets) + } + + return emptyList() + } + + private fun filterIfSingleItem(assets: List): List { + return if (assets.size <= 1) { + emptyList() + } else { + assets + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetGroupingDecoration.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/baseDecoration/AssetBaseDecoration.kt similarity index 80% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetGroupingDecoration.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/baseDecoration/AssetBaseDecoration.kt index 911c5b5680..4365c3460c 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/AssetGroupingDecoration.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/baseDecoration/AssetBaseDecoration.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_assets.presentation.balance.common +package io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration import android.content.Context import android.graphics.Canvas @@ -8,7 +8,6 @@ import android.view.View import androidx.core.view.children import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import io.novafoundation.nova.common.list.GroupedListAdapter import io.novafoundation.nova.common.utils.dp import io.novafoundation.nova.common.view.shape.addRipple import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable @@ -20,17 +19,16 @@ import kotlin.math.roundToInt * The issue is that this decoration does not currently support partial list updates and assumes it will be iterated over whole list * TODO update decoration to not require this invalidation */ -class AssetGroupingDecoration( +class AssetBaseDecoration( private val background: Drawable, private val assetsAdapter: ListAdapter<*, *>, context: Context, + private val preferences: AssetDecorationPreferences ) : RecyclerView.ItemDecoration() { companion object; private val bounds = Rect() - private val groupOuterSpacing = 8.dp(context) - private val groupInnerSpacing = 8.dp(context) // used to hide rounded corners for the last group to simulate effect of not-closed group private val finalGroupExtraPadding = 20.dp(context) @@ -47,7 +45,6 @@ class AssetGroupingDecoration( val bindingPosition = viewHolder.bindingAdapterPosition - val currentType = assetsAdapter.getItemViewType(bindingPosition) val nextType = assetsAdapter.getItemViewTypeOrNull(bindingPosition + 1) if (groupTop == null) { @@ -57,11 +54,11 @@ class AssetGroupingDecoration( when { // if group is finished - isFinalItemInGroup(currentType, nextType) -> { + isFinalItemInGroup(nextType) -> { parent.getDecoratedBoundsWithMargins(view, bounds) bounds.set(view.left, bounds.top, view.right, bounds.bottom) - val groupBottom = bounds.bottom + view.translationY.roundToInt() - groupOuterSpacing + val groupBottom = bounds.bottom + view.translationY.roundToInt() - preferences.outerGroupPadding(viewHolder) background.setBounds(bounds.left, groupTop!!, bounds.right, groupBottom) background.draw(c) @@ -97,10 +94,13 @@ class AssetGroupingDecoration( val adapterPosition = viewHolder.bindingAdapterPosition - val currentType = assetsAdapter.getItemViewTypeOrNull(adapterPosition) val nextType = assetsAdapter.getItemViewTypeOrNull(adapterPosition + 1) - val bottom = if (isFinalItemInGroup(currentType, nextType)) groupInnerSpacing + groupOuterSpacing else 0 + val bottom = if (isFinalItemInGroup(nextType)) { + preferences.outerGroupPadding(viewHolder) + preferences.innerGroupPadding(viewHolder) + } else { + 0 + } outRect.set(0, 0, 0, bottom) } @@ -111,27 +111,31 @@ class AssetGroupingDecoration( return getItemViewType(position) } - private fun isFinalItemInGroup(currentType: Int?, nextType: Int?): Boolean { - return currentType == GroupedListAdapter.TYPE_CHILD && (nextType == GroupedListAdapter.TYPE_GROUP || nextType == null) + private fun isFinalItemInGroup(nextType: Int?): Boolean { + return nextType == null || preferences.isGroupItem(nextType) } private fun shouldSkip(viewHolder: RecyclerView.ViewHolder): Boolean { - return viewHolder.bindingAdapterPosition == RecyclerView.NO_POSITION || - (viewHolder !is AssetViewHolder && viewHolder !is AssetGroupViewHolder) + val noPosition = viewHolder.bindingAdapterPosition == RecyclerView.NO_POSITION + val unsupportedViewHolder = !preferences.shouldUseViewHolder(viewHolder) + + return noPosition || unsupportedViewHolder } } -fun AssetGroupingDecoration.Companion.applyDefaultTo( +fun AssetBaseDecoration.Companion.applyDefaultTo( recyclerView: RecyclerView, - adapter: ListAdapter<*, *> + adapter: ListAdapter<*, *>, + preferences: AssetDecorationPreferences = NetworkAssetDecorationPreferences() ) { val groupBackground = with(recyclerView.context) { addRipple(getRoundedCornerDrawable(R.color.block_background)) } - val decoration = AssetGroupingDecoration( + val decoration = AssetBaseDecoration( background = groupBackground, assetsAdapter = adapter, context = recyclerView.context, + preferences = preferences ) recyclerView.addItemDecoration(decoration) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/baseDecoration/AssetDecorationPreferences.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/baseDecoration/AssetDecorationPreferences.kt new file mode 100644 index 0000000000..8f8ae0f338 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/baseDecoration/AssetDecorationPreferences.kt @@ -0,0 +1,81 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration + +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import io.novafoundation.nova.common.utils.dp +import io.novafoundation.nova.feature_assets.presentation.balance.common.TYPE_NETWORK_GROUP +import io.novafoundation.nova.feature_assets.presentation.balance.common.TYPE_TOKEN_GROUP +import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.NetworkAssetGroupViewHolder +import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.NetworkAssetViewHolder +import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.TokenAssetGroupViewHolder + +interface AssetDecorationPreferences { + + fun innerGroupPadding(viewHolder: ViewHolder): Int + + fun outerGroupPadding(viewHolder: ViewHolder): Int + + fun isGroupItem(viewType: Int): Boolean + + fun shouldUseViewHolder(viewHolder: ViewHolder): Boolean +} + +class NetworkAssetDecorationPreferences : AssetDecorationPreferences { + + override fun innerGroupPadding(viewHolder: ViewHolder): Int { + return 8.dp(viewHolder.itemView.context) + } + + override fun outerGroupPadding(viewHolder: ViewHolder): Int { + return 8.dp(viewHolder.itemView.context) + } + + override fun isGroupItem(viewType: Int): Boolean { + return viewType == TYPE_NETWORK_GROUP + } + + override fun shouldUseViewHolder(viewHolder: ViewHolder): Boolean { + return viewHolder is NetworkAssetViewHolder || + viewHolder is NetworkAssetGroupViewHolder + } +} + +class TokenAssetGroupDecorationPreferences : AssetDecorationPreferences { + + override fun innerGroupPadding(viewHolder: ViewHolder): Int { + return 0 + } + + override fun outerGroupPadding(viewHolder: ViewHolder): Int { + return 8.dp(viewHolder.itemView.context) + } + + override fun isGroupItem(viewType: Int): Boolean { + return viewType == TYPE_TOKEN_GROUP + } + + override fun shouldUseViewHolder(viewHolder: ViewHolder): Boolean { + return viewHolder is TokenAssetGroupViewHolder + } +} + +class CompoundAssetDecorationPreferences(private vararg val preferences: AssetDecorationPreferences) : AssetDecorationPreferences { + + override fun innerGroupPadding(viewHolder: ViewHolder): Int { + val firstPreferences = preferences.firstOrNull { it.shouldUseViewHolder(viewHolder) } + return firstPreferences?.innerGroupPadding(viewHolder) ?: 0 + } + + override fun outerGroupPadding(viewHolder: ViewHolder): Int { + val firstPreferences = preferences.firstOrNull { it.shouldUseViewHolder(viewHolder) } + return firstPreferences?.outerGroupPadding(viewHolder) ?: 0 + } + + override fun isGroupItem(viewType: Int): Boolean { + return preferences.any { it.isGroupItem(viewType) } + } + + override fun shouldUseViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean { + return preferences.any { it.shouldUseViewHolder(viewHolder) } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/NetworkAssetGroupViewHolder.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/NetworkAssetGroupViewHolder.kt new file mode 100644 index 0000000000..352490f828 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/NetworkAssetGroupViewHolder.kt @@ -0,0 +1,17 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common.holders + +import android.view.View +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkGroupUi +import kotlinx.android.synthetic.main.item_network_asset_group.view.itemAssetGroupBalance +import kotlinx.android.synthetic.main.item_network_asset_group.view.itemAssetGroupChain + +class NetworkAssetGroupViewHolder( + containerView: View, +) : GroupedListHolder(containerView) { + + fun bind(assetGroup: NetworkGroupUi) = with(containerView) { + itemAssetGroupChain.setChain(assetGroup.chainUi) + itemAssetGroupBalance.text = assetGroup.groupBalanceFiat + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/NetworkAssetViewHolder.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/NetworkAssetViewHolder.kt new file mode 100644 index 0000000000..c743f884f2 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/NetworkAssetViewHolder.kt @@ -0,0 +1,51 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common.holders + +import android.view.View +import coil.ImageLoader +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.common.utils.setTextColorRes +import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon +import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkAssetUi +import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import kotlinx.android.synthetic.main.item_network_asset.view.itemAssetBalance +import kotlinx.android.synthetic.main.item_network_asset.view.itemAssetImage +import kotlinx.android.synthetic.main.item_network_asset.view.itemAssetPriceAmount +import kotlinx.android.synthetic.main.item_network_asset.view.itemAssetRate +import kotlinx.android.synthetic.main.item_network_asset.view.itemAssetRateChange +import kotlinx.android.synthetic.main.item_network_asset.view.itemAssetToken + +class NetworkAssetViewHolder( + containerView: View, + private val imageLoader: ImageLoader, +) : GroupedListHolder(containerView) { + + fun bind(networkAsset: NetworkAssetUi, itemHandler: BalanceListAdapter.ItemAssetHandler) = with(containerView) { + val asset = networkAsset.asset + itemAssetImage.setTokenIcon(networkAsset.icon, imageLoader) + + bindPriceInfo(asset) + + bindRecentChange(asset) + + bindTotal(asset) + + itemAssetToken.text = asset.token.configuration.symbol.value + + setOnClickListener { itemHandler.assetClicked(asset.token.configuration) } + } + + fun bindTotal(asset: AssetModel) { + containerView.itemAssetBalance.text = asset.amount.token + containerView.itemAssetPriceAmount.text = asset.amount.fiat + } + + fun bindRecentChange(asset: AssetModel) = with(containerView) { + itemAssetRateChange.setTextColorRes(asset.token.rateChangeColorRes) + itemAssetRateChange.text = asset.token.recentRateChange + } + + fun bindPriceInfo(asset: AssetModel) = with(containerView) { + itemAssetRate.text = asset.token.rate + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/TokenAssetGroupViewHolder.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/TokenAssetGroupViewHolder.kt new file mode 100644 index 0000000000..06a6872b1c --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/TokenAssetGroupViewHolder.kt @@ -0,0 +1,84 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common.holders + +import android.view.View +import coil.ImageLoader +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableParentViewHolder +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableParentItem +import io.novafoundation.nova.common.utils.setTextColorRes +import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon +import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import kotlinx.android.synthetic.main.item_token_asset_group.view.itemAssetTokenGroupBalance +import kotlinx.android.synthetic.main.item_token_asset_group.view.itemAssetTokenGroupPriceAmount +import kotlinx.android.synthetic.main.item_token_asset_group.view.itemAssetTokenGroupRate +import kotlinx.android.synthetic.main.item_token_asset_group.view.itemAssetTokenGroupRateChange +import kotlinx.android.synthetic.main.item_token_asset_group.view.itemAssetTokenGroupToken +import kotlinx.android.synthetic.main.item_token_asset_group.view.itemTokenGroupAssetImage + +class TokenAssetGroupViewHolder( + containerView: View, + private val imageLoader: ImageLoader, + private val itemHandler: BalanceListAdapter.ItemAssetHandler, +) : GroupedListHolder(containerView), ExpandableParentViewHolder { + + override var expandableItem: ExpandableParentItem? = null + + fun bind(tokenGroup: TokenGroupUi) = with(containerView) { + expandableItem = tokenGroup + + itemTokenGroupAssetImage.setTokenIcon(tokenGroup.tokenIcon, imageLoader) + + bindPriceRateInternal(tokenGroup) + + bindRecentChangeInternal(tokenGroup) + + bindTotalInternal(tokenGroup) + + updateListener(tokenGroup) + + itemAssetTokenGroupToken.text = tokenGroup.tokenSymbol + } + + fun bindTotal(networkAsset: TokenGroupUi) { + updateListener(networkAsset) + bindTotalInternal(networkAsset) + } + + fun bindRecentChange(networkAsset: TokenGroupUi) { + updateListener(networkAsset) + bindRecentChangeInternal(networkAsset) + } + + fun bindPriceRate(networkAsset: TokenGroupUi) { + updateListener(networkAsset) + bindPriceRateInternal(networkAsset) + } + + fun bindGroupType(networkAsset: TokenGroupUi) { + updateListener(networkAsset) + } + + private fun bindTotalInternal(networkAsset: TokenGroupUi) { + val balance = networkAsset.balance + containerView.itemAssetTokenGroupBalance.text = balance.token + containerView.itemAssetTokenGroupPriceAmount.text = balance.fiat + } + + private fun bindRecentChangeInternal(networkAsset: TokenGroupUi) { + with(containerView) { + itemAssetTokenGroupRateChange.setTextColorRes(networkAsset.rateChangeColorRes) + itemAssetTokenGroupRateChange.text = networkAsset.recentRateChange + } + } + + private fun bindPriceRateInternal(networkAsset: TokenGroupUi) { + with(containerView) { + itemAssetTokenGroupRate.text = networkAsset.rate + } + } + + private fun updateListener(tokenGroupUi: TokenGroupUi) { + containerView.setOnClickListener { itemHandler.tokenGroupClicked(tokenGroupUi) } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/TokenAssetViewHolder.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/TokenAssetViewHolder.kt new file mode 100644 index 0000000000..5045e2854d --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/holders/TokenAssetViewHolder.kt @@ -0,0 +1,46 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common.holders + +import android.view.View +import coil.ImageLoader +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableChildViewHolder +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableChildItem +import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon +import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon +import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenAssetUi +import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import kotlinx.android.synthetic.main.item_token_asset.view.itemTokenAssetImage +import kotlinx.android.synthetic.main.item_token_asset.view.itemTokenAssetToken +import kotlinx.android.synthetic.main.item_token_asset.view.itemTokenAssetBalance +import kotlinx.android.synthetic.main.item_token_asset.view.itemTokenAssetChainIcon +import kotlinx.android.synthetic.main.item_token_asset.view.itemTokenAssetChainName +import kotlinx.android.synthetic.main.item_token_asset.view.itemTokenAssetPriceAmount + +class TokenAssetViewHolder( + containerView: View, + private val imageLoader: ImageLoader, +) : GroupedListHolder(containerView), ExpandableChildViewHolder { + + override var expandableItem: ExpandableChildItem? = null + + fun bind(tokenAsset: TokenAssetUi, itemHandler: BalanceListAdapter.ItemAssetHandler) = with(containerView) { + expandableItem = tokenAsset + + val asset = tokenAsset.asset + itemTokenAssetImage.setTokenIcon(tokenAsset.assetIcon, imageLoader) + itemTokenAssetChainIcon.loadChainIcon(tokenAsset.chain.icon, imageLoader) + itemTokenAssetChainName.text = tokenAsset.chain.name + + bindTotal(asset) + + itemTokenAssetToken.text = asset.token.configuration.symbol.value + + setOnClickListener { itemHandler.assetClicked(asset.token.configuration) } + } + + fun bindTotal(asset: AssetModel) { + containerView.itemTokenAssetBalance.text = asset.amount.token + containerView.itemTokenAssetPriceAmount.text = asset.amount.fiat + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/AssetMappersCommon.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/AssetMappersCommon.kt new file mode 100644 index 0000000000..488f2fc06e --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/AssetMappersCommon.kt @@ -0,0 +1,62 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common.mappers + +import androidx.annotation.ColorRes +import io.novafoundation.nova.common.utils.formatting.formatAsChange +import io.novafoundation.nova.common.utils.isNonNegative +import io.novafoundation.nova.common.utils.isZero +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount +import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import io.novafoundation.nova.feature_assets.presentation.model.TokenModel +import io.novafoundation.nova.feature_currency_api.domain.model.Currency +import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRateChange +import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.model.formatBalanceWithFraction +import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +import java.math.BigDecimal + +fun mapCoinRateChange(coinRateChange: CoinRateChange?, currency: Currency): String { + val rateChange = coinRateChange?.rate + return rateChange.orZero().formatAsCurrency(currency) +} + +fun mapAssetToAssetModel( + amountFormatter: AmountFormatter, + asset: Asset, + balance: PricedAmount +): AssetModel { + return AssetModel( + token = mapTokenToTokenModel(asset.token), + amount = mapAmountToAmountModel( + amount = balance.amount, + asset = asset, + includeAssetTicker = false + ).formatBalanceWithFraction(amountFormatter, R.dimen.asset_balance_fraction_size) + ) +} + +@ColorRes +fun mapCoinRateChangeColorRes(coinRateChange: CoinRateChange?): Int { + val rateChange = coinRateChange?.recentRateChange + + return when { + rateChange == null || rateChange.isZero -> R.color.text_secondary + rateChange.isNonNegative -> R.color.text_positive + else -> R.color.text_negative + } +} + +fun mapTokenToTokenModel(token: Token): TokenModel { + return with(token) { + TokenModel( + configuration = configuration, + rate = mapCoinRateChange(token.coinRate, token.currency), + recentRateChange = (coinRate?.recentRateChange ?: BigDecimal.ZERO).formatAsChange(), + rateChangeColorRes = mapCoinRateChangeColorRes(coinRate) + ) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/NetworkAssetMappers.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/NetworkAssetMappers.kt new file mode 100644 index 0000000000..e66fb4a8cb --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/NetworkAssetMappers.kt @@ -0,0 +1,56 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common.mappers + +import io.novafoundation.nova.common.list.GroupedList +import io.novafoundation.nova.common.list.toListWithHeaders +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.AssetBalance +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkAssetUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkGroupUi +import io.novafoundation.nova.feature_currency_api.domain.model.Currency +import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import java.math.BigDecimal + +fun GroupedList.mapGroupedAssetsToUi( + amountFormatter: AmountFormatter, + assetIconProvider: AssetIconProvider, + currency: Currency, + groupBalance: (NetworkAssetGroup) -> BigDecimal = NetworkAssetGroup::groupTotalBalanceFiat, + balance: (AssetBalance) -> PricedAmount = AssetBalance::total, +): List { + return mapKeys { (assetGroup, _) -> mapAssetGroupToUi(assetGroup, currency, groupBalance) } + .mapValues { (_, assets) -> mapAssetsToAssetModels(amountFormatter, assetIconProvider, assets, balance) } + .toListWithHeaders() + .filterIsInstance() +} + +private fun mapAssetsToAssetModels( + amountFormatter: AmountFormatter, + assetIconProvider: AssetIconProvider, + assets: List, + balance: (AssetBalance) -> PricedAmount +): List { + return assets.map { + NetworkAssetUi( + mapAssetToAssetModel(amountFormatter, it.asset, balance(it.balanceWithOffchain)), + assetIconProvider.getAssetIconOrFallback(it.asset.token.configuration) + ) + } +} + +fun mapAssetGroupToUi( + assetGroup: NetworkAssetGroup, + currency: Currency, + groupBalance: (NetworkAssetGroup) -> BigDecimal +): NetworkGroupUi { + return NetworkGroupUi( + chainUi = mapChainToUi(assetGroup.chain), + groupBalanceFiat = groupBalance(assetGroup).formatAsCurrency(currency) + ) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/TokenAssetMappers.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/TokenAssetMappers.kt new file mode 100644 index 0000000000..49ba3823a1 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/common/mappers/TokenAssetMappers.kt @@ -0,0 +1,90 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.common.mappers + +import io.novafoundation.nova.common.list.GroupedList +import io.novafoundation.nova.common.list.toListWithHeaders +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.presentation.getAssetIconOrFallback +import io.novafoundation.nova.common.utils.formatTokenAmount +import io.novafoundation.nova.common.utils.formatting.formatAsChange +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetBalance +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenAssetUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel +import io.novafoundation.nova.feature_wallet_api.presentation.model.formatBalanceWithFraction + +fun GroupedList.mapGroupedAssetsToUi( + amountFormatter: AmountFormatter, + assetIconProvider: AssetIconProvider, + assetFilter: (groupId: String, List) -> List = { _, assets -> assets }, + groupBalance: (TokenAssetGroup) -> PricedAmount = { it.groupBalance.total }, + balance: (AssetBalance) -> PricedAmount = AssetBalance::total, +): List { + return mapKeys { (group, assets) -> mapTokenAssetGroupToUi(amountFormatter, assetIconProvider, group, assets, groupBalance) } + .mapValues { (group, assets) -> + val assetModels = mapAssetsToAssetModels(amountFormatter, assetIconProvider, group, assets, balance) + assetFilter(group.itemId, assetModels) + } + .toListWithHeaders() + .filterIsInstance() +} + +fun mapTokenAssetGroupToUi( + amountFormatter: AmountFormatter, + assetIconProvider: AssetIconProvider, + assetGroup: TokenAssetGroup, + assets: List, + groupBalance: (TokenAssetGroup) -> PricedAmount = { it.groupBalance.total } +): TokenGroupUi { + val balance = groupBalance(assetGroup) + return TokenGroupUi( + itemId = assetGroup.groupId, + tokenIcon = assetIconProvider.getAssetIconOrFallback(assetGroup.token.icon), + rate = mapCoinRateChange(assetGroup.token.coinRate, assetGroup.token.currency), + recentRateChange = assetGroup.token.coinRate?.recentRateChange.orZero().formatAsChange(), + rateChangeColorRes = mapCoinRateChangeColorRes(assetGroup.token.coinRate), + tokenSymbol = assetGroup.token.symbol.value, + singleItemGroup = assetGroup.itemsCount <= 1, + balance = AmountModel( + token = balance.amount.formatTokenAmount(), + fiat = balance.fiat.formatAsCurrency(assetGroup.token.currency) + ).formatBalanceWithFraction(amountFormatter, R.dimen.asset_balance_fraction_size), + groupType = mapType(assets) + ) +} + +private fun mapAssetsToAssetModels( + amountFormatter: AmountFormatter, + assetIconProvider: AssetIconProvider, + group: TokenGroupUi, + assets: List, + balance: (AssetBalance) -> PricedAmount +): List { + return assets.map { + TokenAssetUi( + group.getId(), + mapAssetToAssetModel(amountFormatter, it.asset, balance(it.balanceWithOffChain)), + assetIconProvider.getAssetIconOrFallback(it.asset.token.configuration), + mapChainToUi(it.chain) + ) + } +} + +private fun mapType( + assets: List, +): TokenGroupUi.GroupType { + return if (assets.size == 1) { + TokenGroupUi.GroupType.SingleItem(assets.first().asset.token.configuration) + } else { + TokenGroupUi.GroupType.Group + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/AssetDetailsModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/AssetDetailsModel.kt index 438cb5c8c8..73731acf2f 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/AssetDetailsModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/AssetDetailsModel.kt @@ -1,10 +1,12 @@ package io.novafoundation.nova.feature_assets.presentation.balance.detail +import io.novafoundation.nova.common.utils.images.Icon import io.novafoundation.nova.feature_assets.presentation.model.TokenModel import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel class AssetDetailsModel( val token: TokenModel, + val assetIcon: Icon, val total: AmountModel, val transferable: AmountModel, val locked: AmountModel diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailFragment.kt index 0684fd9f62..eb666f3f80 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailFragment.kt @@ -11,7 +11,7 @@ import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.applyBarMargin import io.novafoundation.nova.common.utils.hideKeyboard import io.novafoundation.nova.common.utils.setTextColorRes -import io.novafoundation.nova.feature_account_api.presenatation.chain.loadTokenIcon +import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent @@ -120,7 +120,7 @@ class BalanceDetailFragment : BaseFragment() { } viewModel.assetDetailsModel.observe { asset -> - balanceDetailTokenIcon.loadTokenIcon(asset.token.configuration.iconUrl, imageLoader) + balanceDetailTokenIcon.setTokenIcon(asset.assetIcon, imageLoader) balanceDetailTokenName.text = asset.token.configuration.symbol.value balanceDetailRate.text = asset.token.rate diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailViewModel.kt index 26e0f2bd36..d6d1a4cc19 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailViewModel.kt @@ -4,11 +4,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.sumByBigInteger import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor @@ -16,7 +18,7 @@ import io.novafoundation.nova.feature_assets.domain.locks.BalanceLocksInteractor import io.novafoundation.nova.feature_assets.domain.send.SendInteractor import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.balance.common.mapTokenToTokenModel +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapTokenToTokenModel import io.novafoundation.nova.feature_assets.presentation.model.BalanceLocksModel import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_assets.presentation.transaction.filter.TransactionHistoryFilterPayload @@ -63,7 +65,8 @@ class BalanceDetailViewModel( private val currencyInteractor: CurrencyInteractor, private val controllableAssetCheck: ControllableAssetCheckMixin, private val externalBalancesInteractor: ExternalBalancesInteractor, - private val swapAvailabilityInteractor: SwapAvailabilityInteractor + private val swapAvailabilityInteractor: SwapAvailabilityInteractor, + private val assetIconProvider: AssetIconProvider ) : BaseViewModel(), TransactionHistoryUi by transactionHistoryMixin { @@ -204,7 +207,8 @@ class BalanceDetailViewModel( token = mapTokenToTokenModel(asset.token), total = mapAmountToAmountModel(asset.total + totalContributed, asset), transferable = mapAmountToAmountModel(asset.transferable, asset), - locked = mapAmountToAmountModel(asset.locked + totalContributed, asset) + locked = mapAmountToAmountModel(asset.locked + totalContributed, asset), + assetIcon = assetIconProvider.getAssetIconOrFallback(asset.token.configuration) ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/di/BalanceDetailModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/di/BalanceDetailModule.kt index 224b1b101d..801c3ab5d6 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/di/BalanceDetailModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/di/BalanceDetailModule.kt @@ -9,6 +9,7 @@ import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.scope.ScreenScope import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase @@ -64,7 +65,8 @@ class BalanceDetailModule { assetPayload: AssetPayload, addressDisplayUseCase: AddressDisplayUseCase, chainRegistry: ChainRegistry, - currencyRepository: CurrencyRepository + currencyRepository: CurrencyRepository, + assetIconProvider: AssetIconProvider ): TransactionHistoryMixin { return TransactionHistoryProvider( walletInteractor = walletInteractor, @@ -76,7 +78,8 @@ class BalanceDetailModule { chainRegistry = chainRegistry, chainId = assetPayload.chainId, assetId = assetPayload.chainAssetId, - currencyRepository = currencyRepository + currencyRepository = currencyRepository, + assetIconProvider ) } @@ -96,7 +99,8 @@ class BalanceDetailModule { currencyInteractor: CurrencyInteractor, controllableAssetCheckMixin: ControllableAssetCheckMixin, externalBalancesInteractor: ExternalBalancesInteractor, - swapAvailabilityInteractor: SwapAvailabilityInteractor + swapAvailabilityInteractor: SwapAvailabilityInteractor, + assetIconProvider: AssetIconProvider ): ViewModel { return BalanceDetailViewModel( walletInteractor = walletInteractor, @@ -111,7 +115,8 @@ class BalanceDetailModule { currencyInteractor = currencyInteractor, controllableAssetCheck = controllableAssetCheckMixin, externalBalancesInteractor = externalBalancesInteractor, - swapAvailabilityInteractor = swapAvailabilityInteractor + swapAvailabilityInteractor = swapAvailabilityInteractor, + assetIconProvider = assetIconProvider ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/AssetFiltersBottomSheetFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/AssetFiltersBottomSheetFragment.kt deleted file mode 100644 index c1f2c52ff7..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/AssetFiltersBottomSheetFragment.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.balance.filters - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope -import io.novafoundation.nova.common.base.BaseBottomSheetFragment -import io.novafoundation.nova.feature_assets.R -import io.novafoundation.nova.common.di.FeatureUtils -import io.novafoundation.nova.common.view.bindFromMap -import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi -import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent -import io.novafoundation.nova.feature_assets.domain.assets.filters.NonZeroBalanceFilter -import kotlinx.android.synthetic.main.fragment_asset_filters.assetsFilterSwitchZeroBalances - -class AssetFiltersBottomSheetFragment : BaseBottomSheetFragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = layoutInflater.inflate(R.layout.fragment_asset_filters, container, false) - - override fun initViews() {} - - override fun inject() { - FeatureUtils.getFeature( - requireContext(), - AssetsFeatureApi::class.java - ) - .assetFiltersComponentFactory() - .create(this) - .inject(this) - } - - override fun subscribe(viewModel: AssetFiltersViewModel) { - assetsFilterSwitchZeroBalances.bindFromMap(NonZeroBalanceFilter, viewModel.filtersEnabledMap, viewLifecycleOwner.lifecycleScope) - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/AssetFiltersViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/AssetFiltersViewModel.kt deleted file mode 100644 index dc5ac10761..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/AssetFiltersViewModel.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.balance.filters - -import io.novafoundation.nova.common.base.BaseViewModel -import io.novafoundation.nova.common.utils.checkEnabled -import io.novafoundation.nova.common.utils.combineIdentity -import io.novafoundation.nova.common.utils.flowOf -import io.novafoundation.nova.common.utils.inBackground -import io.novafoundation.nova.feature_assets.domain.assets.filters.AssetFilter -import io.novafoundation.nova.feature_assets.domain.assets.filters.AssetFiltersInteractor -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -class AssetFiltersViewModel( - private val interactor: AssetFiltersInteractor, -) : BaseViewModel() { - - private val initialFilters = flowOf { interactor.currentFilters() } - .inBackground() - .share() - - val filtersEnabledMap = createFilterEnabledMap() - - init { - applyInitialState() - filtersEnabledMap.applyOnChange() - } - - private fun applyInitialState() = launch { - val initialFilters = initialFilters.first() - - filtersEnabledMap.forEach { (filter, checked) -> - checked.value = filter in initialFilters - } - } - - private fun createFilterEnabledMap() = interactor.allFilters.associateWith { MutableStateFlow(false) } - - private fun Map>.applyOnChange() { - combineIdentity(this.values) - .drop(1) - .onEach { - applyChanges() - } - .launchIn(this@AssetFiltersViewModel) - } - - private fun applyChanges() { - val filters = interactor.allFilters.filter(filtersEnabledMap::checkEnabled) - interactor.updateFilters(filters) - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/di/AssetFiltersComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/di/AssetFiltersComponent.kt deleted file mode 100644 index d377cd4429..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/di/AssetFiltersComponent.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.balance.filters.di - -import androidx.fragment.app.Fragment -import dagger.BindsInstance -import dagger.Subcomponent -import io.novafoundation.nova.common.di.scope.ScreenScope -import io.novafoundation.nova.feature_assets.presentation.balance.filters.AssetFiltersBottomSheetFragment - -@Subcomponent( - modules = [ - AssetFiltersModule::class - ] -) -@ScreenScope -interface AssetFiltersComponent { - - @Subcomponent.Factory - interface Factory { - - fun create( - @BindsInstance fragment: Fragment, - ): AssetFiltersComponent - } - - fun inject(fragment: AssetFiltersBottomSheetFragment) -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/di/AssetFiltersModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/di/AssetFiltersModule.kt deleted file mode 100644 index 12ae0fe7e5..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/filters/di/AssetFiltersModule.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.balance.filters.di - -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import dagger.Module -import dagger.Provides -import dagger.multibindings.IntoMap -import io.novafoundation.nova.common.di.scope.ScreenScope -import io.novafoundation.nova.common.di.viewmodel.ViewModelKey -import io.novafoundation.nova.common.di.viewmodel.ViewModelModule -import io.novafoundation.nova.feature_assets.data.repository.assetFilters.AssetFiltersRepository -import io.novafoundation.nova.feature_assets.domain.assets.filters.AssetFiltersInteractor -import io.novafoundation.nova.feature_assets.presentation.balance.filters.AssetFiltersViewModel - -@Module(includes = [ViewModelModule::class]) -class AssetFiltersModule { - - @Provides - @ScreenScope - fun provideInteractor( - assetFiltersRepository: AssetFiltersRepository - ) = AssetFiltersInteractor(assetFiltersRepository) - - @Provides - @IntoMap - @ViewModelKey(AssetFiltersViewModel::class) - fun provideViewModel( - interactor: AssetFiltersInteractor, - ): ViewModel { - return AssetFiltersViewModel( - interactor = interactor, - ) - } - - @Provides - fun provideViewModelCreator( - fragment: Fragment, - viewModelFactory: ViewModelProvider.Factory - ): AssetFiltersViewModel { - return ViewModelProvider(fragment, viewModelFactory).get(AssetFiltersViewModel::class.java) - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt index ccd2a52162..90c9211b52 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt @@ -10,15 +10,22 @@ import dev.chrisbanes.insetter.applyInsetter import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.hideKeyboard +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.BalanceBreakdownBottomSheet -import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetGroupingDecoration +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.AssetBaseDecoration +import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetTokensDecoration +import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetTokensItemAnimator import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter -import io.novafoundation.nova.feature_assets.presentation.balance.common.applyDefaultTo +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.applyDefaultTo +import io.novafoundation.nova.feature_assets.presentation.balance.common.createForAssets +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetsHeaderAdapter -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.android.synthetic.main.fragment_balance_list.balanceListAssets import kotlinx.android.synthetic.main.fragment_balance_list.walletContainer import javax.inject.Inject @@ -65,10 +72,13 @@ class BalanceListFragment : balanceListAssets.setHasFixedSize(true) balanceListAssets.adapter = adapter - AssetGroupingDecoration.applyDefaultTo(balanceListAssets, assetsAdapter) + val animationSettings = ExpandableAnimationSettings.createForAssets() + val animator = ExpandableAnimator(balanceListAssets, animationSettings, assetsAdapter) - // modification animations only harm here - balanceListAssets.itemAnimator = null + balanceListAssets.addItemDecoration(AssetTokensDecoration(requireContext(), assetsAdapter, animator)) + balanceListAssets.itemAnimator = AssetTokensItemAnimator(animationSettings, animator) + + AssetBaseDecoration.applyDefaultTo(balanceListAssets, assetsAdapter) walletContainer.setOnRefreshListener { viewModel.fullSync() @@ -86,7 +96,7 @@ class BalanceListFragment : } override fun subscribe(viewModel: BalanceListViewModel) { - viewModel.assetModelsFlow.observe { + viewModel.assetListMixin.assetModelsFlow.observe { assetsAdapter.submitList(it) { balanceListAssets?.invalidateItemDecorations() } @@ -129,12 +139,25 @@ class BalanceListFragment : viewModel.filtersIndicatorIcon.observe(headerAdapter::setFilterIconRes) viewModel.shouldShowCrowdloanBanner.observe(headerAdapter::setCrowdloanBannerVisible) + + viewModel.assetViewModeModelFlow.observe { headerAdapter.setAssetViewModeModel(it) } } - override fun assetClicked(asset: AssetModel) { + override fun assetClicked(asset: Chain.Asset) { viewModel.assetClicked(asset) } + override fun tokenGroupClicked(tokenGroup: TokenGroupUi) { + if (tokenGroup.groupType is TokenGroupUi.GroupType.SingleItem) { + viewModel.assetClicked(tokenGroup.groupType.asset) + } else { + val itemAnimator = balanceListAssets.itemAnimator as AssetTokensItemAnimator + itemAnimator.prepareForAnimation() + + viewModel.assetListMixin.expandToken(tokenGroup) + } + } + override fun totalBalanceClicked() { viewModel.balanceBreakdownClicked() } @@ -147,10 +170,6 @@ class BalanceListFragment : viewModel.searchClicked() } - override fun filtersClicked() { - viewModel.filtersClicked() - } - override fun avatarClicked() { viewModel.avatarClicked() } @@ -183,6 +202,10 @@ class BalanceListFragment : viewModel.crowdloanBannerCloseClicked() } + override fun assetViewModeClicked() { + viewModel.switchViewMode() + } + override fun swapClicked() { viewModel.swapClicked() } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt index 5fb4dab4b3..59265f0fdb 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt @@ -1,25 +1,20 @@ package io.novafoundation.nova.feature_assets.presentation.balance.list -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.style.AbsoluteSizeSpan -import android.text.style.ForegroundColorSpan import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.data.model.AssetViewMode import io.novafoundation.nova.common.presentation.LoadingState import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.formatting.format import io.novafoundation.nova.common.utils.formatting.formatAsPercentage -import io.novafoundation.nova.common.utils.formatting.toAmountWithFraction import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.domain.WalletInteractor -import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdown import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdownInteractor @@ -28,21 +23,25 @@ import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.mode import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.BalanceBreakdownItem import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.BalanceBreakdownTotal import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.TotalBalanceBreakdownModel -import io.novafoundation.nova.feature_assets.presentation.balance.common.mapGroupedAssetsToUi +import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetListMixinFactory +import io.novafoundation.nova.feature_wallet_api.presentation.model.formatBalanceWithFraction import io.novafoundation.nova.feature_assets.presentation.balance.list.model.NftPreviewUi import io.novafoundation.nova.feature_assets.presentation.balance.list.model.TotalBalanceModel -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetViewModeModel import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_currency_api.domain.model.Currency import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency import io.novafoundation.nova.feature_currency_api.presentation.formatters.simpleFormatAsCurrency import io.novafoundation.nova.feature_nft_api.data.model.Nft import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.formatters.mapBalanceIdToUi +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase import io.novafoundation.nova.feature_wallet_connect_api.presentation.mapNumberOfActiveSessionsToUi +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.async import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce @@ -68,10 +67,11 @@ class BalanceListViewModel( private val router: AssetsRouter, private val currencyInteractor: CurrencyInteractor, private val balanceBreakdownInteractor: BalanceBreakdownInteractor, - private val externalBalancesInteractor: ExternalBalancesInteractor, private val resourceManager: ResourceManager, private val walletConnectSessionsUseCase: WalletConnectSessionsUseCase, - private val swapAvailabilityInteractor: SwapAvailabilityInteractor + private val swapAvailabilityInteractor: SwapAvailabilityInteractor, + private val assetListMixinFactory: AssetListMixinFactory, + private val amountFormatter: AmountFormatter ) : BaseViewModel() { private val _hideRefreshEvent = MutableLiveData>() @@ -89,9 +89,9 @@ class BalanceListViewModel( walletInteractor::syncAllNfts ) - private val assetsFlow = walletInteractor.assetsFlow() + val assetListMixin = assetListMixinFactory.create(viewModelScope) - private val filteredAssetsFlow = walletInteractor.filterAssets(assetsFlow) + private val externalBalancesFlow = assetListMixin.externalBalancesFlow private val isFiltersEnabledFlow = walletInteractor.isFiltersEnabledFlow() @@ -105,10 +105,7 @@ class BalanceListViewModel( val selectedWalletModelFlow = selectedAccountUseCase.selectedWalletModelFlow() .shareInBackground() - private val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances() - .shareInBackground() - - private val balanceBreakdown = balanceBreakdownInteractor.balanceBreakdownFlow(assetsFlow, externalBalancesFlow) + private val balanceBreakdown = balanceBreakdownInteractor.balanceBreakdownFlow(assetListMixin.assetsFlow, externalBalancesFlow) .shareInBackground() private val nftsPreviews = assetsListInteractor.observeNftPreviews() @@ -125,12 +122,6 @@ class BalanceListViewModel( .inBackground() .share() - val assetModelsFlow = combine(filteredAssetsFlow, selectedCurrency, externalBalancesFlow) { assets, currency, externalBalances -> - walletInteractor.groupAssets(assets, externalBalances).mapGroupedAssetsToUi(currency) - } - .distinctUntilChanged() - .shareInBackground() - val totalBalanceFlow = combine( balanceBreakdown, swapAvailabilityInteractor.anySwapAvailableFlow() @@ -138,7 +129,7 @@ class BalanceListViewModel( val currency = selectedCurrency.first() TotalBalanceModel( isBreakdownAbailable = breakdown.breakdown.isNotEmpty(), - totalBalanceFiat = breakdown.total.simpleFormatAsCurrency(currency).formatAsTotalBalance(), + totalBalanceFiat = breakdown.total.simpleFormatAsCurrency(currency).formatBalanceWithFraction(amountFormatter, R.dimen.total_balance_fraction_size), lockedBalanceFiat = breakdown.locksTotal.amount.formatAsCurrency(currency), enableSwap = swapSupported ) @@ -146,7 +137,7 @@ class BalanceListViewModel( .inBackground() .share() - val shouldShowPlaceholderFlow = filteredAssetsFlow.map { it.isEmpty() } + val shouldShowPlaceholderFlow = assetListMixin.assetModelsFlow.map { it.isEmpty() } val balanceBreakdownFlow = balanceBreakdown.map { val currency = selectedCurrency.first() @@ -171,6 +162,13 @@ class BalanceListViewModel( val shouldShowCrowdloanBanner = assetsListInteractor.shouldShowCrowdloansBanner() .shareInBackground() + val assetViewModeModelFlow = assetListMixin.assetsViewModeFlow.map { + when (it) { + AssetViewMode.NETWORKS -> AssetViewModeModel(R.drawable.ic_asset_view_networks, R.string.asset_view_networks) + AssetViewMode.TOKENS -> AssetViewModeModel(R.drawable.ic_asset_view_tokens, R.string.asset_view_tokens) + } + }.distinctUntilChanged() + init { selectedCurrency .onEach { fullSync() } @@ -203,10 +201,10 @@ class BalanceListViewModel( } } - fun assetClicked(asset: AssetModel) { + fun assetClicked(asset: Chain.Asset) { val payload = AssetPayload( - chainId = asset.token.configuration.chainId, - chainAssetId = asset.token.configuration.id + chainId = asset.chainId, + chainAssetId = asset.id ) router.openAssetDetails(payload) @@ -216,10 +214,6 @@ class BalanceListViewModel( router.openSwitchWallet() } - fun filtersClicked() { - router.openAssetFilters() - } - fun manageClicked() { router.openManageTokens() } @@ -299,27 +293,6 @@ class BalanceListViewModel( } } - private fun String.formatAsTotalBalance(): CharSequence { - val amountWithFraction = toAmountWithFraction() - - val textColor = resourceManager.getColor(R.color.text_secondary) - val colorSpan = ForegroundColorSpan(textColor) - val sizeSpan = AbsoluteSizeSpan(resourceManager.getDimensionPixelSize(R.dimen.total_balance_fraction_size)) - - return with(amountWithFraction) { - val spannableBuilder = SpannableStringBuilder() - .append(amount) - if (fraction != null) { - spannableBuilder.append(separator + fraction) - val startIndex = amount.length - val endIndex = amount.length + separator.length + fraction!!.length - spannableBuilder.setSpan(colorSpan, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - spannableBuilder.setSpan(sizeSpan, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } - spannableBuilder - } - } - fun sendClicked() { router.openSendFlow() } @@ -349,4 +322,8 @@ class BalanceListViewModel( private fun hideCrowdloanBanner() = launch { assetsListInteractor.hideCrowdloanBanner() } + + fun switchViewMode() { + launch { assetListMixin.switchViewMode() } + } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt index 779ddb96eb..ef025d8cab 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModelProvider import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository import io.novafoundation.nova.common.di.scope.ScreenScope import io.novafoundation.nova.common.di.viewmodel.ViewModelKey @@ -18,12 +19,15 @@ import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInter import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdownInteractor import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetListMixinFactory +import io.novafoundation.nova.feature_assets.presentation.balance.common.ExpandableAssetsMixinFactory import io.novafoundation.nova.feature_assets.presentation.balance.list.BalanceListViewModel import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase @Module(includes = [ViewModelModule::class]) @@ -34,8 +38,9 @@ class BalanceListModule { fun provideInteractor( accountRepository: AccountRepository, nftRepository: NftRepository, - bannerVisibilityRepository: BannerVisibilityRepository - ) = AssetsListInteractor(accountRepository, nftRepository, bannerVisibilityRepository) + bannerVisibilityRepository: BannerVisibilityRepository, + assetsViewModeRepository: AssetsViewModeRepository + ) = AssetsListInteractor(accountRepository, nftRepository, bannerVisibilityRepository, assetsViewModeRepository) @Provides @ScreenScope @@ -51,6 +56,22 @@ class BalanceListModule { ) } + @Provides + @ScreenScope + fun provideAssetListMixinFactory( + walletInteractor: WalletInteractor, + assetsListInteractor: AssetsListInteractor, + externalBalancesInteractor: ExternalBalancesInteractor, + expandableAssetsMixinFactory: ExpandableAssetsMixinFactory + ): AssetListMixinFactory { + return AssetListMixinFactory( + walletInteractor, + assetsListInteractor, + externalBalancesInteractor, + expandableAssetsMixinFactory + ) + } + @Provides @IntoMap @ViewModelKey(BalanceListViewModel::class) @@ -61,10 +82,11 @@ class BalanceListModule { router: AssetsRouter, currencyInteractor: CurrencyInteractor, balanceBreakdownInteractor: BalanceBreakdownInteractor, - externalBalancesInteractor: ExternalBalancesInteractor, resourceManager: ResourceManager, walletConnectSessionsUseCase: WalletConnectSessionsUseCase, - swapAvailabilityInteractor: SwapAvailabilityInteractor + swapAvailabilityInteractor: SwapAvailabilityInteractor, + assetListMixinFactory: AssetListMixinFactory, + amountFormatter: AmountFormatter ): ViewModel { return BalanceListViewModel( walletInteractor = walletInteractor, @@ -73,10 +95,11 @@ class BalanceListModule { router = router, currencyInteractor = currencyInteractor, balanceBreakdownInteractor = balanceBreakdownInteractor, - externalBalancesInteractor = externalBalancesInteractor, resourceManager = resourceManager, walletConnectSessionsUseCase = walletConnectSessionsUseCase, - swapAvailabilityInteractor = swapAvailabilityInteractor + swapAvailabilityInteractor = swapAvailabilityInteractor, + assetListMixinFactory = assetListMixinFactory, + amountFormatter = amountFormatter ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/AssetRvItem.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/AssetRvItem.kt new file mode 100644 index 0000000000..8801d80e9a --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/AssetRvItem.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.list.model.items + +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableBaseItem +import io.novafoundation.nova.feature_assets.presentation.model.AssetModel + +interface BalanceListRvItem : ExpandableBaseItem { + val itemId: String + + override fun getId(): String { + return itemId + } +} + +interface AssetGroupRvItem : BalanceListRvItem + +interface AssetRvItem : BalanceListRvItem { + val asset: AssetModel +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/NetworkAssetUi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/NetworkAssetUi.kt new file mode 100644 index 0000000000..e5aa6c204b --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/NetworkAssetUi.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.list.model.items + +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import io.novafoundation.nova.runtime.ext.fullId + +data class NetworkAssetUi(override val asset: AssetModel, val icon: Icon) : AssetRvItem { + override val itemId: String = "network_" + asset.token.configuration.fullId.toString() +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/AssetGroupUi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/NetworkGroupUi.kt similarity index 51% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/AssetGroupUi.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/NetworkGroupUi.kt index 8d451c8823..d7e4909f4e 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/AssetGroupUi.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/NetworkGroupUi.kt @@ -1,8 +1,11 @@ -package io.novafoundation.nova.feature_assets.presentation.balance.list.model +package io.novafoundation.nova.feature_assets.presentation.balance.list.model.items import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi -data class AssetGroupUi( +data class NetworkGroupUi( val chainUi: ChainUi, - val groupBalanceFiat: String -) + val groupBalanceFiat: CharSequence +) : AssetGroupRvItem { + + override val itemId: String = chainUi.id +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/TokenAssetUi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/TokenAssetUi.kt new file mode 100644 index 0000000000..3c179a103e --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/TokenAssetUi.kt @@ -0,0 +1,17 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.list.model.items + +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableChildItem +import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi +import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import io.novafoundation.nova.runtime.ext.fullId + +data class TokenAssetUi( + override val groupId: String, + override val asset: AssetModel, + val assetIcon: Icon, + val chain: ChainUi +) : AssetRvItem, ExpandableChildItem { + + override val itemId: String = "token_" + asset.token.configuration.fullId.toString() +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/TokenGroupUi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/TokenGroupUi.kt new file mode 100644 index 0000000000..45bd2da6f8 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/items/TokenGroupUi.kt @@ -0,0 +1,27 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.list.model.items + +import androidx.annotation.ColorRes +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableParentItem +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +data class TokenGroupUi( + override val itemId: String, + val tokenIcon: Icon, + val rate: String, + val recentRateChange: String, + @ColorRes val rateChangeColorRes: Int, + val tokenSymbol: String, + val singleItemGroup: Boolean, + val balance: AmountModel, + val groupType: GroupType +) : AssetGroupRvItem, ExpandableParentItem { + + sealed interface GroupType { + object Group : GroupType + + data class SingleItem(val asset: Chain.Asset) : GroupType + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetViewModeView.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetViewModeView.kt new file mode 100644 index 0000000000..06f4a61693 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetViewModeView.kt @@ -0,0 +1,108 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.list.view + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import android.view.animation.AnticipateOvershootInterpolator +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import io.novafoundation.nova.common.utils.WithContextExtensions +import io.novafoundation.nova.common.utils.setTextColorRes +import io.novafoundation.nova.feature_assets.R +import kotlinx.android.synthetic.main.view_asset_view_mode.view.assetViewModeIcon +import kotlinx.android.synthetic.main.view_asset_view_mode.view.assetViewModeText + +class AssetViewModeView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr), WithContextExtensions { + + override val providedContext: Context = context + + private var slideAnimationBottom = true + + private val animationListener = object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation?) { + isClickable = false + } + + override fun onAnimationEnd(animation: Animation?) { + isClickable = true + } + + override fun onAnimationRepeat(animation: Animation?) { + } + } + + private val slideTopInAnimation = AnimationUtils.loadAnimation(context, R.anim.asset_mode_slide_top_in).applyInterpolator() + private val slideTopOutAnimation = AnimationUtils.loadAnimation(context, R.anim.asset_mode_slide_top_out).applyInterpolator() + private val slideBottomInAnimation = AnimationUtils.loadAnimation(context, R.anim.asset_mode_slide_bottom_in).applyInterpolator() + private val slideBottomOutAnimation = AnimationUtils.loadAnimation(context, R.anim.asset_mode_slide_bottom_out).applyInterpolator() + + init { + View.inflate(context, R.layout.view_asset_view_mode, this) + + orientation = HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + + assetViewModeIcon.setFactory { + val imageView = ImageView(context, null, 0) + imageView.scaleType = ImageView.ScaleType.CENTER_INSIDE + imageView + } + + assetViewModeText.setFactory { + val textView = TextView(context, null, 0, R.style.TextAppearance_NovaFoundation_SemiBold_Title3) + textView.setGravity(Gravity.CLIP_VERTICAL) + textView.setTextColorRes(R.color.text_primary) + textView + } + + slideTopInAnimation.setAnimationListener(animationListener) + slideBottomInAnimation.setAnimationListener(animationListener) + } + + fun switchTextTo(model: AssetViewModeModel) { + switchTextTo(model.iconRes, model.textRes) + } + + fun switchTextTo(@DrawableRes iconRes: Int, @StringRes textRes: Int) { + if (!shouldPlayAnimation(textRes)) return + + assetViewModeIcon.setImageResource(iconRes) + + assetViewModeText.currentView + + if (slideAnimationBottom) { + assetViewModeText.inAnimation = slideBottomInAnimation + assetViewModeText.outAnimation = slideBottomOutAnimation + } else { + assetViewModeText.inAnimation = slideTopInAnimation + assetViewModeText.outAnimation = slideTopOutAnimation + } + + slideAnimationBottom = !slideAnimationBottom + + assetViewModeText.setText(context.getText(textRes)) + } + + private fun Animation.applyInterpolator(): Animation { + interpolator = AnticipateOvershootInterpolator(2.0f) + return this + } + + private fun shouldPlayAnimation(@StringRes textRes: Int): Boolean { + val currentTextView = assetViewModeText.currentView as? TextView ?: return true + + return currentTextView.text != context.getString(textRes) + } +} + +data class AssetViewModeModel(@DrawableRes val iconRes: Int, @StringRes val textRes: Int) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetsHeaderAdapter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetsHeaderAdapter.kt index 8b815c37c8..23731ab55d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetsHeaderAdapter.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetsHeaderAdapter.kt @@ -12,9 +12,9 @@ import io.novafoundation.nova.feature_assets.presentation.balance.list.model.Tot import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectSessionsModel import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.item_asset_header.view.balanceListAssetPlaceholder +import kotlinx.android.synthetic.main.item_asset_header.view.balanceListAssetTitle import kotlinx.android.synthetic.main.item_asset_header.view.balanceListAvatar import kotlinx.android.synthetic.main.item_asset_header.view.balanceListCrowdloansPromoBanner -import kotlinx.android.synthetic.main.item_asset_header.view.balanceListFilters import kotlinx.android.synthetic.main.item_asset_header.view.balanceListManage import kotlinx.android.synthetic.main.item_asset_header.view.balanceListNfts import kotlinx.android.synthetic.main.item_asset_header.view.balanceListSearch @@ -28,7 +28,7 @@ class AssetsHeaderAdapter(private val handler: Handler) : RecyclerView.Adapter? = null private var crowdloanBannerVisible: Boolean = false + private var assetViewModeModel: AssetViewModeModel? = null fun setFilterIconRes(filterIconRes: Int) { this.filterIconRes = filterIconRes - - notifyItemChanged(0, Payload.FILTER_ICON) } fun setCrowdloanBannerVisible(crowdloanBannerVisible: Boolean) { @@ -107,6 +108,12 @@ class AssetsHeaderAdapter(private val handler: Handler) : RecyclerView.Adapter holder.bindNftPreviews(nftPreviews) Payload.PLACEHOLDER -> holder.bindPlaceholder(shouldShowPlaceholder) Payload.WALLET_CONNECT -> holder.bindWalletConnect(walletConnectModel) - Payload.FILTER_ICON -> holder.bindFilterIcon(filterIconRes) Payload.CROWDLOAN_BANNER_VISIBLE -> holder.bindCrowdloanBanner(crowdloanBannerVisible) + Payload.ASSET_VIEW_MODE -> holder.bindAssetViewMode(assetViewModeModel) } } } @@ -139,7 +146,8 @@ class AssetsHeaderAdapter(private val handler: Handler) : RecyclerView.Adapter?) = with(containerView) { @@ -227,11 +236,11 @@ class HeaderHolder( containerView.balanceListWalletConnect.setConnectionCount(it.connections) } - fun bindFilterIcon(filterIconRes: Int?) { - filterIconRes?.let { containerView.balanceListFilters.setImageResource(it) } - } - fun bindCrowdloanBanner(bannerVisible: Boolean) = with(containerView) { balanceListCrowdloansPromoBanner.setVisible(bannerVisible) } + + fun bindAssetViewMode(assetViewModeModel: AssetViewModeModel?) = with(containerView) { + assetViewModeModel?.let { balanceListAssetTitle.switchTextTo(assetViewModeModel) } + } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/AssetSearchFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/AssetSearchFragment.kt index ea28900dae..fd29d8aa92 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/AssetSearchFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/AssetSearchFragment.kt @@ -13,15 +13,20 @@ import io.novafoundation.nova.common.utils.applyStatusBarInsets import io.novafoundation.nova.common.utils.bindTo import io.novafoundation.nova.common.utils.keyboard.hideSoftKeyboard import io.novafoundation.nova.common.utils.keyboard.showSoftKeyboard +import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings +import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator import io.novafoundation.nova.common.utils.setVisible -import io.novafoundation.nova.common.utils.submitListPreservingViewPoint import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent -import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetGroupingDecoration +import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetTokensDecoration +import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetTokensItemAnimator +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.AssetBaseDecoration import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter -import io.novafoundation.nova.feature_assets.presentation.balance.common.applyDefaultTo -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.applyDefaultTo +import io.novafoundation.nova.feature_assets.presentation.balance.common.createForAssets +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.android.synthetic.main.fragment_asset_search.searchAssetContainer import kotlinx.android.synthetic.main.fragment_asset_search.searchAssetList import kotlinx.android.synthetic.main.fragment_asset_search.searchAssetSearch @@ -58,8 +63,13 @@ class AssetSearchFragment : searchAssetList.setHasFixedSize(true) searchAssetList.adapter = assetsAdapter - AssetGroupingDecoration.applyDefaultTo(searchAssetList, assetsAdapter) - searchAssetList.itemAnimator = null + val animationSettings = ExpandableAnimationSettings.createForAssets() + val animator = ExpandableAnimator(searchAssetList, animationSettings, assetsAdapter) + + searchAssetList.addItemDecoration(AssetTokensDecoration(requireContext(), assetsAdapter, animator)) + searchAssetList.itemAnimator = AssetTokensItemAnimator(animationSettings, animator) + + AssetBaseDecoration.applyDefaultTo(searchAssetList, assetsAdapter) searchAssetSearch.cancel.setOnClickListener { viewModel.cancelClicked() @@ -80,15 +90,17 @@ class AssetSearchFragment : override fun subscribe(viewModel: AssetSearchViewModel) { searchAssetSearch.searchInput.content.bindTo(viewModel.query, lifecycleScope) + viewModel.query.observe { + searchAssetList.post { + searchAssetList.layoutManager!!.scrollToPosition(0) + } + } + viewModel.searchResults.observe { data -> searchAssetsPlaceholder.setVisible(data.isEmpty()) searchAssetList.setVisible(data.isNotEmpty()) - assetsAdapter.submitListPreservingViewPoint( - data = data, - into = searchAssetList, - extraDiffCompletedCallback = { searchAssetList.invalidateItemDecorations() } - ) + assetsAdapter.submitList(data) { searchAssetList.invalidateItemDecorations() } } } @@ -98,7 +110,18 @@ class AssetSearchFragment : searchAssetSearch.searchInput.hideSoftKeyboard() } - override fun assetClicked(asset: AssetModel) { + override fun assetClicked(asset: Chain.Asset) { viewModel.assetClicked(asset) } + + override fun tokenGroupClicked(tokenGroup: TokenGroupUi) { + if (tokenGroup.groupType is TokenGroupUi.GroupType.SingleItem) { + viewModel.assetClicked(tokenGroup.groupType.asset) + } else { + val itemAnimator = searchAssetList.itemAnimator as AssetTokensItemAnimator + itemAnimator.prepareForAnimation() + + viewModel.assetListMixin.expandToken(tokenGroup) + } + } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/AssetSearchViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/AssetSearchViewModel.kt index 367d4c3447..1fcac9279e 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/AssetSearchViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/AssetSearchViewModel.kt @@ -1,50 +1,41 @@ package io.novafoundation.nova.feature_assets.presentation.balance.search import io.novafoundation.nova.common.base.BaseViewModel -import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_assets.presentation.AssetsRouter -import io.novafoundation.nova.feature_assets.presentation.balance.common.mapGroupedAssetsToUi -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel -import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_assets.presentation.balance.common.ExpandableAssetsMixinFactory +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged class AssetSearchViewModel( private val router: AssetsRouter, - interactor: AssetSearchInteractor, - currencyInteractor: CurrencyInteractor, + interactorFactory: AssetSearchInteractorFactory, externalBalancesInteractor: ExternalBalancesInteractor, + expandableAssetsMixinFactory: ExpandableAssetsMixinFactory ) : BaseViewModel() { - val query = MutableStateFlow("") + val interactor = interactorFactory.createByAssetViewMode() - private val selectedCurrency = currencyInteractor.observeSelectCurrency() - .inBackground() - .share() + val query = MutableStateFlow("") private val externalBalances = externalBalancesInteractor.observeExternalBalances() - val searchResults = combine( - interactor.searchAssetsFlow(query, externalBalances), - selectedCurrency, - ) { assets, currency -> - assets.mapGroupedAssetsToUi(currency) - } - .distinctUntilChanged() - .shareInBackground() + private val assetsFlow = interactor.searchAssetsFlow(query, externalBalances) + + val assetListMixin = expandableAssetsMixinFactory.create(assetsFlow) + + val searchResults = assetListMixin.assetModelsFlow fun cancelClicked() { router.back() } - fun assetClicked(assetModel: AssetModel) { + fun assetClicked(asset: Chain.Asset) { val payload = AssetPayload( - chainId = assetModel.token.configuration.chainId, - chainAssetId = assetModel.token.configuration.id + chainId = asset.chainId, + chainAssetId = asset.id ) router.openAssetDetails(payload) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/di/AssetSearchModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/di/AssetSearchModule.kt index c85f6e372f..ef96d43cd7 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/di/AssetSearchModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/search/di/AssetSearchModule.kt @@ -9,10 +9,10 @@ import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ExpandableAssetsMixinFactory import io.novafoundation.nova.feature_assets.presentation.balance.search.AssetSearchViewModel -import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor @Module(includes = [ViewModelModule::class]) class AssetSearchModule { @@ -27,15 +27,15 @@ class AssetSearchModule { @ViewModelKey(AssetSearchViewModel::class) fun provideViewModel( router: AssetsRouter, - interactor: AssetSearchInteractor, - currencyInteractor: CurrencyInteractor, - externalBalancesInteractor: ExternalBalancesInteractor + interactorFactory: AssetSearchInteractorFactory, + externalBalancesInteractor: ExternalBalancesInteractor, + expandableAssetsMixinFactory: ExpandableAssetsMixinFactory ): ViewModel { return AssetSearchViewModel( router = router, - interactor = interactor, - currencyInteractor = currencyInteractor, - externalBalancesInteractor = externalBalancesInteractor + interactorFactory = interactorFactory, + externalBalancesInteractor = externalBalancesInteractor, + expandableAssetsMixinFactory = expandableAssetsMixinFactory ) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/AssetBuyFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/AssetBuyFlowViewModel.kt deleted file mode 100644 index 98f57ae28b..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/AssetBuyFlowViewModel.kt +++ /dev/null @@ -1,48 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.buy.flow - -import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase -import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup -import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance -import io.novafoundation.nova.feature_assets.presentation.AssetsRouter -import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.flow.AssetFlowViewModel -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel -import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin -import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor -import kotlinx.coroutines.flow.Flow - -class AssetBuyFlowViewModel( - interactor: AssetSearchInteractor, - router: AssetsRouter, - externalBalancesInteractor: ExternalBalancesInteractor, - currencyInteractor: CurrencyInteractor, - controllableAssetCheck: ControllableAssetCheckMixin, - accountUseCase: SelectedAccountUseCase, - buyMixinFactory: BuyMixin.Factory, - resourceManager: ResourceManager, -) : AssetFlowViewModel( - interactor, - router, - currencyInteractor, - controllableAssetCheck, - accountUseCase, - externalBalancesInteractor, - resourceManager, -) { - - val buyMixin = buyMixinFactory.create(scope = this) - - override fun searchAssetsFlow(): Flow>> { - return interactor.buyAssetSearch(query, externalBalancesFlow) - } - - override fun assetClicked(assetModel: AssetModel) { - validate(assetModel) { - val chainAsset = assetModel.token.configuration - buyMixin.buyClicked(chainAsset) - } - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/AssetBuyFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/AssetBuyFlowFragment.kt similarity index 91% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/AssetBuyFlowFragment.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/AssetBuyFlowFragment.kt index 546177c34e..b3a10a76b0 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/AssetBuyFlowFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/AssetBuyFlowFragment.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_assets.presentation.buy.flow +package io.novafoundation.nova.feature_assets.presentation.buy.flow.asset import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent -import io.novafoundation.nova.feature_assets.presentation.flow.AssetFlowFragment +import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowFragment import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixinUi import javax.inject.Inject diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/AssetBuyFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/AssetBuyFlowViewModel.kt new file mode 100644 index 0000000000..42ee15147f --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/AssetBuyFlowViewModel.kt @@ -0,0 +1,64 @@ +package io.novafoundation.nova.feature_assets.presentation.buy.flow.asset + +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin +import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow + +class AssetBuyFlowViewModel( + interactorFactory: AssetSearchInteractorFactory, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + currencyInteractor: CurrencyInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + buyMixinFactory: BuyMixin.Factory, + resourceManager: ResourceManager, + assetIconProvider: AssetIconProvider, + assetViewModeInteractor: AssetViewModeInteractor, + amountFormatter: AmountFormatter +) : AssetFlowViewModel( + interactorFactory, + router, + currencyInteractor, + controllableAssetCheck, + accountUseCase, + externalBalancesInteractor, + resourceManager, + assetIconProvider, + assetViewModeInteractor, + amountFormatter +) { + + val buyMixin = buyMixinFactory.create(scope = this) + + override fun searchAssetsFlow(): Flow { + return interactor.buyAssetSearch(query, externalBalancesFlow) + } + + override fun assetClicked(asset: Chain.Asset) { + validate(asset) { + buyMixin.buyClicked(asset) + } + } + + override fun tokenClicked(tokenGroup: TokenGroupUi) { + when (val type = tokenGroup.groupType) { + is TokenGroupUi.GroupType.SingleItem -> assetClicked(type.asset) + TokenGroupUi.GroupType.Group -> router.openBuyNetworks(NetworkFlowPayload(tokenGroup.tokenSymbol)) + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/di/AssetBuyFlowComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/di/AssetBuyFlowComponent.kt similarity index 93% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/di/AssetBuyFlowComponent.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/di/AssetBuyFlowComponent.kt index f6c85695eb..92a9d5749e 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/di/AssetBuyFlowComponent.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/di/AssetBuyFlowComponent.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_assets.presentation.buy.flow.di +package io.novafoundation.nova.feature_assets.presentation.buy.flow.asset.di import androidx.fragment.app.Fragment import dagger.BindsInstance import dagger.Subcomponent import io.novafoundation.nova.common.di.scope.ScreenScope -import io.novafoundation.nova.feature_assets.presentation.buy.flow.AssetBuyFlowFragment +import io.novafoundation.nova.feature_assets.presentation.buy.flow.asset.AssetBuyFlowFragment @Subcomponent( modules = [ diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/di/AssetBuyFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/di/AssetBuyFlowModule.kt similarity index 72% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/di/AssetBuyFlowModule.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/di/AssetBuyFlowModule.kt index 8274e38108..7a4d8d20b5 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/di/AssetBuyFlowModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/asset/di/AssetBuyFlowModule.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_assets.presentation.buy.flow.di +package io.novafoundation.nova.feature_assets.presentation.buy.flow.asset.di import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel @@ -8,15 +8,18 @@ import dagger.Provides import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.buy.flow.AssetBuyFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.buy.flow.asset.AssetBuyFlowViewModel import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter @Module(includes = [ViewModelModule::class]) class AssetBuyFlowModule { @@ -30,24 +33,30 @@ class AssetBuyFlowModule { @IntoMap @ViewModelKey(AssetBuyFlowViewModel::class) fun provideViewModel( - interactor: AssetSearchInteractor, + interactorFactory: AssetSearchInteractorFactory, router: AssetsRouter, externalBalancesInteractor: ExternalBalancesInteractor, currencyInteractor: CurrencyInteractor, controllableAssetCheck: ControllableAssetCheckMixin, accountUseCase: SelectedAccountUseCase, buyMixinFactory: BuyMixin.Factory, - resourceManager: ResourceManager + resourceManager: ResourceManager, + assetIconProvider: AssetIconProvider, + assetViewModeInteractor: AssetViewModeInteractor, + amountFormatter: AmountFormatter ): ViewModel { return AssetBuyFlowViewModel( - interactor = interactor, + interactorFactory = interactorFactory, router = router, externalBalancesInteractor = externalBalancesInteractor, currencyInteractor = currencyInteractor, controllableAssetCheck = controllableAssetCheck, accountUseCase = accountUseCase, buyMixinFactory = buyMixinFactory, - resourceManager = resourceManager + resourceManager = resourceManager, + assetIconProvider = assetIconProvider, + assetViewModeInteractor = assetViewModeInteractor, + amountFormatter = amountFormatter ) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/NetworkBuyFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/NetworkBuyFlowFragment.kt new file mode 100644 index 0000000000..e548707e61 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/NetworkBuyFlowFragment.kt @@ -0,0 +1,29 @@ +package io.novafoundation.nova.feature_assets.presentation.buy.flow.network + +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.payload +import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi +import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowFragment +import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixinUi +import javax.inject.Inject + +class NetworkBuyFlowFragment : + NetworkFlowFragment() { + + @Inject + lateinit var buyMixin: BuyMixinUi + + override fun inject() { + FeatureUtils.getFeature(this, AssetsFeatureApi::class.java) + .networkBuyFlowComponent() + .create(this, payload()) + .inject(this) + } + + override fun subscribe(viewModel: NetworkBuyFlowViewModel) { + super.subscribe(viewModel) + + buyMixin.setupBuyIntegration(this, viewModel.buyMixin) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/NetworkBuyFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/NetworkBuyFlowViewModel.kt new file mode 100644 index 0000000000..f9cb172e99 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/NetworkBuyFlowViewModel.kt @@ -0,0 +1,65 @@ +package io.novafoundation.nova.feature_assets.presentation.buy.flow.network + +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.TokenSymbol +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.model.NetworkFlowRvItem +import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.asset +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +class NetworkBuyFlowViewModel( + interactor: AssetNetworksInteractor, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + buyMixinFactory: BuyMixin.Factory, + resourceManager: ResourceManager, + networkFlowPayload: NetworkFlowPayload, + chainRegistry: ChainRegistry +) : NetworkFlowViewModel( + interactor, + router, + controllableAssetCheck, + accountUseCase, + externalBalancesInteractor, + resourceManager, + networkFlowPayload, + chainRegistry +) { + + val buyMixin = buyMixinFactory.create(scope = this) + + override fun getAssetBalance(asset: AssetWithNetwork): PricedAmount { + return asset.balanceWithOffChain.total + } + + override fun assetsFlow(tokenSymbol: TokenSymbol): Flow> { + return interactor.buyAssetFlow(tokenSymbol, externalBalancesFlow) + } + + override fun networkClicked(network: NetworkFlowRvItem) { + validateControllsAsset(network) { + launch { + val chainAsset = chainRegistry.asset(network.chainId, network.assetId) + buyMixin.buyClicked(chainAsset) + } + } + } + + override fun getTitle(tokenSymbol: TokenSymbol): String { + return resourceManager.getString(R.string.buy_network_flow_title, tokenSymbol.value) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/di/NetworkBuyFlowComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/di/NetworkBuyFlowComponent.kt new file mode 100644 index 0000000000..f24d0a50a3 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/di/NetworkBuyFlowComponent.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_assets.presentation.buy.flow.network.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_assets.presentation.buy.flow.network.NetworkBuyFlowFragment +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload + +@Subcomponent( + modules = [ + NetworkBuyFlowModule::class + ] +) +@ScreenScope +interface NetworkBuyFlowComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment, + @BindsInstance networkFlowPayload: NetworkFlowPayload + ): NetworkBuyFlowComponent + } + + fun inject(fragment: NetworkBuyFlowFragment) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/di/NetworkBuyFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/di/NetworkBuyFlowModule.kt new file mode 100644 index 0000000000..fec4736786 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/buy/flow/network/di/NetworkBuyFlowModule.kt @@ -0,0 +1,56 @@ +package io.novafoundation.nova.feature_assets.presentation.buy.flow.network.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.buy.flow.network.NetworkBuyFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +@Module(includes = [ViewModelModule::class]) +class NetworkBuyFlowModule { + + @Provides + internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): NetworkBuyFlowViewModel { + return ViewModelProvider(fragment, factory).get(NetworkBuyFlowViewModel::class.java) + } + + @Provides + @IntoMap + @ViewModelKey(NetworkBuyFlowViewModel::class) + fun provideViewModel( + interactor: AssetNetworksInteractor, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + buyMixinFactory: BuyMixin.Factory, + resourceManager: ResourceManager, + networkFlowPayload: NetworkFlowPayload, + chainRegistry: ChainRegistry + ): ViewModel { + return NetworkBuyFlowViewModel( + interactor = interactor, + router = router, + externalBalancesInteractor = externalBalancesInteractor, + controllableAssetCheck = controllableAssetCheck, + accountUseCase = accountUseCase, + buyMixinFactory = buyMixinFactory, + resourceManager = resourceManager, + networkFlowPayload = networkFlowPayload, + chainRegistry = chainRegistry + ) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/AssetFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/AssetFlowViewModel.kt deleted file mode 100644 index 552866ea25..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/AssetFlowViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.flow - -import io.novafoundation.nova.common.base.BaseViewModel -import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.flowOfAll -import io.novafoundation.nova.common.utils.inBackground -import io.novafoundation.nova.common.view.PlaceholderModel -import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase -import io.novafoundation.nova.feature_assets.R -import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup -import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance -import io.novafoundation.nova.feature_assets.presentation.AssetsRouter -import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.balance.common.mapGroupedAssetsToUi -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel -import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor -import io.novafoundation.nova.feature_currency_api.domain.model.Currency -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.launch - -class AssetFlowListModel( - val assets: List, - val placeholder: PlaceholderModel?, -) - -abstract class AssetFlowViewModel( - protected val interactor: AssetSearchInteractor, - protected val router: AssetsRouter, - protected val currencyInteractor: CurrencyInteractor, - private val controllableAssetCheck: ControllableAssetCheckMixin, - internal val accountUseCase: SelectedAccountUseCase, - externalBalancesInteractor: ExternalBalancesInteractor, - internal val resourceManager: ResourceManager, -) : BaseViewModel() { - - val acknowledgeLedgerWarning = controllableAssetCheck.acknowledgeLedgerWarning - - val query = MutableStateFlow("") - - private val selectedCurrency = currencyInteractor.observeSelectCurrency() - .inBackground() - .share() - - protected val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances() - - val searchResults = combine( - flowOfAll { searchAssetsFlow() }, // lazy use searchAssetsFlow to let subclasses initialize self - selectedCurrency, - ) { assets, currency -> - val groupedAssets = mapAssets(assets, currency) - AssetFlowListModel( - groupedAssets, - getPlaceholder(query.value, groupedAssets) - ) - } - .distinctUntilChanged() - .shareInBackground(SharingStarted.Lazily) - - fun backClicked() { - router.back() - } - - abstract fun searchAssetsFlow(): Flow>> - - abstract fun assetClicked(assetModel: AssetModel) - - open fun mapAssets(assets: Map>, currency: Currency): List { - return assets.mapGroupedAssetsToUi(currency) - } - - internal fun validate(assetModel: AssetModel, onAccept: (AssetModel) -> Unit) { - launch { - val metaAccount = accountUseCase.getSelectedMetaAccount() - val chainAsset = assetModel.token.configuration - controllableAssetCheck.check(metaAccount, chainAsset) { - onAccept(assetModel) - } - } - } - - protected open fun getPlaceholder(query: String, assets: List): PlaceholderModel? { - return when { - assets.isEmpty() -> PlaceholderModel( - text = resourceManager.getString(R.string.assets_search_placeholder), - imageRes = R.drawable.ic_no_search_results - ) - - else -> null - } - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/AssetFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowFragment.kt similarity index 68% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/AssetFlowFragment.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowFragment.kt index 51024d77d0..42d1891ca4 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/AssetFlowFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowFragment.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_assets.presentation.flow +package io.novafoundation.nova.feature_assets.presentation.flow.asset import android.os.Bundle import android.view.LayoutInflater @@ -17,11 +17,15 @@ import io.novafoundation.nova.common.utils.keyboard.showSoftKeyboard import io.novafoundation.nova.common.utils.submitListPreservingViewPoint import io.novafoundation.nova.common.view.setModelOrHide import io.novafoundation.nova.feature_assets.R -import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetGroupingDecoration +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.AssetBaseDecoration import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter -import io.novafoundation.nova.feature_assets.presentation.balance.common.applyDefaultTo -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.CompoundAssetDecorationPreferences +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.NetworkAssetDecorationPreferences +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.TokenAssetGroupDecorationPreferences +import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.applyDefaultTo +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi import io.novafoundation.nova.feature_assets.presentation.receive.view.LedgerNotSupportedWarningBottomSheet +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import javax.inject.Inject import kotlinx.android.synthetic.main.fragment_asset_flow_search.assetFlowList import kotlinx.android.synthetic.main.fragment_asset_flow_search.assetFlowPlaceholder @@ -64,7 +68,14 @@ abstract class AssetFlowFragment : setHasFixedSize(true) adapter = assetsAdapter - AssetGroupingDecoration.applyDefaultTo(this, assetsAdapter) + AssetBaseDecoration.applyDefaultTo( + this, + assetsAdapter, + CompoundAssetDecorationPreferences( + NetworkAssetDecorationPreferences(), + TokenAssetGroupDecorationPreferences() + ) + ) itemAnimator = null } @@ -75,17 +86,24 @@ abstract class AssetFlowFragment : override fun subscribe(viewModel: T) { assetFlowToolbar.searchField.content.bindTo(viewModel.query, lifecycleScope) - viewModel.searchResults.observe { searchResult -> - assetFlowPlaceholder.setModelOrHide(searchResult.placeholder) - assetFlowList.setVisible(searchResult.assets.isNotEmpty()) + viewModel.searchHint.observe { + assetFlowToolbar.searchField.setHint(it) + } + + viewModel.searchResults.observe { assets -> + assetFlowList.setVisible(assets.isNotEmpty()) assetsAdapter.submitListPreservingViewPoint( - data = searchResult.assets, + data = assets, into = assetFlowList, extraDiffCompletedCallback = { assetFlowList.invalidateItemDecorations() } ) } + viewModel.placeholder.observe { placeholder -> + assetFlowPlaceholder.setModelOrHide(placeholder) + } + viewModel.acknowledgeLedgerWarning.awaitableActionLiveData.observeEvent { LedgerNotSupportedWarningBottomSheet( context = requireContext(), @@ -95,9 +113,15 @@ abstract class AssetFlowFragment : } } - override fun assetClicked(asset: AssetModel) { + override fun assetClicked(asset: Chain.Asset) { viewModel.assetClicked(asset) assetFlowToolbar.searchField.hideSoftKeyboard() } + + override fun tokenGroupClicked(tokenGroup: TokenGroupUi) { + viewModel.tokenClicked(tokenGroup) + + assetFlowToolbar.searchField.hideSoftKeyboard() + } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowViewModel.kt new file mode 100644 index 0000000000..e651ae3206 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowViewModel.kt @@ -0,0 +1,128 @@ +package io.novafoundation.nova.feature_assets.presentation.flow.asset + +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.data.model.AssetViewMode +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.common.utils.inBackground +import io.novafoundation.nova.common.view.PlaceholderModel +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult +import io.novafoundation.nova.feature_assets.domain.assets.models.groupList +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapTokenAssetGroupToUi +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapGroupedAssetsToUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_currency_api.domain.model.Currency +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +abstract class AssetFlowViewModel( + interactorFactory: AssetSearchInteractorFactory, + protected val router: AssetsRouter, + protected val currencyInteractor: CurrencyInteractor, + private val controllableAssetCheck: ControllableAssetCheckMixin, + protected val accountUseCase: SelectedAccountUseCase, + externalBalancesInteractor: ExternalBalancesInteractor, + protected val resourceManager: ResourceManager, + private val assetIconProvider: AssetIconProvider, + private val assetViewModeInteractor: AssetViewModeInteractor, + private val amountFormatter: AmountFormatter +) : BaseViewModel() { + + protected val interactor = interactorFactory.createByAssetViewMode() + + val acknowledgeLedgerWarning = controllableAssetCheck.acknowledgeLedgerWarning + + val query = MutableStateFlow("") + + private val selectedCurrency = currencyInteractor.observeSelectCurrency() + .inBackground() + .share() + + protected val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances() + + private val searchAssetsFlow = flowOfAll { searchAssetsFlow() } // lazy use searchAssetsFlow to let subclasses initialize self + .shareInBackground(SharingStarted.Lazily) + + val searchHint = assetViewModeInteractor.assetsViewModeFlow() + .map { + when (it) { + AssetViewMode.NETWORKS -> resourceManager.getString(R.string.assets_search_hint) + AssetViewMode.TOKENS -> resourceManager.getString(R.string.assets_search_token_hint) + } + } + + val searchResults = combine( + searchAssetsFlow, + selectedCurrency, + ) { assets, currency -> + mapAssets(assets, currency) + }.distinctUntilChanged() + + val placeholder = searchAssetsFlow.map { getPlaceholder(query.value, it.groupList()) } + + fun backClicked() { + router.back() + } + + abstract fun searchAssetsFlow(): Flow + + abstract fun assetClicked(asset: Chain.Asset) + + abstract fun tokenClicked(tokenGroup: TokenGroupUi) + + private fun mapAssets(searchResult: AssetsByViewModeResult, currency: Currency): List { + return when (searchResult) { + is AssetsByViewModeResult.ByNetworks -> mapNetworkAssets(searchResult.assets, currency) + is AssetsByViewModeResult.ByTokens -> mapTokensAssets(searchResult.tokens) + } + } + + open fun mapNetworkAssets(assets: Map>, currency: Currency): List { + return assets.mapGroupedAssetsToUi(amountFormatter, assetIconProvider, currency) + } + + open fun mapTokensAssets(assets: Map>): List { + return assets.map { mapTokenAssetGroupToUi(amountFormatter, assetIconProvider, it.key, assets = it.value) } + } + + internal fun validate(asset: Chain.Asset, onAccept: (Chain.Asset) -> Unit) { + launch { + val metaAccount = accountUseCase.getSelectedMetaAccount() + controllableAssetCheck.check(metaAccount, asset) { + onAccept(asset) + } + } + } + + protected open fun getPlaceholder(query: String, assets: List): PlaceholderModel? { + return when { + assets.isEmpty() -> PlaceholderModel( + text = resourceManager.getString(R.string.assets_search_placeholder), + imageRes = R.drawable.ic_no_search_results + ) + + else -> null + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowAdapter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowAdapter.kt new file mode 100644 index 0000000000..17dd942009 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowAdapter.kt @@ -0,0 +1,61 @@ +package io.novafoundation.nova.feature_assets.presentation.flow.network + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import coil.ImageLoader +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.common.utils.inflateChild +import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.presentation.flow.network.model.NetworkFlowRvItem +import kotlinx.android.synthetic.main.item_network_flow.view.itemNetworkBalance +import kotlinx.android.synthetic.main.item_network_flow.view.itemNetworkImage +import kotlinx.android.synthetic.main.item_network_flow.view.itemNetworkPriceAmount +import kotlinx.android.synthetic.main.item_network_flow.view.itemNetworkName + +class NetworkFlowAdapter( + private val imageLoader: ImageLoader, + private val itemHandler: ItemNetworkHandler, +) : ListAdapter(DiffCallback) { + + interface ItemNetworkHandler { + fun networkClicked(network: NetworkFlowRvItem) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NetworkFlowViewHolder { + return NetworkFlowViewHolder(parent.inflateChild(R.layout.item_network_flow), imageLoader, itemHandler) + } + + override fun onBindViewHolder(holder: NetworkFlowViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +private object DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: NetworkFlowRvItem, newItem: NetworkFlowRvItem): Boolean { + return oldItem.chainId == newItem.chainId + } + + override fun areContentsTheSame(oldItem: NetworkFlowRvItem, newItem: NetworkFlowRvItem): Boolean { + return oldItem == newItem + } +} + +class NetworkFlowViewHolder( + containerView: View, + private val imageLoader: ImageLoader, + private val itemHandler: NetworkFlowAdapter.ItemNetworkHandler, +) : GroupedListHolder(containerView) { + + fun bind(item: NetworkFlowRvItem) = with(containerView) { + itemNetworkImage.loadChainIcon(item.icon, imageLoader) + itemNetworkName.text = item.networkName + itemNetworkBalance.text = item.balance.token + itemNetworkPriceAmount.text = item.balance.fiat + + setOnClickListener { itemHandler.networkClicked(item) } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowFragment.kt new file mode 100644 index 0000000000..5ba9c9e691 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowFragment.kt @@ -0,0 +1,80 @@ +package io.novafoundation.nova.feature_assets.presentation.flow.network + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.ConcatAdapter +import coil.ImageLoader +import io.novafoundation.nova.common.base.BaseFragment +import io.novafoundation.nova.common.utils.FragmentPayloadCreator +import io.novafoundation.nova.common.utils.PayloadCreator +import io.novafoundation.nova.common.utils.applyStatusBarInsets +import io.novafoundation.nova.common.view.recyclerview.adapter.text.TextAdapter +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.presentation.flow.network.model.NetworkFlowRvItem +import io.novafoundation.nova.feature_assets.presentation.receive.view.LedgerNotSupportedWarningBottomSheet +import javax.inject.Inject +import kotlinx.android.synthetic.main.fragment_network_flow.networkFlowList +import kotlinx.android.synthetic.main.fragment_network_flow.networkFlowToolbar + +abstract class NetworkFlowFragment : + BaseFragment(), + NetworkFlowAdapter.ItemNetworkHandler { + + companion object : PayloadCreator by FragmentPayloadCreator() + + @Inject + lateinit var imageLoader: ImageLoader + + private val titleAdapter by lazy(LazyThreadSafetyMode.NONE) { + TextAdapter(styleRes = R.style.TextAppearance_NovaFoundation_Bold_Title3) + } + + private val networkAdapter by lazy(LazyThreadSafetyMode.NONE) { + NetworkFlowAdapter(imageLoader, this) + } + + private val adapter by lazy(LazyThreadSafetyMode.NONE) { + ConcatAdapter(titleAdapter, networkAdapter) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return layoutInflater.inflate(R.layout.fragment_network_flow, container, false) + } + + override fun initViews() { + networkFlowToolbar.applyStatusBarInsets() + networkFlowToolbar.setHomeButtonListener { viewModel.backClicked() } + + networkFlowList.setHasFixedSize(true) + networkFlowList.adapter = adapter + networkFlowList.itemAnimator = null + } + + override fun subscribe(viewModel: T) { + viewModel.titleFlow.observe { + titleAdapter.setText(it) + } + + viewModel.networks.observe { + networkAdapter.submitList(it) + } + + viewModel.acknowledgeLedgerWarning.awaitableActionLiveData.observeEvent { + LedgerNotSupportedWarningBottomSheet( + context = requireContext(), + onSuccess = { it.onSuccess(Unit) }, + message = it.payload + ).show() + } + } + + override fun networkClicked(network: NetworkFlowRvItem) { + viewModel.networkClicked(network) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowPayload.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowPayload.kt new file mode 100644 index 0000000000..4af40784f4 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowPayload.kt @@ -0,0 +1,10 @@ +package io.novafoundation.nova.feature_assets.presentation.flow.network + +import android.os.Parcelable +import io.novafoundation.nova.common.utils.TokenSymbol +import kotlinx.android.parcel.Parcelize + +@Parcelize +class NetworkFlowPayload(val tokenSymbol: String) : Parcelable + +fun NetworkFlowPayload.asTokenSymbol() = TokenSymbol(tokenSymbol) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowViewModel.kt new file mode 100644 index 0000000000..0a09caeec7 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowViewModel.kt @@ -0,0 +1,79 @@ +package io.novafoundation.nova.feature_assets.presentation.flow.network + +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.TokenSymbol +import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.flow.network.model.NetworkFlowRvItem +import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.asset +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +abstract class NetworkFlowViewModel( + protected val interactor: AssetNetworksInteractor, + protected val router: AssetsRouter, + private val controllableAssetCheck: ControllableAssetCheckMixin, + protected val accountUseCase: SelectedAccountUseCase, + externalBalancesInteractor: ExternalBalancesInteractor, + protected val resourceManager: ResourceManager, + private val networkFlowPayload: NetworkFlowPayload, + protected val chainRegistry: ChainRegistry +) : BaseViewModel() { + + val acknowledgeLedgerWarning = controllableAssetCheck.acknowledgeLedgerWarning + + protected val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances() + + val titleFlow: Flow = flowOf { getTitle(networkFlowPayload.asTokenSymbol()) } + val networks: Flow> = assetsFlow(networkFlowPayload.asTokenSymbol()) + .map { mapAssets(it) } + + abstract fun getAssetBalance(asset: AssetWithNetwork): PricedAmount + + abstract fun assetsFlow(tokenSymbol: TokenSymbol): Flow> + + abstract fun networkClicked(network: NetworkFlowRvItem) + + abstract fun getTitle(tokenSymbol: TokenSymbol): String + + fun backClicked() { + router.back() + } + + internal fun validateControllsAsset(networkFlowRvItem: NetworkFlowRvItem, onAccept: () -> Unit) { + launch { + val metaAccount = accountUseCase.getSelectedMetaAccount() + val chainAsset = chainRegistry.asset(networkFlowRvItem.chainId, networkFlowRvItem.assetId) + controllableAssetCheck.check(metaAccount, chainAsset) { + onAccept() + } + } + } + + private fun mapAssets(assetWithNetworks: List): List { + return assetWithNetworks + .map { + NetworkFlowRvItem( + it.chain.id, + it.asset.token.configuration.id, + it.chain.name, + it.chain.icon, + mapAmountToAmountModel( + amount = getAssetBalance(it).amount, + asset = it.asset, + includeAssetTicker = false + ) + ) + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/model/NetworkFlowRvItem.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/model/NetworkFlowRvItem.kt new file mode 100644 index 0000000000..051b6cd2fc --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/model/NetworkFlowRvItem.kt @@ -0,0 +1,11 @@ +package io.novafoundation.nova.feature_assets.presentation.flow.network.model + +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel + +data class NetworkFlowRvItem( + val chainId: String, + val assetId: Int, + val networkName: String, + val icon: String?, + val balance: AmountModel +) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/ReceiveFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/ReceiveFragment.kt index 21a865ae63..85a04af92b 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/ReceiveFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/ReceiveFragment.kt @@ -4,23 +4,29 @@ import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.view.drawToBitmap import coil.ImageLoader import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils -import io.novafoundation.nova.common.utils.setVisible +import io.novafoundation.nova.common.utils.applyStatusBarInsets import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable -import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions -import io.novafoundation.nova.feature_account_api.presenatation.chain.loadTokenIcon import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_assets.presentation.receive.model.QrSharingPayload -import kotlinx.android.synthetic.main.fragment_receive.receiveFrom import kotlinx.android.synthetic.main.fragment_receive.receiveQrCode import kotlinx.android.synthetic.main.fragment_receive.receiveShare -import kotlinx.android.synthetic.main.fragment_receive.receiveToolbar import javax.inject.Inject +import kotlinx.android.synthetic.main.fragment_receive.receiveAccount +import kotlinx.android.synthetic.main.fragment_receive.receiveAddress +import kotlinx.android.synthetic.main.fragment_receive.receiveBackButton +import kotlinx.android.synthetic.main.fragment_receive.receiveChain +import kotlinx.android.synthetic.main.fragment_receive.receiveCopyButton +import kotlinx.android.synthetic.main.fragment_receive.receiveQrCodeContainer +import kotlinx.android.synthetic.main.fragment_receive.receiveSubtitle +import kotlinx.android.synthetic.main.fragment_receive.receiveTitle +import kotlinx.android.synthetic.main.fragment_receive.receiveToolbar private const val KEY_PAYLOAD = "KEY_PAYLOAD" @@ -43,16 +49,19 @@ class ReceiveFragment : BaseFragment() { ) = layoutInflater.inflate(R.layout.fragment_receive, container, false) override fun initViews() { - receiveFrom.setWholeClickListener { viewModel.recipientClicked() } + receiveToolbar.applyStatusBarInsets() - receiveToolbar.setHomeButtonListener { viewModel.backClicked() } + receiveCopyButton.setOnClickListener { viewModel.copyAddressClicked() } - receiveShare.setOnClickListener { viewModel.shareButtonClicked() } + receiveBackButton.setOnClickListener { viewModel.backClicked() } - receiveFrom.primaryIcon.setVisible(true) + receiveShare.setOnClickListener { + val qrBitmap = receiveQrCode.drawToBitmap() + viewModel.shareButtonClicked(qrBitmap) + } - receiveQrCode.background = requireContext().getRoundedCornerDrawable(fillColorRes = R.color.qr_code_background) - receiveQrCode.clipToOutline = true // for round corners + receiveQrCodeContainer.background = requireContext().getRoundedCornerDrawable(fillColorRes = R.color.qr_code_background) + receiveQrCodeContainer.clipToOutline = true } override fun inject() { @@ -66,18 +75,12 @@ class ReceiveFragment : BaseFragment() { } override fun subscribe(viewModel: ReceiveViewModel) { - setupExternalActions(viewModel) - - viewModel.qrBitmapFlow.observe(receiveQrCode::setImageBitmap) - - viewModel.receiver.observe { - receiveFrom.setTextIcon(it.addressModel.image) - receiveFrom.primaryIcon.loadTokenIcon(it.chainAssetIcon, imageLoader) - receiveFrom.setMessage(it.addressModel.address) - receiveFrom.setLabel(it.chain.name) - } - - viewModel.toolbarTitle.observe(receiveToolbar::setTitle) + viewModel.chainFlow.observe(receiveChain::setChain) + viewModel.titleFlow.observe(receiveTitle::setText) + viewModel.subtitleFlow.observe(receiveSubtitle::setText) + viewModel.qrCodeFlow.observe(receiveQrCode::setQrModel) + viewModel.accountNameFlow.observe(receiveAccount::setText) + viewModel.addressFlow.observe(receiveAddress::setText) viewModel.shareEvent.observeEvent(::startQrSharingIntent) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/ReceiveViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/ReceiveViewModel.kt index c0f8b830f1..cf0d9c3777 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/ReceiveViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/ReceiveViewModel.kt @@ -1,28 +1,29 @@ package io.novafoundation.nova.feature_assets.presentation.receive +import android.graphics.Bitmap import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ClipboardManager import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.QrCodeGenerator import io.novafoundation.nova.common.utils.flowOf -import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.invoke import io.novafoundation.nova.common.utils.lazyAsync +import io.novafoundation.nova.common.view.QrCodeModel import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.addressIn -import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAddressModel -import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.domain.receive.ReceiveInteractor import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.receive.model.QrSharingPayload -import io.novafoundation.nova.feature_assets.presentation.receive.model.TokenReceiver import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset @@ -30,71 +31,77 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +private const val COLORED_OVERLAY_PADDING_DP = 0 +private const val WHITE_OVERLAY_PADDING_DP = 8 + class ReceiveViewModel( private val interactor: ReceiveInteractor, private val qrCodeGenerator: QrCodeGenerator, - private val addressIconGenerator: AddressIconGenerator, private val resourceManager: ResourceManager, - private val externalActions: ExternalActions.Presentation, private val assetPayload: AssetPayload, private val chainRegistry: ChainRegistry, selectedAccountUseCase: SelectedAccountUseCase, private val router: AssetsRouter, -) : BaseViewModel(), ExternalActions by externalActions { + private val clipboardManager: ClipboardManager, + private val assetIconProvider: AssetIconProvider +) : BaseViewModel() { private val selectedMetaAccountFlow = selectedAccountUseCase.selectedMetaAccountFlow() + .shareInBackground() private val chainWithAssetAsync by lazyAsync { chainRegistry.chainWithAsset(assetPayload.chainId, assetPayload.chainAssetId) } - val qrBitmapFlow = flowOf { - val qrString = interactor.getQrCodeSharingString(assetPayload.chainId) + val chainFlow = flowOf { mapChainToUi(chainWithAssetAsync().chain) } - qrCodeGenerator.generateQrBitmap(qrString) + val titleFlow = flowOf { + val (_, chainAsset) = chainWithAssetAsync() + resourceManager.getString(R.string.wallet_asset_receive_token, chainAsset.symbol) } - .inBackground() - .share() - - val receiver = selectedMetaAccountFlow - .map { - val (chain, chainAsset) = chainWithAssetAsync() - val address = it.addressIn(chain)!! - - TokenReceiver( - addressModel = addressIconGenerator.createAddressModel(chain, address, AddressIconGenerator.SIZE_BIG, it.name), - chain = mapChainToUi(chain), - chainAssetIcon = chainAsset.iconUrl - ) - } - .inBackground() - .share() - val toolbarTitle = flowOf { - val (_, chainAsset) = chainWithAssetAsync() + val subtitleFlow = flowOf { + val (chain, chainAsset) = chainWithAssetAsync() + resourceManager.getString(R.string.wallet_asset_receive_token_subtitle, chainAsset.symbol, chain.name) + } - resourceManager.getString(R.string.wallet_asset_receive_token, chainAsset.symbol) + val qrCodeFlow = flowOf { + val assetIconMode = interactor.getAssetIconMode() + val qrInput = interactor.getQrCodeSharingString(assetPayload.chainId) + + val (overlayPadding, overlayBackground) = when (assetIconMode) { + AssetIconMode.COLORED -> COLORED_OVERLAY_PADDING_DP to null + AssetIconMode.WHITE -> WHITE_OVERLAY_PADDING_DP to resourceManager.getDrawable(R.drawable.bg_common_circle) + } + + QrCodeModel( + qrCodeGenerator.generateQrCode(qrInput), + overlayBackground, + overlayPadding, + assetIconProvider.getAssetIconOrFallback(chainWithAssetAsync().asset) + ) } - .inBackground() - .share() + + val accountNameFlow = selectedMetaAccountFlow.map { it.name } + + val addressFlow = selectedMetaAccountFlow.map { it.addressIn(chainWithAssetAsync().chain)!! } private val _shareEvent = MutableLiveData>() val shareEvent: LiveData> = _shareEvent - fun recipientClicked() = launch { - val accountAddress = receiver.first().addressModel.address - val (chain, _) = chainWithAssetAsync() + fun copyAddressClicked() = launch { + val accountAddress = addressFlow.first() + clipboardManager.addToClipboard(accountAddress) - externalActions.showExternalActions(ExternalActions.Type.Address(accountAddress), chain) + showToast(resourceManager.getString(io.novafoundation.nova.common.R.string.common_copied)) } fun backClicked() { router.back() } - fun shareButtonClicked() = launch { - val qrBitmap = qrBitmapFlow.first() - val address = receiver.first().addressModel.address + fun shareButtonClicked(qrBitmap: Bitmap) = launch { + val address = addressFlow.first() val (chain, chainAsset) = chainWithAssetAsync() viewModelScope.launch { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/di/ReceiveModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/di/ReceiveModule.kt index 52d8931d6c..022525f7e7 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/di/ReceiveModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/di/ReceiveModule.kt @@ -6,16 +6,17 @@ import androidx.lifecycle.ViewModelProvider import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap -import io.novafoundation.nova.common.address.AddressIconGenerator +import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository import io.novafoundation.nova.common.di.scope.ScreenScope import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.interfaces.FileProvider +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ClipboardManager import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.QrCodeGenerator import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase -import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions import io.novafoundation.nova.feature_assets.domain.receive.ReceiveInteractor import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_assets.presentation.AssetsRouter @@ -31,7 +32,8 @@ class ReceiveModule { fileProvider: FileProvider, chainRegistry: ChainRegistry, accountRepository: AccountRepository, - ) = ReceiveInteractor(fileProvider, chainRegistry, accountRepository) + assetsIconModeRepository: AssetsIconModeRepository + ) = ReceiveInteractor(fileProvider, chainRegistry, accountRepository, assetsIconModeRepository) @Provides @IntoMap @@ -39,24 +41,24 @@ class ReceiveModule { fun provideViewModel( interactor: ReceiveInteractor, qrCodeGenerator: QrCodeGenerator, - addressIconGenerator: AddressIconGenerator, resourceManager: ResourceManager, - externalActions: ExternalActions.Presentation, router: AssetsRouter, chainRegistry: ChainRegistry, selectedAccountUseCase: SelectedAccountUseCase, payload: AssetPayload, + clipboardManager: ClipboardManager, + assetIconProvider: AssetIconProvider ): ViewModel { return ReceiveViewModel( interactor, qrCodeGenerator, - addressIconGenerator, resourceManager, - externalActions, payload, chainRegistry, selectedAccountUseCase, router, + clipboardManager, + assetIconProvider ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/AssetReceiveFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/AssetReceiveFlowViewModel.kt deleted file mode 100644 index 9a76d56b64..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/AssetReceiveFlowViewModel.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.receive.flow - -import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase -import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup -import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance -import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload -import io.novafoundation.nova.feature_assets.presentation.AssetsRouter -import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.flow.AssetFlowViewModel -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel -import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor -import kotlinx.coroutines.flow.Flow - -class AssetReceiveFlowViewModel( - interactor: AssetSearchInteractor, - router: AssetsRouter, - currencyInteractor: CurrencyInteractor, - externalBalancesInteractor: ExternalBalancesInteractor, - controllableAssetCheck: ControllableAssetCheckMixin, - accountUseCase: SelectedAccountUseCase, - resourceManager: ResourceManager, -) : AssetFlowViewModel( - interactor, - router, - currencyInteractor, - controllableAssetCheck, - accountUseCase, - externalBalancesInteractor, - resourceManager, -) { - override fun searchAssetsFlow(): Flow>> { - return interactor.searchAssetsFlow(query, externalBalancesFlow) - } - - override fun assetClicked(assetModel: AssetModel) { - validate(assetModel) { - openNextScreen(assetModel) - } - } - - private fun openNextScreen(assetModel: AssetModel) { - val chainAsset = assetModel.token.configuration - val assePayload = AssetPayload(chainAsset.chainId, chainAsset.id) - router.openReceive(assePayload) - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/AssetReceiveFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/AssetReceiveFlowFragment.kt similarity index 87% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/AssetReceiveFlowFragment.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/AssetReceiveFlowFragment.kt index 7223786337..2efa084697 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/AssetReceiveFlowFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/AssetReceiveFlowFragment.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_assets.presentation.receive.flow +package io.novafoundation.nova.feature_assets.presentation.receive.flow.asset import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent -import io.novafoundation.nova.feature_assets.presentation.flow.AssetFlowFragment +import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowFragment class AssetReceiveFlowFragment : AssetFlowFragment() { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/AssetReceiveFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/AssetReceiveFlowViewModel.kt new file mode 100644 index 0000000000..761aa0d94d --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/AssetReceiveFlowViewModel.kt @@ -0,0 +1,66 @@ +package io.novafoundation.nova.feature_assets.presentation.receive.flow.asset + +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory +import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow + +class AssetReceiveFlowViewModel( + interactorFactory: AssetSearchInteractorFactory, + router: AssetsRouter, + currencyInteractor: CurrencyInteractor, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + assetIconProvider: AssetIconProvider, + assetViewModeInteractor: AssetViewModeInteractor, + amountFormatter: AmountFormatter +) : AssetFlowViewModel( + interactorFactory, + router, + currencyInteractor, + controllableAssetCheck, + accountUseCase, + externalBalancesInteractor, + resourceManager, + assetIconProvider, + assetViewModeInteractor, + amountFormatter +) { + override fun searchAssetsFlow(): Flow { + return interactor.searchReceiveAssetsFlow(query, externalBalancesFlow) + } + + override fun assetClicked(asset: Chain.Asset) { + validate(asset) { + openNextScreen(asset) + } + } + + override fun tokenClicked(tokenGroup: TokenGroupUi) { + when (val type = tokenGroup.groupType) { + is TokenGroupUi.GroupType.SingleItem -> assetClicked(type.asset) + + TokenGroupUi.GroupType.Group -> router.openReceiveNetworks(NetworkFlowPayload(tokenGroup.tokenSymbol)) + } + } + + private fun openNextScreen(asset: Chain.Asset) { + val assetPayload = AssetPayload(asset.chainId, asset.id) + router.openReceive(assetPayload) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/di/AssetReceiveFlowComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/di/AssetReceiveFlowComponent.kt similarity index 91% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/di/AssetReceiveFlowComponent.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/di/AssetReceiveFlowComponent.kt index 5bc9f0c190..788b624e45 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/di/AssetReceiveFlowComponent.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/di/AssetReceiveFlowComponent.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_assets.presentation.receive.flow.di +package io.novafoundation.nova.feature_assets.presentation.receive.flow.asset.di import androidx.fragment.app.Fragment import dagger.BindsInstance import dagger.Subcomponent import io.novafoundation.nova.common.di.scope.ScreenScope -import io.novafoundation.nova.feature_assets.presentation.receive.flow.AssetReceiveFlowFragment +import io.novafoundation.nova.feature_assets.presentation.receive.flow.asset.AssetReceiveFlowFragment @Subcomponent( modules = [ diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/di/AssetReceiveFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/di/AssetReceiveFlowModule.kt similarity index 71% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/di/AssetReceiveFlowModule.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/di/AssetReceiveFlowModule.kt index f85f02ef93..5dc695fdec 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/di/AssetReceiveFlowModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/asset/di/AssetReceiveFlowModule.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_assets.presentation.receive.flow.di +package io.novafoundation.nova.feature_assets.presentation.receive.flow.asset.di import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel @@ -8,14 +8,17 @@ import dagger.Provides import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.receive.flow.AssetReceiveFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.receive.flow.asset.AssetReceiveFlowViewModel import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter @Module(includes = [ViewModelModule::class]) class AssetReceiveFlowModule { @@ -29,22 +32,28 @@ class AssetReceiveFlowModule { @IntoMap @ViewModelKey(AssetReceiveFlowViewModel::class) fun provideViewModel( - interactor: AssetSearchInteractor, + interactorFactory: AssetSearchInteractorFactory, router: AssetsRouter, currencyInteractor: CurrencyInteractor, externalBalancesInteractor: ExternalBalancesInteractor, controllableAssetCheck: ControllableAssetCheckMixin, accountUseCase: SelectedAccountUseCase, - resourceManager: ResourceManager + resourceManager: ResourceManager, + assetIconProvider: AssetIconProvider, + assetViewModeInteractor: AssetViewModeInteractor, + amountFormatter: AmountFormatter ): ViewModel { return AssetReceiveFlowViewModel( - interactor = interactor, + interactorFactory = interactorFactory, router = router, currencyInteractor = currencyInteractor, externalBalancesInteractor = externalBalancesInteractor, controllableAssetCheck = controllableAssetCheck, accountUseCase = accountUseCase, - resourceManager = resourceManager + resourceManager = resourceManager, + assetIconProvider = assetIconProvider, + assetViewModeInteractor = assetViewModeInteractor, + amountFormatter = amountFormatter ) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/NetworkReceiveFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/NetworkReceiveFlowFragment.kt new file mode 100644 index 0000000000..a936e0abf0 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/NetworkReceiveFlowFragment.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.feature_assets.presentation.receive.flow.network + +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.payload +import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi +import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowFragment + +class NetworkReceiveFlowFragment : + NetworkFlowFragment() { + + override fun inject() { + FeatureUtils.getFeature(this, AssetsFeatureApi::class.java) + .networkReceiveFlowComponent() + .create(this, payload()) + .inject(this) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/NetworkReceiveFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/NetworkReceiveFlowViewModel.kt new file mode 100644 index 0000000000..b8802c081f --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/NetworkReceiveFlowViewModel.kt @@ -0,0 +1,57 @@ +package io.novafoundation.nova.feature_assets.presentation.receive.flow.network + +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.TokenSymbol +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.model.NetworkFlowRvItem +import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import kotlinx.coroutines.flow.Flow + +class NetworkReceiveFlowViewModel( + interactor: AssetNetworksInteractor, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + networkFlowPayload: NetworkFlowPayload, + chainRegistry: ChainRegistry +) : NetworkFlowViewModel( + interactor, + router, + controllableAssetCheck, + accountUseCase, + externalBalancesInteractor, + resourceManager, + networkFlowPayload, + chainRegistry +) { + + override fun getAssetBalance(asset: AssetWithNetwork): PricedAmount { + return asset.balanceWithOffChain.total + } + + override fun assetsFlow(tokenSymbol: TokenSymbol): Flow> { + return interactor.receiveAssetFlow(tokenSymbol, externalBalancesFlow) + } + + override fun networkClicked(network: NetworkFlowRvItem) { + validateControllsAsset(network) { + router.openReceive(AssetPayload(network.chainId, network.assetId)) + } + } + + override fun getTitle(tokenSymbol: TokenSymbol): String { + return resourceManager.getString(R.string.receive_network_flow_title, tokenSymbol.value) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/di/NetworkReceiveFlowComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/di/NetworkReceiveFlowComponent.kt new file mode 100644 index 0000000000..4f680b0ce1 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/di/NetworkReceiveFlowComponent.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_assets.presentation.receive.flow.network.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.receive.flow.network.NetworkReceiveFlowFragment + +@Subcomponent( + modules = [ + NetworkReceiveFlowModule::class + ] +) +@ScreenScope +interface NetworkReceiveFlowComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment, + @BindsInstance networkFlowPayload: NetworkFlowPayload + ): NetworkReceiveFlowComponent + } + + fun inject(fragment: NetworkReceiveFlowFragment) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/di/NetworkReceiveFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/di/NetworkReceiveFlowModule.kt new file mode 100644 index 0000000000..326185c0bd --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/flow/network/di/NetworkReceiveFlowModule.kt @@ -0,0 +1,53 @@ +package io.novafoundation.nova.feature_assets.presentation.receive.flow.network.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.receive.flow.network.NetworkReceiveFlowViewModel +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +@Module(includes = [ViewModelModule::class]) +class NetworkReceiveFlowModule { + + @Provides + internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): NetworkReceiveFlowViewModel { + return ViewModelProvider(fragment, factory).get(NetworkReceiveFlowViewModel::class.java) + } + + @Provides + @IntoMap + @ViewModelKey(NetworkReceiveFlowViewModel::class) + fun provideViewModel( + interactor: AssetNetworksInteractor, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + networkFlowPayload: NetworkFlowPayload, + chainRegistry: ChainRegistry + ): ViewModel { + return NetworkReceiveFlowViewModel( + interactor = interactor, + router = router, + externalBalancesInteractor = externalBalancesInteractor, + controllableAssetCheck = controllableAssetCheck, + accountUseCase = accountUseCase, + resourceManager = resourceManager, + networkFlowPayload = networkFlowPayload, + chainRegistry = chainRegistry + ) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/model/TokenReceiver.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/model/TokenReceiver.kt index 71ce98ed89..a9edb9ac2a 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/model/TokenReceiver.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/model/TokenReceiver.kt @@ -1,10 +1,11 @@ package io.novafoundation.nova.feature_assets.presentation.receive.model import io.novafoundation.nova.common.address.AddressModel +import io.novafoundation.nova.common.utils.images.Icon import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi class TokenReceiver( val addressModel: AddressModel, val chain: ChainUi, - val chainAssetIcon: String? + val chainAssetIcon: Icon ) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/AssetSendFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/AssetSendFlowViewModel.kt deleted file mode 100644 index c0114504de..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/AssetSendFlowViewModel.kt +++ /dev/null @@ -1,69 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.send.flow - -import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.view.PlaceholderModel -import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase -import io.novafoundation.nova.feature_assets.R -import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup -import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance -import io.novafoundation.nova.feature_assets.presentation.AssetsRouter -import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.balance.common.mapGroupedAssetsToUi -import io.novafoundation.nova.feature_assets.presentation.flow.AssetFlowViewModel -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel -import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload -import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor -import io.novafoundation.nova.feature_currency_api.domain.model.Currency -import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload -import kotlinx.coroutines.flow.Flow - -class AssetSendFlowViewModel( - interactor: AssetSearchInteractor, - router: AssetsRouter, - currencyInteractor: CurrencyInteractor, - externalBalancesInteractor: ExternalBalancesInteractor, - controllableAssetCheck: ControllableAssetCheckMixin, - accountUseCase: SelectedAccountUseCase, - resourceManager: ResourceManager, -) : AssetFlowViewModel( - interactor, - router, - currencyInteractor, - controllableAssetCheck, - accountUseCase, - externalBalancesInteractor, - resourceManager, -) { - - override fun searchAssetsFlow(): Flow>> { - return interactor.sendAssetSearch(query, externalBalancesFlow) - } - - override fun assetClicked(assetModel: AssetModel) { - val chainAsset = assetModel.token.configuration - val assetPayload = AssetPayload(chainAsset.chainId, chainAsset.id) - router.openSend(SendPayload.SpecifiedOrigin(assetPayload)) - } - - override fun mapAssets(assets: Map>, currency: Currency): List { - return assets.mapGroupedAssetsToUi(currency, AssetGroup::groupTransferableBalanceFiat, AssetWithOffChainBalance.Balance::transferable) - } - - override fun getPlaceholder(query: String, assets: List): PlaceholderModel? { - if (query.isEmpty() && assets.isEmpty()) { - return PlaceholderModel( - text = resourceManager.getString(R.string.assets_send_flow_placeholder), - imageRes = R.drawable.ic_no_search_results, - buttonText = resourceManager.getString(R.string.assets_buy_tokens_placeholder_button), - ) - } else { - return super.getPlaceholder(query, assets) - } - } - - fun openBuyFlow() { - router.openBuyFlowFromSendFlow() - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/AssetSendFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/AssetSendFlowFragment.kt similarity index 90% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/AssetSendFlowFragment.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/AssetSendFlowFragment.kt index a0acd40034..d1777f17c2 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/AssetSendFlowFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/AssetSendFlowFragment.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_assets.presentation.send.flow +package io.novafoundation.nova.feature_assets.presentation.send.flow.asset import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent -import io.novafoundation.nova.feature_assets.presentation.flow.AssetFlowFragment +import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowFragment import kotlinx.android.synthetic.main.fragment_asset_flow_search.assetFlowPlaceholder class AssetSendFlowFragment : AssetFlowFragment() { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/AssetSendFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/AssetSendFlowViewModel.kt new file mode 100644 index 0000000000..135a2126af --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/AssetSendFlowViewModel.kt @@ -0,0 +1,105 @@ +package io.novafoundation.nova.feature_assets.presentation.send.flow.asset + +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.view.PlaceholderModel +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetBalance +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapTokenAssetGroupToUi +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapGroupedAssetsToUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload +import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_currency_api.domain.model.Currency +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow + +class AssetSendFlowViewModel( + interactorFactory: AssetSearchInteractorFactory, + router: AssetsRouter, + currencyInteractor: CurrencyInteractor, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + private val assetIconProvider: AssetIconProvider, + assetViewModeInteractor: AssetViewModeInteractor, + private val amountFormatter: AmountFormatter +) : AssetFlowViewModel( + interactorFactory, + router, + currencyInteractor, + controllableAssetCheck, + accountUseCase, + externalBalancesInteractor, + resourceManager, + assetIconProvider, + assetViewModeInteractor, + amountFormatter +) { + + override fun searchAssetsFlow(): Flow { + return interactor.sendAssetSearch(query, externalBalancesFlow) + } + + override fun assetClicked(asset: Chain.Asset) { + val assetPayload = AssetPayload(asset.chainId, asset.id) + router.openSend(SendPayload.SpecifiedOrigin(assetPayload)) + } + + override fun tokenClicked(tokenGroup: TokenGroupUi) { + when (val type = tokenGroup.groupType) { + is TokenGroupUi.GroupType.SingleItem -> assetClicked(type.asset) + + TokenGroupUi.GroupType.Group -> router.openSendNetworks(NetworkFlowPayload(tokenGroup.tokenSymbol)) + } + } + + override fun mapNetworkAssets(assets: Map>, currency: Currency): List { + return assets.mapGroupedAssetsToUi( + amountFormatter, + assetIconProvider, + currency, + NetworkAssetGroup::groupTransferableBalanceFiat, + AssetBalance::transferable + ) + } + + override fun mapTokensAssets(assets: Map>): List { + return assets.map { (group, assets) -> + mapTokenAssetGroupToUi(amountFormatter, assetIconProvider, group, assets = assets) { it.groupBalance.transferable } + } + } + + override fun getPlaceholder(query: String, assets: List): PlaceholderModel? { + if (query.isEmpty() && assets.isEmpty()) { + return PlaceholderModel( + text = resourceManager.getString(R.string.assets_send_flow_placeholder), + imageRes = R.drawable.ic_no_search_results, + buttonText = resourceManager.getString(R.string.assets_buy_tokens_placeholder_button), + ) + } else { + return super.getPlaceholder(query, assets) + } + } + + fun openBuyFlow() { + router.openBuyFlowFromSendFlow() + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/di/AssetSendFlowComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/di/AssetSendFlowComponent.kt similarity index 93% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/di/AssetSendFlowComponent.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/di/AssetSendFlowComponent.kt index 66314048dd..3f206c0621 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/di/AssetSendFlowComponent.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/di/AssetSendFlowComponent.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_assets.presentation.send.flow.di +package io.novafoundation.nova.feature_assets.presentation.send.flow.asset.di import androidx.fragment.app.Fragment import dagger.BindsInstance import dagger.Subcomponent import io.novafoundation.nova.common.di.scope.ScreenScope -import io.novafoundation.nova.feature_assets.presentation.send.flow.AssetSendFlowFragment +import io.novafoundation.nova.feature_assets.presentation.send.flow.asset.AssetSendFlowFragment @Subcomponent( modules = [ diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/di/AssetSendFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/di/AssetSendFlowModule.kt similarity index 71% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/di/AssetSendFlowModule.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/di/AssetSendFlowModule.kt index 79ce136139..1c9ede5d73 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/di/AssetSendFlowModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/asset/di/AssetSendFlowModule.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_assets.presentation.send.flow.di +package io.novafoundation.nova.feature_assets.presentation.send.flow.asset.di import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel @@ -8,14 +8,17 @@ import dagger.Provides import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.send.flow.AssetSendFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.send.flow.asset.AssetSendFlowViewModel import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter @Module(includes = [ViewModelModule::class]) class AssetSendFlowModule { @@ -29,22 +32,28 @@ class AssetSendFlowModule { @IntoMap @ViewModelKey(AssetSendFlowViewModel::class) fun provideViewModel( - interactor: AssetSearchInteractor, + interactorFactory: AssetSearchInteractorFactory, router: AssetsRouter, currencyInteractor: CurrencyInteractor, externalBalancesInteractor: ExternalBalancesInteractor, controllableAssetCheck: ControllableAssetCheckMixin, accountUseCase: SelectedAccountUseCase, - resourceManager: ResourceManager + resourceManager: ResourceManager, + assetIconProvider: AssetIconProvider, + assetViewModeInteractor: AssetViewModeInteractor, + amountFormatter: AmountFormatter ): ViewModel { return AssetSendFlowViewModel( - interactor = interactor, + interactorFactory = interactorFactory, router = router, currencyInteractor = currencyInteractor, externalBalancesInteractor = externalBalancesInteractor, controllableAssetCheck = controllableAssetCheck, accountUseCase = accountUseCase, - resourceManager = resourceManager + resourceManager = resourceManager, + assetIconProvider = assetIconProvider, + assetViewModeInteractor = assetViewModeInteractor, + amountFormatter = amountFormatter ) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/NetworkSendFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/NetworkSendFlowFragment.kt new file mode 100644 index 0000000000..cb2b8f32ba --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/NetworkSendFlowFragment.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.feature_assets.presentation.send.flow.network + +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.payload +import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi +import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowFragment + +class NetworkSendFlowFragment : + NetworkFlowFragment() { + + override fun inject() { + FeatureUtils.getFeature(this, AssetsFeatureApi::class.java) + .networkSendFlowComponent() + .create(this, payload()) + .inject(this) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/NetworkSendFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/NetworkSendFlowViewModel.kt new file mode 100644 index 0000000000..31ac3f2e79 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/NetworkSendFlowViewModel.kt @@ -0,0 +1,57 @@ +package io.novafoundation.nova.feature_assets.presentation.send.flow.network + +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.TokenSymbol +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.model.NetworkFlowRvItem +import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload +import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import kotlinx.coroutines.flow.Flow + +class NetworkSendFlowViewModel( + interactor: AssetNetworksInteractor, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + networkFlowPayload: NetworkFlowPayload, + chainRegistry: ChainRegistry +) : NetworkFlowViewModel( + interactor, + router, + controllableAssetCheck, + accountUseCase, + externalBalancesInteractor, + resourceManager, + networkFlowPayload, + chainRegistry +) { + + override fun getAssetBalance(asset: AssetWithNetwork): PricedAmount { + return asset.balanceWithOffChain.transferable + } + + override fun assetsFlow(tokenSymbol: TokenSymbol): Flow> { + return interactor.sendAssetFlow(tokenSymbol, externalBalancesFlow) + } + + override fun networkClicked(network: NetworkFlowRvItem) { + val assetPayload = AssetPayload(network.chainId, network.assetId) + router.openSend(SendPayload.SpecifiedOrigin(assetPayload)) + } + + override fun getTitle(tokenSymbol: TokenSymbol): String { + return resourceManager.getString(R.string.send_network_flow_title, tokenSymbol.value) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/di/NetworkSendFlowComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/di/NetworkSendFlowComponent.kt new file mode 100644 index 0000000000..d67d7cd930 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/di/NetworkSendFlowComponent.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_assets.presentation.send.flow.network.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.send.flow.network.NetworkSendFlowFragment + +@Subcomponent( + modules = [ + NetworkSendFlowModule::class + ] +) +@ScreenScope +interface NetworkSendFlowComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment, + @BindsInstance networkFlowPayload: NetworkFlowPayload + ): NetworkSendFlowComponent + } + + fun inject(fragment: NetworkSendFlowFragment) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/di/NetworkSendFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/di/NetworkSendFlowModule.kt new file mode 100644 index 0000000000..e20a497bea --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/flow/network/di/NetworkSendFlowModule.kt @@ -0,0 +1,53 @@ +package io.novafoundation.nova.feature_assets.presentation.send.flow.network.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.send.flow.network.NetworkSendFlowViewModel +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +@Module(includes = [ViewModelModule::class]) +class NetworkSendFlowModule { + + @Provides + internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): NetworkSendFlowViewModel { + return ViewModelProvider(fragment, factory).get(NetworkSendFlowViewModel::class.java) + } + + @Provides + @IntoMap + @ViewModelKey(NetworkSendFlowViewModel::class) + fun provideViewModel( + interactor: AssetNetworksInteractor, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + networkFlowPayload: NetworkFlowPayload, + chainRegistry: ChainRegistry + ): ViewModel { + return NetworkSendFlowViewModel( + interactor = interactor, + router = router, + externalBalancesInteractor = externalBalancesInteractor, + controllableAssetCheck = controllableAssetCheck, + accountUseCase = accountUseCase, + resourceManager = resourceManager, + networkFlowPayload = networkFlowPayload, + chainRegistry = chainRegistry + ) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt deleted file mode 100644 index 53635a063f..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt +++ /dev/null @@ -1,79 +0,0 @@ -package io.novafoundation.nova.feature_assets.presentation.swap - -import androidx.annotation.StringRes -import androidx.lifecycle.viewModelScope -import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase -import io.novafoundation.nova.feature_assets.R -import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor -import io.novafoundation.nova.feature_assets.domain.common.AssetGroup -import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance -import io.novafoundation.nova.feature_assets.presentation.AssetsRouter -import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.balance.common.mapGroupedAssetsToUi -import io.novafoundation.nova.feature_assets.presentation.flow.AssetFlowViewModel -import io.novafoundation.nova.feature_assets.presentation.model.AssetModel -import io.novafoundation.nova.feature_assets.presentation.swap.executor.SwapFlowExecutor -import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor -import io.novafoundation.nova.feature_currency_api.domain.model.Currency -import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor -import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch - -class AssetSwapFlowViewModel( - interactor: AssetSearchInteractor, - router: AssetsRouter, - currencyInteractor: CurrencyInteractor, - externalBalancesInteractor: ExternalBalancesInteractor, - controllableAssetCheck: ControllableAssetCheckMixin, - accountUseCase: SelectedAccountUseCase, - resourceManager: ResourceManager, - private val swapAvailabilityInteractor: SwapAvailabilityInteractor, - private val swapFlowExecutor: SwapFlowExecutor, - private val payload: SwapFlowPayload -) : AssetFlowViewModel( - interactor, - router, - currencyInteractor, - controllableAssetCheck, - accountUseCase, - externalBalancesInteractor, - resourceManager, -) { - - init { - launch { - swapAvailabilityInteractor.sync(viewModelScope) - } - } - - @StringRes - fun getTitleRes(): Int { - return when (payload) { - SwapFlowPayload.InitialSelecting, is SwapFlowPayload.ReselectAssetIn -> R.string.assets_swap_flow_pay_title - is SwapFlowPayload.ReselectAssetOut -> R.string.assets_swap_flow_receive_title - } - } - - override fun searchAssetsFlow(): Flow>> { - return interactor.searchSwapAssetsFlow( - forAsset = payload.constraintDirectionsAsset?.fullChainAssetId, - queryFlow = query, - externalBalancesFlow = externalBalancesFlow, - coroutineScope = viewModelScope - ) - } - - override fun assetClicked(assetModel: AssetModel) { - launch { - val chainAsset = assetModel.token.configuration - swapFlowExecutor.openNextScreen(viewModelScope, chainAsset) - } - } - - override fun mapAssets(assets: Map>, currency: Currency): List { - return assets.mapGroupedAssetsToUi(currency, AssetGroup::groupTransferableBalanceFiat, AssetWithOffChainBalance.Balance::transferable) - } -} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowFragment.kt similarity index 85% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowFragment.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowFragment.kt index 464c7fb33a..7837929571 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowFragment.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_assets.presentation.swap +package io.novafoundation.nova.feature_assets.presentation.swap.asset import android.os.Bundle import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent -import io.novafoundation.nova.feature_assets.presentation.flow.AssetFlowFragment +import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowFragment class AssetSwapFlowFragment : AssetFlowFragment() { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowViewModel.kt new file mode 100644 index 0000000000..820d286688 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowViewModel.kt @@ -0,0 +1,117 @@ +package io.novafoundation.nova.feature_assets.presentation.swap.asset + +import androidx.annotation.StringRes +import androidx.lifecycle.viewModelScope +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.launchUnit +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup +import io.novafoundation.nova.feature_assets.domain.common.AssetBalance +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapTokenAssetGroupToUi +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapGroupedAssetsToUi +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi +import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.executor.SwapFlowExecutor +import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowPayload +import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor +import io.novafoundation.nova.feature_currency_api.domain.model.Currency +import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +class AssetSwapFlowViewModel( + interactorFactory: AssetSearchInteractorFactory, + router: AssetsRouter, + currencyInteractor: CurrencyInteractor, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + private val swapAvailabilityInteractor: SwapAvailabilityInteractor, + private val swapFlowExecutor: SwapFlowExecutor, + private val swapPayload: SwapFlowPayload, + private val assetIconProvider: AssetIconProvider, + assetViewModeInteractor: AssetViewModeInteractor, + private val amountFormatter: AmountFormatter +) : AssetFlowViewModel( + interactorFactory, + router, + currencyInteractor, + controllableAssetCheck, + accountUseCase, + externalBalancesInteractor, + resourceManager, + assetIconProvider, + assetViewModeInteractor, + amountFormatter +) { + + init { + launch { + swapAvailabilityInteractor.sync(viewModelScope) + } + } + + @StringRes + fun getTitleRes(): Int { + return when (swapPayload) { + SwapFlowPayload.InitialSelecting, is SwapFlowPayload.ReselectAssetIn -> R.string.assets_swap_flow_pay_title + is SwapFlowPayload.ReselectAssetOut -> R.string.assets_swap_flow_receive_title + } + } + + override fun searchAssetsFlow(): Flow { + return interactor.searchSwapAssetsFlow( + forAsset = swapPayload.constraintDirectionsAsset?.fullChainAssetId, + queryFlow = query, + externalBalancesFlow = externalBalancesFlow, + coroutineScope = viewModelScope + ) + } + + override fun assetClicked(asset: Chain.Asset) { + launch { + swapFlowExecutor.openNextScreen(viewModelScope, asset) + } + } + + override fun tokenClicked(tokenGroup: TokenGroupUi) = launchUnit { + when (val type = tokenGroup.groupType) { + is TokenGroupUi.GroupType.SingleItem -> assetClicked(type.asset) + + TokenGroupUi.GroupType.Group -> router.openSwapNetworks(NetworkSwapFlowPayload(NetworkFlowPayload(tokenGroup.tokenSymbol), swapPayload)) + } + } + + override fun mapNetworkAssets(assets: Map>, currency: Currency): List { + return assets.mapGroupedAssetsToUi( + amountFormatter, + assetIconProvider, + currency, + NetworkAssetGroup::groupTransferableBalanceFiat, + AssetBalance::transferable + ) + } + + override fun mapTokensAssets(assets: Map>): List { + return assets.map { (group, assets) -> + mapTokenAssetGroupToUi(amountFormatter, assetIconProvider, group, assets = assets) { it.groupBalance.transferable } + } + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/SwapFlowPayload.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/SwapFlowPayload.kt similarity index 91% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/SwapFlowPayload.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/SwapFlowPayload.kt index 6415528db8..8c77ce0465 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/SwapFlowPayload.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/SwapFlowPayload.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_assets.presentation.swap +package io.novafoundation.nova.feature_assets.presentation.swap.asset import android.os.Parcelable import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/di/AssetSwapFlowComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/di/AssetSwapFlowComponent.kt similarity index 76% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/di/AssetSwapFlowComponent.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/di/AssetSwapFlowComponent.kt index 2163b3d2aa..02ad36b191 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/di/AssetSwapFlowComponent.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/di/AssetSwapFlowComponent.kt @@ -1,11 +1,11 @@ -package io.novafoundation.nova.feature_assets.presentation.swap.di +package io.novafoundation.nova.feature_assets.presentation.swap.asset.di import androidx.fragment.app.Fragment import dagger.BindsInstance import dagger.Subcomponent import io.novafoundation.nova.common.di.scope.ScreenScope -import io.novafoundation.nova.feature_assets.presentation.swap.AssetSwapFlowFragment -import io.novafoundation.nova.feature_assets.presentation.swap.SwapFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.asset.AssetSwapFlowFragment +import io.novafoundation.nova.feature_assets.presentation.swap.asset.SwapFlowPayload @Subcomponent( modules = [ diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/di/AssetSwapFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/di/AssetSwapFlowModule.kt similarity index 69% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/di/AssetSwapFlowModule.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/di/AssetSwapFlowModule.kt index 00cde3e117..b016c377e2 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/di/AssetSwapFlowModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/di/AssetSwapFlowModule.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_assets.presentation.swap.di +package io.novafoundation.nova.feature_assets.presentation.swap.asset.di import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel @@ -8,39 +8,24 @@ import dagger.Provides import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractor +import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.swap.AssetSwapFlowViewModel -import io.novafoundation.nova.feature_assets.presentation.swap.SwapFlowPayload -import io.novafoundation.nova.feature_assets.presentation.swap.executor.InitialSwapFlowExecutor +import io.novafoundation.nova.feature_assets.presentation.swap.asset.AssetSwapFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.swap.asset.SwapFlowPayload import io.novafoundation.nova.feature_assets.presentation.swap.executor.SwapFlowExecutorFactory import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor -import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountFormatter @Module(includes = [ViewModelModule::class]) class AssetSwapFlowModule { - @Provides - fun provideInitialSwapFlowExecutor( - assetsRouter: AssetsRouter - ): InitialSwapFlowExecutor { - return InitialSwapFlowExecutor(assetsRouter) - } - - @Provides - fun provideSwapExecutor( - initialSwapFlowExecutor: InitialSwapFlowExecutor, - assetsRouter: AssetsRouter, - swapSettingsStateProvider: SwapSettingsStateProvider - ): SwapFlowExecutorFactory { - return SwapFlowExecutorFactory(initialSwapFlowExecutor, assetsRouter, swapSettingsStateProvider) - } - @Provides internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): AssetSwapFlowViewModel { return ViewModelProvider(fragment, factory).get(AssetSwapFlowViewModel::class.java) @@ -50,7 +35,7 @@ class AssetSwapFlowModule { @IntoMap @ViewModelKey(AssetSwapFlowViewModel::class) fun provideViewModel( - interactor: AssetSearchInteractor, + interactorFactory: AssetSearchInteractorFactory, router: AssetsRouter, currencyInteractor: CurrencyInteractor, externalBalancesInteractor: ExternalBalancesInteractor, @@ -59,10 +44,13 @@ class AssetSwapFlowModule { resourceManager: ResourceManager, payload: SwapFlowPayload, executorFactory: SwapFlowExecutorFactory, - swapAvailabilityInteractor: SwapAvailabilityInteractor + swapAvailabilityInteractor: SwapAvailabilityInteractor, + assetIconProvider: AssetIconProvider, + assetViewModeInteractor: AssetViewModeInteractor, + amountFormatter: AmountFormatter ): ViewModel { return AssetSwapFlowViewModel( - interactor = interactor, + interactorFactory = interactorFactory, router = router, currencyInteractor = currencyInteractor, externalBalancesInteractor = externalBalancesInteractor, @@ -70,8 +58,11 @@ class AssetSwapFlowModule { accountUseCase = accountUseCase, resourceManager = resourceManager, swapFlowExecutor = executorFactory.create(payload), - payload = payload, - swapAvailabilityInteractor = swapAvailabilityInteractor + swapPayload = payload, + swapAvailabilityInteractor = swapAvailabilityInteractor, + assetIconProvider = assetIconProvider, + assetViewModeInteractor = assetViewModeInteractor, + amountFormatter = amountFormatter ) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/ReselectSwapFlowExecutor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/ReselectSwapFlowExecutor.kt index 7d89f5540f..5b8d2854d3 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/ReselectSwapFlowExecutor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/ReselectSwapFlowExecutor.kt @@ -21,6 +21,7 @@ class ReselectSwapFlowExecutor( SelectingDirection.IN -> state.setAssetInUpdatingFee(chainAsset) SelectingDirection.OUT -> state.setAssetOut(chainAsset) } - assetsRouter.back() + + assetsRouter.returnToMainSwapScreen() } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/SwapFlowExecutor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/SwapFlowExecutor.kt index 2bf30d9cf2..4b58983382 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/SwapFlowExecutor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/SwapFlowExecutor.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_assets.presentation.swap.executor import io.novafoundation.nova.feature_assets.presentation.AssetsRouter -import io.novafoundation.nova.feature_assets.presentation.swap.SwapFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.asset.SwapFlowPayload import io.novafoundation.nova.feature_assets.presentation.swap.executor.ReselectSwapFlowExecutor.SelectingDirection import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowFragment.kt new file mode 100644 index 0000000000..1cf3b4ac6d --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowFragment.kt @@ -0,0 +1,22 @@ +package io.novafoundation.nova.feature_assets.presentation.swap.network + +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.FragmentPayloadCreator +import io.novafoundation.nova.common.utils.PayloadCreator +import io.novafoundation.nova.common.utils.payload +import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi +import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowFragment + +class NetworkSwapFlowFragment : + NetworkFlowFragment() { + + companion object : PayloadCreator by FragmentPayloadCreator() + + override fun inject() { + FeatureUtils.getFeature(this, AssetsFeatureApi::class.java) + .networkSwapFlowComponent() + .create(this, payload()) + .inject(this) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowPayload.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowPayload.kt new file mode 100644 index 0000000000..76d901f7d2 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowPayload.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.feature_assets.presentation.swap.network + +import android.os.Parcelable +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.asset.SwapFlowPayload +import kotlinx.android.parcel.Parcelize + +@Parcelize +class NetworkSwapFlowPayload( + val networkFlowPayload: NetworkFlowPayload, + val swapFlowPayload: SwapFlowPayload +) : Parcelable diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowViewModel.kt new file mode 100644 index 0000000000..208687d1d3 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowViewModel.kt @@ -0,0 +1,62 @@ +package io.novafoundation.nova.feature_assets.presentation.swap.network + +import androidx.lifecycle.viewModelScope +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.TokenSymbol +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount +import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload +import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowViewModel +import io.novafoundation.nova.feature_assets.presentation.flow.network.model.NetworkFlowRvItem +import io.novafoundation.nova.feature_assets.presentation.swap.executor.SwapFlowExecutor +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.asset +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +class NetworkSwapFlowViewModel( + interactor: AssetNetworksInteractor, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + networkFlowPayload: NetworkFlowPayload, + chainRegistry: ChainRegistry, + private val swapFlowExecutor: SwapFlowExecutor +) : NetworkFlowViewModel( + interactor, + router, + controllableAssetCheck, + accountUseCase, + externalBalancesInteractor, + resourceManager, + networkFlowPayload, + chainRegistry +) { + + override fun getAssetBalance(asset: AssetWithNetwork): PricedAmount { + return asset.balanceWithOffChain.transferable + } + + override fun assetsFlow(tokenSymbol: TokenSymbol): Flow> { + return interactor.swapAssetsFlow(tokenSymbol, externalBalancesFlow, viewModelScope) + } + + override fun networkClicked(network: NetworkFlowRvItem) { + launch { + val chainAsset = chainRegistry.asset(network.chainId, network.assetId) + swapFlowExecutor.openNextScreen(coroutineScope, chainAsset) + } + } + + override fun getTitle(tokenSymbol: TokenSymbol): String { + return resourceManager.getString(R.string.swap_network_flow_title, tokenSymbol.value) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowComponent.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowComponent.kt new file mode 100644 index 0000000000..afb00b0880 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowComponent.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_assets.presentation.swap.network.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowFragment +import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowPayload + +@Subcomponent( + modules = [ + NetworkSwapFlowModule::class + ] +) +@ScreenScope +interface NetworkSwapFlowComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment, + @BindsInstance payload: NetworkSwapFlowPayload + ): NetworkSwapFlowComponent + } + + fun inject(fragment: NetworkSwapFlowFragment) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowModule.kt new file mode 100644 index 0000000000..23d8dc8c91 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowModule.kt @@ -0,0 +1,56 @@ +package io.novafoundation.nova.feature_assets.presentation.swap.network.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor +import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor +import io.novafoundation.nova.feature_assets.presentation.AssetsRouter +import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin +import io.novafoundation.nova.feature_assets.presentation.swap.executor.SwapFlowExecutorFactory +import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowViewModel +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +@Module(includes = [ViewModelModule::class]) +class NetworkSwapFlowModule { + + @Provides + internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): NetworkSwapFlowViewModel { + return ViewModelProvider(fragment, factory).get(NetworkSwapFlowViewModel::class.java) + } + + @Provides + @IntoMap + @ViewModelKey(NetworkSwapFlowViewModel::class) + fun provideViewModel( + interactor: AssetNetworksInteractor, + router: AssetsRouter, + externalBalancesInteractor: ExternalBalancesInteractor, + controllableAssetCheck: ControllableAssetCheckMixin, + accountUseCase: SelectedAccountUseCase, + resourceManager: ResourceManager, + executorFactory: SwapFlowExecutorFactory, + payload: NetworkSwapFlowPayload, + chainRegistry: ChainRegistry + ): ViewModel { + return NetworkSwapFlowViewModel( + interactor = interactor, + router = router, + externalBalancesInteractor = externalBalancesInteractor, + controllableAssetCheck = controllableAssetCheck, + accountUseCase = accountUseCase, + resourceManager = resourceManager, + networkFlowPayload = payload.networkFlowPayload, + chainRegistry = chainRegistry, + swapFlowExecutor = executorFactory.create(payload.swapFlowPayload) + ) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensAdapter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensAdapter.kt index c03b01efd9..56d4b8f5db 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensAdapter.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensAdapter.kt @@ -9,10 +9,9 @@ import io.novafoundation.nova.common.list.BaseListAdapter import io.novafoundation.nova.common.list.BaseViewHolder import io.novafoundation.nova.common.list.PayloadGenerator import io.novafoundation.nova.common.list.resolvePayload +import io.novafoundation.nova.common.utils.images.setIcon import io.novafoundation.nova.common.utils.inflateChild -import io.novafoundation.nova.common.utils.setImageTintRes import io.novafoundation.nova.common.utils.setTextColorRes -import io.novafoundation.nova.feature_account_api.presenatation.chain.loadTokenIcon import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.presentation.tokens.manage.model.MultiChainTokenModel import kotlinx.android.synthetic.main.item_manage_token_multichain.view.itemManageTokenMultichainEnabled @@ -79,7 +78,7 @@ class ManageTokensViewHolder( bindEnabled(item) - itemManageTokenMultichainIcon.loadTokenIcon(item.header.icon, imageLoader) + itemManageTokenMultichainIcon.setIcon(item.header.icon, imageLoader) itemManageTokenMultichainSymbol.text = item.header.symbol } @@ -91,8 +90,9 @@ class ManageTokensViewHolder( itemManageTokenMultichainEnabled.isChecked = item.enabled itemManageTokenMultichainEnabled.isEnabled = item.switchable + itemManageTokenMultichainIcon.alpha = if (item.enabled) 1f else 0.48f + val contentColorRes = if (item.enabled) R.color.text_primary else R.color.text_secondary - itemManageTokenMultichainIcon.setImageTintRes(contentColorRes) itemManageTokenMultichainSymbol.setTextColorRes(contentColorRes) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensFragment.kt index 56525822b4..4bec00e171 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensFragment.kt @@ -15,15 +15,18 @@ import io.novafoundation.nova.common.utils.keyboard.hideSoftKeyboard import io.novafoundation.nova.common.utils.setVisible import io.novafoundation.nova.common.utils.keyboard.showSoftKeyboard import io.novafoundation.nova.common.utils.submitListPreservingViewPoint +import io.novafoundation.nova.common.view.bindFromMap import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent +import io.novafoundation.nova.feature_assets.domain.assets.filters.NonZeroBalanceFilter import kotlinx.android.synthetic.main.fragment_manage_tokens.manageTokensContainer import kotlinx.android.synthetic.main.fragment_manage_tokens.manageTokensList import kotlinx.android.synthetic.main.fragment_manage_tokens.manageTokensPlaceholder import kotlinx.android.synthetic.main.fragment_manage_tokens.manageTokensSearch import kotlinx.android.synthetic.main.fragment_manage_tokens.manageTokensToolbar import javax.inject.Inject +import kotlinx.android.synthetic.main.fragment_manage_tokens.manageTokensSwitchZeroBalances class ManageTokensFragment : BaseFragment(), @@ -62,6 +65,8 @@ class ManageTokensFragment : manageTokensSearch.requestFocus() manageTokensSearch.content.showSoftKeyboard() + + manageTokensSwitchZeroBalances.bindFromMap(NonZeroBalanceFilter, viewModel.filtersEnabledMap, viewLifecycleOwner.lifecycleScope) } override fun inject() { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensViewModel.kt index c592a80b9c..f85007b99c 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/ManageTokensViewModel.kt @@ -1,7 +1,12 @@ package io.novafoundation.nova.feature_assets.presentation.tokens.manage +import androidx.lifecycle.viewModelScope import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.utils.checkEnabled +import io.novafoundation.nova.common.utils.combineIdentity import io.novafoundation.nova.common.utils.mapList +import io.novafoundation.nova.feature_assets.domain.assets.filters.AssetFilter +import io.novafoundation.nova.feature_assets.domain.assets.filters.AssetFiltersInteractor import io.novafoundation.nova.feature_assets.domain.tokens.manage.ManageTokenInteractor import io.novafoundation.nova.feature_assets.domain.tokens.manage.MultiChainToken import io.novafoundation.nova.feature_assets.domain.tokens.manage.allChainAssetIds @@ -11,15 +16,21 @@ import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.Ma import io.novafoundation.nova.feature_assets.presentation.tokens.manage.model.MultiChainTokenMapper import io.novafoundation.nova.feature_assets.presentation.tokens.manage.model.MultiChainTokenModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch class ManageTokensViewModel( private val router: AssetsRouter, private val interactor: ManageTokenInteractor, - private val commonUiMapper: MultiChainTokenMapper + private val commonUiMapper: MultiChainTokenMapper, + private val assetFiltersInteractor: AssetFiltersInteractor, ) : BaseViewModel() { + val filtersEnabledMap = assetFiltersInteractor.allFilters.associateWith { MutableStateFlow(false) } + val query = MutableStateFlow("") private val multiChainTokensFlow = interactor.multiChainTokensFlow(query) @@ -29,6 +40,10 @@ class ManageTokensViewModel( .mapList(::mapMultiChainTokenToUi) .shareInBackground() + init { + applyFiltersInitialState() + } + fun closeClicked() { router.back() } @@ -64,4 +79,24 @@ class ManageTokensViewModel( switchable = multiChainToken.isSwitchable ) } + + private fun applyFiltersInitialState() = launch { + val initialFilters = assetFiltersInteractor.currentFilters() + + filtersEnabledMap.forEach { (filter, checked) -> + checked.value = filter in initialFilters + } + + filtersEnabledMap.applyOnChange() + } + + private fun Map>.applyOnChange() { + combineIdentity(this.values) + .drop(1) + .onEach { + val enabledFilters = assetFiltersInteractor.allFilters.filter(filtersEnabledMap::checkEnabled) + assetFiltersInteractor.updateFilters(enabledFilters) + } + .launchIn(viewModelScope) + } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/chain/ManageChainTokensFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/chain/ManageChainTokensFragment.kt index b5a6c7d457..c593c40058 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/chain/ManageChainTokensFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/chain/ManageChainTokensFragment.kt @@ -8,7 +8,7 @@ import androidx.core.os.bundleOf import coil.ImageLoader import io.novafoundation.nova.common.base.BaseBottomSheetFragment import io.novafoundation.nova.common.di.FeatureUtils -import io.novafoundation.nova.feature_account_api.presenatation.chain.loadTokenIcon +import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent @@ -58,7 +58,7 @@ class ManageChainTokensFragment : override fun subscribe(viewModel: ManageChainTokensViewModel) { viewModel.headerModel.observe { headerModel -> - manageChainTokenIcon.loadTokenIcon(headerModel.icon, imageLoader) + manageChainTokenIcon.setTokenIcon(headerModel.icon, imageLoader) manageChainTokenSymbol.text = headerModel.symbol manageChainTokenSubtitle.text = headerModel.networks } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/di/ManageTokensModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/di/ManageTokensModule.kt index 28ff5f778a..7374b7e54a 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/di/ManageTokensModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/di/ManageTokensModule.kt @@ -6,8 +6,11 @@ import androidx.lifecycle.ViewModelProvider import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.scope.ScreenScope import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.feature_assets.data.repository.assetFilters.AssetFiltersRepository +import io.novafoundation.nova.feature_assets.domain.assets.filters.AssetFiltersInteractor import io.novafoundation.nova.feature_assets.domain.tokens.manage.ManageTokenInteractor import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.tokens.manage.ManageTokensViewModel @@ -16,6 +19,12 @@ import io.novafoundation.nova.feature_assets.presentation.tokens.manage.model.Mu @Module(includes = [ViewModelModule::class]) class ManageTokensModule { + @Provides + @ScreenScope + fun provideAssetFiltersInteractor( + assetFiltersRepository: AssetFiltersRepository + ) = AssetFiltersInteractor(assetFiltersRepository) + @Provides internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): ManageTokensViewModel { return ViewModelProvider(fragment, factory).get(ManageTokensViewModel::class.java) @@ -27,12 +36,14 @@ class ManageTokensModule { fun provideViewModel( router: AssetsRouter, interactor: ManageTokenInteractor, - commonUiMapper: MultiChainTokenMapper + commonUiMapper: MultiChainTokenMapper, + assetFiltersInteractor: AssetFiltersInteractor ): ViewModel { return ManageTokensViewModel( router = router, interactor = interactor, - commonUiMapper = commonUiMapper + commonUiMapper = commonUiMapper, + assetFiltersInteractor = assetFiltersInteractor ) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/model/MultiChainTokenModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/model/MultiChainTokenModel.kt index 9432c0a24b..e8bb7404fb 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/model/MultiChainTokenModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/manage/model/MultiChainTokenModel.kt @@ -1,7 +1,10 @@ package io.novafoundation.nova.feature_assets.presentation.tokens.manage.model +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.presentation.getAssetIconOrFallback import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.formatListPreview +import io.novafoundation.nova.common.utils.images.Icon import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.domain.tokens.manage.MultiChainToken @@ -12,19 +15,20 @@ data class MultiChainTokenModel( ) { class HeaderModel( - val icon: String?, + val icon: Icon, val symbol: String, val networks: String, ) } class MultiChainTokenMapper( + private val assetIconProvider: AssetIconProvider, private val resourceManager: ResourceManager ) { fun mapHeaderToUi(multiChainToken: MultiChainToken): MultiChainTokenModel.HeaderModel { return MultiChainTokenModel.HeaderModel( - icon = multiChainToken.icon, + icon = assetIconProvider.getAssetIconOrFallback(multiChainToken.icon), symbol = multiChainToken.symbol, networks = constructNetworksSubtitle(multiChainToken) ) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/di/ExtrinsicDetailModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/di/ExtrinsicDetailModule.kt index 6d8205acbc..ea43083787 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/di/ExtrinsicDetailModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/di/ExtrinsicDetailModule.kt @@ -9,6 +9,7 @@ import dagger.multibindings.IntoMap import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions @@ -30,6 +31,7 @@ class ExtrinsicDetailModule { operation: OperationParcelizeModel.Extrinsic, externalActions: ExternalActions.Presentation, resourceManager: ResourceManager, + assetIconProvider: AssetIconProvider ): ViewModel { return ExtrinsicDetailViewModel( addressDisplayUseCase, @@ -38,7 +40,8 @@ class ExtrinsicDetailModule { router, operation, externalActions, - resourceManager + resourceManager, + assetIconProvider ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/extrinsic/ExtrinsicDetailFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/extrinsic/ExtrinsicDetailFragment.kt index bb18c64e30..cb3a9b7ca6 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/extrinsic/ExtrinsicDetailFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/extrinsic/ExtrinsicDetailFragment.kt @@ -9,12 +9,12 @@ import coil.ImageLoader import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.formatting.formatDateTime +import io.novafoundation.nova.common.utils.images.setIcon import io.novafoundation.nova.common.utils.setTextColorRes import io.novafoundation.nova.common.utils.setTextOrHide import io.novafoundation.nova.common.view.TableCellView import io.novafoundation.nova.common.view.TableView import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions -import io.novafoundation.nova.feature_account_api.presenatation.chain.loadTokenIcon import io.novafoundation.nova.feature_account_api.view.showAddress import io.novafoundation.nova.feature_account_api.view.showChain import io.novafoundation.nova.feature_assets.R @@ -93,7 +93,7 @@ class ExtrinsicDetailFragment : BaseFragment() { viewModel.chainUi.observe(extrinsicDetailNetwork::showChain) viewModel.operationIcon.observe { - extrinsicDetailIcon.loadTokenIcon(it, imageLoader) + extrinsicDetailIcon.setIcon(it, imageLoader) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/extrinsic/ExtrinsicDetailViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/extrinsic/ExtrinsicDetailViewModel.kt index b655729a3c..30de9d8b79 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/extrinsic/ExtrinsicDetailViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/extrinsic/ExtrinsicDetailViewModel.kt @@ -2,6 +2,9 @@ package io.novafoundation.nova.feature_assets.presentation.transaction.detail.ex import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.presentation.getAssetIconOrFallback import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.inBackground @@ -28,6 +31,7 @@ class ExtrinsicDetailViewModel( val operation: OperationParcelizeModel.Extrinsic, private val externalActions: ExternalActions.Presentation, private val resourceManager: ResourceManager, + private val assetIconProvider: AssetIconProvider ) : BaseViewModel(), ExternalActions by externalActions { @@ -52,7 +56,7 @@ class ExtrinsicDetailViewModel( .share() val operationIcon = flowOf { - chainAsset().iconUrl + assetIconProvider.getAssetIconOrFallback(chainAsset().icon, AssetIconMode.WHITE) }.shareInBackground() val content = flowOf { @@ -91,10 +95,12 @@ class ExtrinsicDetailViewModel( label = blockEntry.label, addressModel = getIcon(blockEntry.address), ) + is ExtrinsicContentParcel.BlockEntry.LabeledValue -> ExtrinsicContentModel.BlockEntry.LabeledValue( label = blockEntry.label, value = blockEntry.value ) + is ExtrinsicContentParcel.BlockEntry.TransactionId -> ExtrinsicContentModel.BlockEntry.TransactionId( label = resourceManager.getString(R.string.common_transaction_id), hash = blockEntry.hash diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/swap/SwapDetailViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/swap/SwapDetailViewModel.kt index 6530111bcf..0ea5fa3d21 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/swap/SwapDetailViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/swap/SwapDetailViewModel.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_assets.presentation.transaction.detail.sw import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.address.AddressModel import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.invoke import io.novafoundation.nova.common.utils.lazyAsync @@ -13,7 +14,7 @@ import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions -import io.novafoundation.nova.feature_account_api.presenatation.chain.icon +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.model.ChainAssetWithAmountParcelModel @@ -49,6 +50,7 @@ class SwapDetailViewModel( private val walletUiUseCase: WalletUiUseCase, private val swapRateFormatter: SwapRateFormatter, private val descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, + private val assetIconProvider: AssetIconProvider, val operation: OperationParcelizeModel.Swap, ) : BaseViewModel(), ExternalActions by externalActions, @@ -152,7 +154,7 @@ class SwapDetailViewModel( income: Boolean ): SwapAssetView.Model { return SwapAssetView.Model( - assetIcon = token.configuration.icon(), + assetIcon = assetIconProvider.getAssetIconOrFallback(token.configuration), amount = mapAmountToAmountModel(amount, token, estimatedFiat = true), chainUi = mapChainToUi(chainRegistry.getChain(token.configuration.chainId)), amountTextColorRes = if (income) R.color.text_positive else R.color.text_primary diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/swap/di/SwapDetailModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/swap/di/SwapDetailModule.kt index fead69fb8b..bdce8bbff5 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/swap/di/SwapDetailModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/detail/swap/di/SwapDetailModule.kt @@ -9,6 +9,7 @@ import dagger.multibindings.IntoMap import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions @@ -34,6 +35,7 @@ class SwapDetailModule { arbitraryTokenUseCase: ArbitraryTokenUseCase, walletUiUseCase: WalletUiUseCase, swapRateFormatter: SwapRateFormatter, + assetIconProvider: AssetIconProvider, descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher ): ViewModel { return SwapDetailViewModel( @@ -45,7 +47,8 @@ class SwapDetailModule { arbitraryTokenUseCase = arbitraryTokenUseCase, walletUiUseCase = walletUiUseCase, swapRateFormatter = swapRateFormatter, - descriptionBottomSheetLauncher = descriptionBottomSheetLauncher + descriptionBottomSheetLauncher = descriptionBottomSheetLauncher, + assetIconProvider = assetIconProvider ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/history/mixin/OperationMappers.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/history/mixin/OperationMappers.kt index 62de34155a..0dae89bf93 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/history/mixin/OperationMappers.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/history/mixin/OperationMappers.kt @@ -5,12 +5,15 @@ import android.text.TextUtils import android.text.style.ImageSpan import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.buildSpannable import io.novafoundation.nova.common.utils.capitalize import io.novafoundation.nova.common.utils.images.asIcon import io.novafoundation.nova.common.utils.splitSnakeOrCamelCase import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.presentation.model.AmountParcelModel import io.novafoundation.nova.feature_assets.presentation.model.ChainAssetWithAmountParcelModel @@ -80,7 +83,7 @@ private fun mapStatusToStatusAppearance(status: Operation.Status): OperationStat @DrawableRes private fun transferDirectionIcon(isIncome: Boolean): Int { - return if (isIncome) R.drawable.ic_arrow_down else R.drawable.ic_arrow_up + return if (isIncome) R.drawable.ic_receive_history else R.drawable.ic_send_history } @ColorRes @@ -212,6 +215,7 @@ fun mapOperationToOperationModel( operation: Operation, nameIdentifier: AddressDisplayUseCase.Identifier, resourceManager: ResourceManager, + assetIconProvider: AssetIconProvider ): OperationModel { val statusAppearance = mapStatusToStatusAppearance(operation.status) val formattedTime = resourceManager.formatTime(operation.time) @@ -230,7 +234,7 @@ fun mapOperationToOperationModel( subHeader = resourceManager.getString(subtitleRes), subHeaderEllipsize = TextUtils.TruncateAt.END, statusAppearance = statusAppearance, - operationIcon = resourceManager.getDrawable(R.drawable.ic_staking_filled).asIcon(), + operationIcon = resourceManager.getDrawable(R.drawable.ic_staking_history).asIcon(), ) } @@ -273,7 +277,7 @@ fun mapOperationToOperationModel( subHeader = subHeader.value, subHeaderEllipsize = subHeader.elipsize, statusAppearance = statusAppearance, - operationIcon = operation.chainAsset.iconUrl?.asIcon() ?: R.drawable.ic_nova.asIcon() + operationIcon = assetIconProvider.getAssetIconOrFallback(operation.chainAsset, AssetIconMode.WHITE) ) } @@ -290,7 +294,7 @@ fun mapOperationToOperationModel( statusAppearance = statusAppearance, subHeader = operationType.formatSubHeader(resourceManager), subHeaderEllipsize = TextUtils.TruncateAt.END, - operationIcon = R.drawable.ic_flip_swap.asIcon() + operationIcon = R.drawable.ic_swap_history.asIcon() ) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/history/mixin/TransactionHistoryProvider.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/history/mixin/TransactionHistoryProvider.kt index 0e2d560ddd..2aabcd9418 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/history/mixin/TransactionHistoryProvider.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/transaction/history/mixin/TransactionHistoryProvider.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_assets.presentation.transaction.history.mixin import android.util.Log +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.daysFromMillis @@ -50,7 +51,8 @@ class TransactionHistoryProvider( private val chainRegistry: ChainRegistry, private val chainId: ChainId, private val assetId: Int, - private val currencyRepository: CurrencyRepository + private val currencyRepository: CurrencyRepository, + private val assetIconProvider: AssetIconProvider ) : TransactionHistoryMixin, CoroutineScope by CoroutineScope(Dispatchers.Default) { private val domainState = singleReplaySharedFlow() @@ -237,7 +239,7 @@ class TransactionHistoryProvider( val header = DayHeader(daysSinceEpoch) val operationModels = operationsPerDay.map { operation -> - mapOperationToOperationModel(chain, token, operation, accountIdentifier, resourceManager) + mapOperationToOperationModel(chain, token, operation, accountIdentifier, resourceManager, assetIconProvider) } listOf(header) + operationModels diff --git a/feature-assets/src/main/res/layout/fragment_asset_filters.xml b/feature-assets/src/main/res/layout/fragment_asset_filters.xml index c4479e02e0..125490b1f0 100644 --- a/feature-assets/src/main/res/layout/fragment_asset_filters.xml +++ b/feature-assets/src/main/res/layout/fragment_asset_filters.xml @@ -1,6 +1,5 @@ + tools:listitem="@layout/item_network_asset" /> + android:orientation="vertical"> + android:hint="@string/assets_search_hint" /> + tools:listitem="@layout/item_network_asset" /> + android:visibility="gone" /> \ No newline at end of file diff --git a/feature-assets/src/main/res/layout/fragment_balance_detail.xml b/feature-assets/src/main/res/layout/fragment_balance_detail.xml index 83c3fd3dc5..420e7a9470 100644 --- a/feature-assets/src/main/res/layout/fragment_balance_detail.xml +++ b/feature-assets/src/main/res/layout/fragment_balance_detail.xml @@ -53,8 +53,7 @@ app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/balanceDetailTokenName" - app:tint="@color/icon_primary" /> + app:layout_constraintTop_toTopOf="@+id/balanceDetailTokenName" /> + tools:text="$10.25" /> + tools:listitem="@layout/item_network_asset" /> diff --git a/feature-assets/src/main/res/layout/fragment_extrinsic_details.xml b/feature-assets/src/main/res/layout/fragment_extrinsic_details.xml index 4b098985a3..23d81a5744 100644 --- a/feature-assets/src/main/res/layout/fragment_extrinsic_details.xml +++ b/feature-assets/src/main/res/layout/fragment_extrinsic_details.xml @@ -35,7 +35,6 @@ android:layout_height="64dp" android:layout_marginTop="16dp" android:background="@drawable/bg_icon_container_on_color" - android:padding="11dp" app:tint="@color/text_secondary" /> - - - + android:background="@color/solid_navigation_background" + android:orientation="vertical" + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + @@ -46,14 +71,13 @@ android:id="@+id/manageTokensList" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_marginTop="8dp" android:layout_weight="1" android:clipToPadding="false" android:paddingTop="8dp" android:paddingBottom="16dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toBottomOf="@id/manageTokensSearch" + app:layout_constraintTop_toBottomOf="@id/manageTokensToolbarContainer" tools:listitem="@layout/item_manage_token_multichain" /> \ No newline at end of file diff --git a/feature-assets/src/main/res/layout/fragment_network_flow.xml b/feature-assets/src/main/res/layout/fragment_network_flow.xml new file mode 100644 index 0000000000..854f50df70 --- /dev/null +++ b/feature-assets/src/main/res/layout/fragment_network_flow.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/feature-assets/src/main/res/layout/fragment_pool_reward_details.xml b/feature-assets/src/main/res/layout/fragment_pool_reward_details.xml index 07df4d5992..4614f3f7e6 100644 --- a/feature-assets/src/main/res/layout/fragment_pool_reward_details.xml +++ b/feature-assets/src/main/res/layout/fragment_pool_reward_details.xml @@ -34,8 +34,7 @@ android:layout_height="64dp" android:layout_marginTop="16dp" android:background="@drawable/bg_icon_container_on_color" - android:padding="11dp" - android:src="@drawable/ic_staking_filled" + android:src="@drawable/ic_staking_history" app:tint="@color/icon_secondary" /> - + android:layout_height="match_parent" + android:orientation="vertical"> - + android:paddingTop="4dp" + android:paddingBottom="4dp"> - + android:layout_marginStart="8dp" + android:padding="8dp" + android:src="@drawable/ic_arrow_back" + app:tint="@color/icon_primary" /> - + android:layout_gravity="center" /> + + + + + + + + + + + android:layout_marginTop="8dp" + android:ellipsize="end" + android:gravity="center_horizontal" + android:maxLines="1" + android:paddingHorizontal="16dp" + android:textColor="@color/text_primary_on_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/receiveQrCode" + tools:text="🌌 NOVA" /> - + android:layout_gravity="center_horizontal" + android:layout_marginHorizontal="16dp" + android:layout_marginTop="8dp" + android:gravity="center_horizontal" + android:paddingHorizontal="16dp" + android:textColor="@color/text_secondary_on_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/receiveAccount" + tools:text="Day71BAT8tLr1u43yUV2pKDZE6BvFWrXqFNzMdSHz6iRMAZxKuSr" /> - - - + + + + + + + + diff --git a/feature-assets/src/main/res/layout/fragment_reward_slash_details.xml b/feature-assets/src/main/res/layout/fragment_reward_slash_details.xml index c885606b8f..0909a7a1cc 100644 --- a/feature-assets/src/main/res/layout/fragment_reward_slash_details.xml +++ b/feature-assets/src/main/res/layout/fragment_reward_slash_details.xml @@ -34,8 +34,7 @@ android:layout_height="64dp" android:layout_marginTop="16dp" android:background="@drawable/bg_icon_container_on_color" - android:padding="11dp" - android:src="@drawable/ic_staking_filled" + android:src="@drawable/ic_staking_history" app:tint="@color/icon_secondary" /> + tools:src="@drawable/ic_send_history" /> - - - + app:layout_constraintTop_toTopOf="@+id/balanceListManage" + app:tint="@color/chip_icon" /> + tools:src="@drawable/ic_token_dot_colored" /> + tools:src="@drawable/ic_token_dot_colored" /> + tools:text="DOT" /> + + + + + + + + + + + diff --git a/feature-assets/src/main/res/layout/item_token_asset.xml b/feature-assets/src/main/res/layout/item_token_asset.xml new file mode 100644 index 0000000000..490efab6f8 --- /dev/null +++ b/feature-assets/src/main/res/layout/item_token_asset.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/feature-assets/src/main/res/layout/item_token_asset_group.xml b/feature-assets/src/main/res/layout/item_token_asset_group.xml new file mode 100644 index 0000000000..ee0c824cd0 --- /dev/null +++ b/feature-assets/src/main/res/layout/item_token_asset_group.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + diff --git a/feature-assets/src/main/res/layout/view_asset_view_mode.xml b/feature-assets/src/main/res/layout/view_asset_view_mode.xml new file mode 100644 index 0000000000..fb9fe92c9e --- /dev/null +++ b/feature-assets/src/main/res/layout/view_asset_view_mode.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/feature-assets/src/main/res/values/dimens.xml b/feature-assets/src/main/res/values/dimens.xml index 29078c48e4..a30e9e8e00 100644 --- a/feature-assets/src/main/res/values/dimens.xml +++ b/feature-assets/src/main/res/values/dimens.xml @@ -3,4 +3,5 @@ 64dp 80dp 24sp + 15sp \ No newline at end of file diff --git a/feature-assets/src/main/res/values/styles.xml b/feature-assets/src/main/res/values/styles.xml index 1dc1bbfc82..d1fa4d1677 100644 --- a/feature-assets/src/main/res/values/styles.xml +++ b/feature-assets/src/main/res/values/styles.xml @@ -13,7 +13,6 @@ diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/CrowdloanFeatureDependencies.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/CrowdloanFeatureDependencies.kt index 400677e556..d750c8a7fa 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/CrowdloanFeatureDependencies.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/CrowdloanFeatureDependencies.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.common.data.network.NetworkApiCreator import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.core.storage.StorageCache @@ -36,6 +37,16 @@ import javax.inject.Named interface CrowdloanFeatureDependencies { + val parachainInfoRepository: ParachainInfoRepository + + val signerProvider: SignerProvider + + val storageStorageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory + + val externalBalanceDao: ExternalBalanceDao + + val assetIconProvider: AssetIconProvider + fun contributionDao(): ContributionDao fun accountUpdaterScope(): AccountUpdateScope @@ -91,12 +102,4 @@ interface CrowdloanFeatureDependencies { fun customDialogDisplayer(): CustomDialogDisplayer.Presentation fun feeLoaderMixinFactory(): FeeLoaderMixin.Factory - - val parachainInfoRepository: ParachainInfoRepository - - val signerProvider: SignerProvider - - val storageStorageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory - - val externalBalanceDao: ExternalBalanceDao } diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeFragment.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeFragment.kt index 52d0fc69f0..a782831241 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeFragment.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeFragment.kt @@ -89,7 +89,7 @@ class ConfirmContributeFragment : BaseFragment() { viewModel.assetModelFlow.observe { confirmContributeAmount.setAssetBalance(it.assetBalance) confirmContributeAmount.setAssetName(it.tokenSymbol) - confirmContributeAmount.loadAssetImage(it.imageUrl) + confirmContributeAmount.loadAssetImage(it.icon) } confirmContributeAmount.amountInput.setText(viewModel.selectedAmount) diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt index c580567744..13d0fdd50c 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.api.Validatable +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.flowOf @@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class ConfirmContributeViewModel( + private val assetIconProvider: AssetIconProvider, private val router: CrowdloanRouter, private val contributionInteractor: CrowdloanContributeInteractor, private val resourceManager: ResourceManager, @@ -72,7 +74,7 @@ class ConfirmContributeViewModel( .share() val assetModelFlow = assetFlow - .map { mapAssetToAssetModel(it, resourceManager) } + .map { mapAssetToAssetModel(assetIconProvider, it, resourceManager) } .inBackground() .share() diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/di/ConfirmContributeModule.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/di/ConfirmContributeModule.kt index 8dc0a3d630..d309b4d198 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/di/ConfirmContributeModule.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/di/ConfirmContributeModule.kt @@ -9,6 +9,7 @@ import dagger.multibindings.IntoMap import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase @@ -30,6 +31,7 @@ class ConfirmContributeModule { @IntoMap @ViewModelKey(ConfirmContributeViewModel::class) fun provideViewModel( + assetIconProvider: AssetIconProvider, interactor: CrowdloanContributeInteractor, router: CrowdloanRouter, resourceManager: ResourceManager, @@ -44,6 +46,7 @@ class ConfirmContributeModule { singleAssetSharedState: CrowdloanSharedState, ): ViewModel { return ConfirmContributeViewModel( + assetIconProvider, router, interactor, resourceManager, diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeFragment.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeFragment.kt index 3cb84a9615..54e6b2c4dc 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeFragment.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeFragment.kt @@ -96,7 +96,7 @@ class CrowdloanContributeFragment : BaseFragment() viewModel.assetModelFlow.observe { crowdloanContributeAmount.setAssetBalance(it.assetBalance) crowdloanContributeAmount.setAssetName(it.tokenSymbol) - crowdloanContributeAmount.loadAssetImage(it.imageUrl) + crowdloanContributeAmount.loadAssetImage(it.icon) } crowdloanContributeAmount.amountInput.bindTo(viewModel.enteredAmountFlow, lifecycleScope) diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt index 89d25edf56..e26f2a889b 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.api.Browserable import io.novafoundation.nova.common.mixin.api.Validatable import io.novafoundation.nova.common.mixin.api.of +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.flowOf @@ -71,6 +72,7 @@ sealed class ExtraBonusState { } class CrowdloanContributeViewModel( + private val assetIconProvider: AssetIconProvider, private val router: CrowdloanRouter, private val contributionInteractor: CrowdloanContributeInteractor, private val resourceManager: ResourceManager, @@ -97,7 +99,7 @@ class CrowdloanContributeViewModel( .share() val assetModelFlow = assetFlow - .map { mapAssetToAssetModel(it, resourceManager) } + .map { mapAssetToAssetModel(assetIconProvider, it, resourceManager) } .inBackground() .share() diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/di/CrowdloanContributeModule.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/di/CrowdloanContributeModule.kt index ef392ef335..19d820cce0 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/di/CrowdloanContributeModule.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/di/CrowdloanContributeModule.kt @@ -8,6 +8,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager @@ -27,6 +28,7 @@ class CrowdloanContributeModule { @IntoMap @ViewModelKey(CrowdloanContributeViewModel::class) fun provideViewModel( + assetIconProvider: AssetIconProvider, interactor: CrowdloanContributeInteractor, router: CrowdloanRouter, resourceManager: ResourceManager, @@ -38,6 +40,7 @@ class CrowdloanContributeModule { customContributeManager: CustomContributeManager, ): ViewModel { return CrowdloanContributeViewModel( + assetIconProvider, router, interactor, resourceManager, diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt index fd8cea9162..dca0ce27dc 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt @@ -311,7 +311,7 @@ class EvmSignInteractor( val chainCurrency = evmChain.nativeCurrency return Chain.Asset( - iconUrl = evmChain.iconUrl, + icon = null, id = 0, priceId = null, chainId = evmChain.chainId, diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/di/GovernanceFeatureDependencies.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/di/GovernanceFeatureDependencies.kt index ab6fe593b2..3fc3d9edd1 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/di/GovernanceFeatureDependencies.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/di/GovernanceFeatureDependencies.kt @@ -11,6 +11,7 @@ import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.di.modules.Caching import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.mixin.hints.ResourcesHintsMixinFactory +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.multiResult.PartialRetriableMixin import io.novafoundation.nova.common.validation.ValidationExecutor @@ -48,6 +49,24 @@ import javax.inject.Named interface GovernanceFeatureDependencies { + val onChainIdentityRepository: OnChainIdentityRepository + + val listChooserMixinFactory: ListChooserMixin.Factory + + val identityMixinFactory: IdentityMixin.Factory + + val partialRetriableMixinFactory: PartialRetriableMixin.Factory + + val storageStorageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory + + val bannerVisibilityRepository: BannerVisibilityRepository + + val chainMultiLocationConverterFactory: ChainMultiLocationConverterFactory + + val assetMultiLocationConverterFactory: MultiLocationConverterFactory + + val assetIconProvider: AssetIconProvider + val feeLoaderMixinFactory: FeeLoaderMixin.Factory val validationExecutor: ValidationExecutor @@ -118,20 +137,4 @@ interface GovernanceFeatureDependencies { @Named(REMOTE_STORAGE_SOURCE) fun remoteStorageDataSource(): StorageDataSource - - val onChainIdentityRepository: OnChainIdentityRepository - - val listChooserMixinFactory: ListChooserMixin.Factory - - val identityMixinFactory: IdentityMixin.Factory - - val partialRetriableMixinFactory: PartialRetriableMixin.Factory - - val storageStorageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory - - val bannerVisibilityRepository: BannerVisibilityRepository - - val chainMultiLocationConverterFactory: ChainMultiLocationConverterFactory - - val assetMultiLocationConverterFactory: MultiLocationConverterFactory } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/di/GovernanceFeatureModule.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/di/GovernanceFeatureModule.kt index 6b5a05461c..01af05c6f5 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/di/GovernanceFeatureModule.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/di/GovernanceFeatureModule.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider @@ -156,8 +157,9 @@ class GovernanceFeatureModule { @FeatureScope fun provideTracksFormatter( trackCategorizer: TrackCategorizer, - resourceManager: ResourceManager - ): TrackFormatter = RealTrackFormatter(trackCategorizer, resourceManager) + resourceManager: ResourceManager, + assetIconProvider: AssetIconProvider + ): TrackFormatter = RealTrackFormatter(trackCategorizer, resourceManager, assetIconProvider) @Provides @FeatureScope diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/model/AmountChangeModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/model/AmountChangeModel.kt index 0745cea501..fc27f44c15 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/model/AmountChangeModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/model/AmountChangeModel.kt @@ -6,22 +6,22 @@ import io.novafoundation.nova.feature_governance_impl.R import io.novafoundation.nova.feature_governance_impl.presentation.referenda.vote.setup.common.model.AmountChangeModel.DifferenceModel class AmountChangeModel( - val to: String, - val from: String?, + val to: CharSequence, + val from: CharSequence?, val difference: DifferenceModel? ) { class DifferenceModel( @DrawableRes val icon: Int, - val text: String, + val text: CharSequence, @ColorRes val color: Int ) } fun AmountChangeModel( - from: String?, - to: String, - difference: String?, + from: CharSequence?, + to: CharSequence, + difference: CharSequence?, positive: Boolean? ): AmountChangeModel { val differenceModel = if (positive != null && difference != null) { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/view/AmountChangesView.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/view/AmountChangesView.kt index 6a99b7ec16..5dccda2de6 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/view/AmountChangesView.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/view/AmountChangesView.kt @@ -39,7 +39,7 @@ class AmountChangesView @JvmOverloads constructor( attrs?.let { applyAttributes(it) } } - fun setFrom(value: String?) { + fun setFrom(value: CharSequence?) { if (value != null) { valueChangesFrom.text = value valueChangesFromGroup.makeVisible() @@ -48,11 +48,11 @@ class AmountChangesView @JvmOverloads constructor( } } - fun setTo(value: String) { + fun setTo(value: CharSequence) { valueChangesTo.text = value } - fun setDifference(@DrawableRes icon: Int, text: String, @ColorRes textColor: Int) { + fun setDifference(@DrawableRes icon: Int, text: CharSequence, @ColorRes textColor: Int) { valueChangesDifference.makeVisible() valueChangesDifference.setDrawableStart(icon, widthInDp = 16, tint = textColor) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/track/TrackFormatter.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/track/TrackFormatter.kt index 9cecfa905e..36a475bdb9 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/track/TrackFormatter.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/track/TrackFormatter.kt @@ -1,9 +1,13 @@ package io.novafoundation.nova.feature_governance_impl.presentation.track +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.formatListPreview import io.novafoundation.nova.common.utils.capitalize import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.common.utils.images.asIcon +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback import io.novafoundation.nova.feature_governance_api.domain.referendum.track.category.TrackType import io.novafoundation.nova.feature_governance_api.domain.track.Track import io.novafoundation.nova.feature_governance_impl.R @@ -27,70 +31,86 @@ fun TrackFormatter.formatTracks(tracks: List, asset: Chain.Asset): Tracks class RealTrackFormatter( private val trackCategorizer: TrackCategorizer, private val resourceManager: ResourceManager, + private val assetIconProvider: AssetIconProvider ) : TrackFormatter { override fun formatTrack(track: Track, asset: Chain.Asset): TrackModel { return when (trackCategorizer.typeOf(track.name)) { TrackType.ROOT -> TrackModel( name = resourceManager.getString(R.string.referendum_track_root), - icon = asset.iconUrl?.let { Icon.FromLink(it) } ?: Icon.FromDrawableRes(R.drawable.ic_block), + icon = assetIconProvider.getAssetIconOrFallback(asset, iconMode = AssetIconMode.WHITE, fallbackIcon = R.drawable.ic_block.asIcon()), ) + TrackType.WHITELISTED_CALLER -> TrackModel( name = resourceManager.getString(R.string.referendum_whitelisted_caller), icon = Icon.FromDrawableRes(R.drawable.ic_users), ) + TrackType.STAKING_ADMIN -> TrackModel( name = resourceManager.getString(R.string.referendum_staking_admin), icon = Icon.FromDrawableRes(R.drawable.ic_staking_filled), ) + TrackType.TREASURER -> TrackModel( name = resourceManager.getString(R.string.referendum_track_treasurer), icon = Icon.FromDrawableRes(R.drawable.ic_gem), ) + TrackType.LEASE_ADMIN -> TrackModel( name = resourceManager.getString(R.string.referendum_track_lease_admin), icon = Icon.FromDrawableRes(R.drawable.ic_governance_check_to_slot), ) + TrackType.FELLOWSHIP_ADMIN -> TrackModel( name = resourceManager.getString(R.string.referendum_track_fellowship_admin), icon = Icon.FromDrawableRes(R.drawable.ic_users), ) + TrackType.GENERAL_ADMIN -> TrackModel( name = resourceManager.getString(R.string.referendum_track_general_admin), icon = Icon.FromDrawableRes(R.drawable.ic_governance_check_to_slot), ) + TrackType.AUCTION_ADMIN -> TrackModel( name = resourceManager.getString(R.string.referendum_track_auction_admin), icon = Icon.FromDrawableRes(R.drawable.ic_rocket), ) + TrackType.REFERENDUM_CANCELLER -> TrackModel( name = resourceManager.getString(R.string.referendum_track_referendum_canceller), icon = Icon.FromDrawableRes(R.drawable.ic_governance_check_to_slot), ) + TrackType.REFERENDUM_KILLER -> TrackModel( name = resourceManager.getString(R.string.referendum_track_referendum_killer), icon = Icon.FromDrawableRes(R.drawable.ic_governance_check_to_slot), ) + TrackType.SMALL_TIPPER -> TrackModel( name = resourceManager.getString(R.string.referendum_track_small_tipper), icon = Icon.FromDrawableRes(R.drawable.ic_gem), ) + TrackType.BIG_TIPPER -> TrackModel( name = resourceManager.getString(R.string.referendum_track_big_tipper), icon = Icon.FromDrawableRes(R.drawable.ic_gem), ) + TrackType.SMALL_SPEND -> TrackModel( name = resourceManager.getString(R.string.referendum_track_small_spender), icon = Icon.FromDrawableRes(R.drawable.ic_gem), ) + TrackType.MEDIUM_SPEND -> TrackModel( name = resourceManager.getString(R.string.referendum_track_medium_spender), icon = Icon.FromDrawableRes(R.drawable.ic_gem), ) + TrackType.BIG_SPEND -> TrackModel( name = resourceManager.getString(R.string.referendum_track_big_spender), icon = Icon.FromDrawableRes(R.drawable.ic_gem), ) + TrackType.OTHER -> TrackModel( name = mapUnknownTrackNameToUi(track.name), icon = Icon.FromDrawableRes(R.drawable.ic_block), diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/list/model/GovernanceLockModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/list/model/GovernanceLockModel.kt index d0da0474e7..9425f55b96 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/list/model/GovernanceLockModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/list/model/GovernanceLockModel.kt @@ -6,7 +6,7 @@ import io.novafoundation.nova.common.utils.formatting.TimerValue data class GovernanceLockModel( val index: Int, - val amount: String, + val amount: CharSequence, val status: StatusContent, @ColorRes val statusColorRes: Int, @DrawableRes val statusIconRes: Int?, diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/view/GovernanceLocksView.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/view/GovernanceLocksView.kt index 1738324252..e92a40239d 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/view/GovernanceLocksView.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/view/GovernanceLocksView.kt @@ -53,7 +53,7 @@ class GovernanceLocksView @JvmOverloads constructor( } class GovernanceLocksModel( - val amount: String?, + val amount: CharSequence?, val title: String, val hasUnlockableLocks: Boolean ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/view/NovaChipView.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/view/NovaChipView.kt index 6a9335fa34..fdc2a256af 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/view/NovaChipView.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/view/NovaChipView.kt @@ -221,7 +221,7 @@ class NovaChipView @JvmOverloads constructor( setIconTint(textColorRes) } - fun setText(text: String?) { + fun setText(text: CharSequence?) { chipText.setTextOrHide(text) invalidateDrawablePadding() } @@ -254,4 +254,4 @@ class NovaChipView @JvmOverloads constructor( } } -fun NovaChipView.setTextOrHide(text: String?) = letOrHide(text, ::setText) +fun NovaChipView.setTextOrHide(text: CharSequence?) = letOrHide(text, ::setText) diff --git a/feature-governance-impl/src/main/res/layout/item_tinder_gov_card.xml b/feature-governance-impl/src/main/res/layout/item_tinder_gov_card.xml index 886ed927cd..4c9b602f81 100644 --- a/feature-governance-impl/src/main/res/layout/item_tinder_gov_card.xml +++ b/feature-governance-impl/src/main/res/layout/item_tinder_gov_card.xml @@ -84,7 +84,7 @@ + + fun setIconMode(iconMode: AssetIconMode) +} + +class RealAppearanceInteractor( + private val assetsIconModeRepository: AssetsIconModeRepository +) : AppearanceInteractor { + + override fun assetIconModeFlow() = assetsIconModeRepository.assetsIconModeFlow() + + override fun setIconMode(iconMode: AssetIconMode) { + assetsIconModeRepository.setAssetsIconMode(iconMode) + } +} diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/utils/CustomChainFactory.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/utils/CustomChainFactory.kt index 2a6ec8f606..f63fb16290 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/utils/CustomChainFactory.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/utils/CustomChainFactory.kt @@ -91,7 +91,7 @@ class CustomChainFactory( id = 0, name = payload.chainName, enabled = true, - iconUrl = prefilledUtilityAsset?.iconUrl, + icon = prefilledUtilityAsset?.icon, priceId = priceId, chainId = chainId, symbol = payload.tokenSymbol.asTokenSymbol(), diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/AppearanceFragment.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/AppearanceFragment.kt new file mode 100644 index 0000000000..c1312c3918 --- /dev/null +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/AppearanceFragment.kt @@ -0,0 +1,61 @@ +package io.novafoundation.nova.feature_settings_impl.presentation.assetIcons + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import io.novafoundation.nova.common.base.BaseFragment +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.applyStatusBarInsets +import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi +import io.novafoundation.nova.feature_settings_impl.R +import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent +import kotlinx.android.synthetic.main.fragment_appearance.appearanceColoredButton +import kotlinx.android.synthetic.main.fragment_appearance.appearanceColoredIcon +import kotlinx.android.synthetic.main.fragment_appearance.appearanceColoredText +import kotlinx.android.synthetic.main.fragment_appearance.appearanceToolbar +import kotlinx.android.synthetic.main.fragment_appearance.appearanceWhiteButton +import kotlinx.android.synthetic.main.fragment_appearance.appearanceWhiteIcon +import kotlinx.android.synthetic.main.fragment_appearance.appearanceWhiteText + +class AppearanceFragment : BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + return inflater.inflate(R.layout.fragment_appearance, container, false) + } + + override fun initViews() { + appearanceToolbar.applyStatusBarInsets() + appearanceToolbar.setHomeButtonListener { viewModel.backClicked() } + + appearanceWhiteButton.setOnClickListener { viewModel.selectWhiteIcon() } + appearanceColoredButton.setOnClickListener { viewModel.selectColoredIcon() } + + appearanceWhiteButton.background = getRippleDrawable(cornerSizeInDp = 10) + appearanceColoredButton.background = getRippleDrawable(cornerSizeInDp = 10) + } + + override fun inject() { + FeatureUtils.getFeature( + requireContext(), + SettingsFeatureApi::class.java + ) + .appearanceFactory() + .create(this) + .inject(this) + } + + override fun subscribe(viewModel: AppearanceViewModel) { + viewModel.assetIconsStateFlow.observe { + appearanceWhiteIcon.isSelected = it.whiteActive + appearanceWhiteText.isSelected = it.whiteActive + + appearanceColoredIcon.isSelected = it.coloredActive + appearanceColoredText.isSelected = it.coloredActive + } + } +} diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/AppearanceViewModel.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/AppearanceViewModel.kt new file mode 100644 index 0000000000..9f2c39e946 --- /dev/null +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/AppearanceViewModel.kt @@ -0,0 +1,40 @@ +package io.novafoundation.nova.feature_settings_impl.presentation.assetIcons + +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.data.model.AssetIconMode +import io.novafoundation.nova.feature_settings_impl.SettingsRouter +import io.novafoundation.nova.feature_settings_impl.domain.AppearanceInteractor +import kotlinx.coroutines.flow.map + +class AssetIconsStateModel( + val whiteActive: Boolean, + val coloredActive: Boolean +) + +class AppearanceViewModel( + private val interactor: AppearanceInteractor, + private val router: SettingsRouter +) : BaseViewModel() { + + val assetIconsStateFlow = interactor.assetIconModeFlow() + .map { + AssetIconsStateModel( + whiteActive = it == AssetIconMode.WHITE, + coloredActive = it == AssetIconMode.COLORED + ) + } + + fun selectWhiteIcon() { + interactor.setIconMode(AssetIconMode.WHITE) + router.returnToWallet() + } + + fun selectColoredIcon() { + interactor.setIconMode(AssetIconMode.COLORED) + router.returnToWallet() + } + + fun backClicked() { + router.back() + } +} diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/di/AppearanceComponent.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/di/AppearanceComponent.kt new file mode 100644 index 0000000000..643bca793f --- /dev/null +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/di/AppearanceComponent.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.AppearanceFragment + +@Subcomponent( + modules = [ + AppearanceModule::class + ] +) +@ScreenScope +interface AppearanceComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment + ): AppearanceComponent + } + + fun inject(fragment: AppearanceFragment) +} diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/di/AppearanceModule.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/di/AppearanceModule.kt new file mode 100644 index 0000000000..09f54feda4 --- /dev/null +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/assetIcons/di/AppearanceModule.kt @@ -0,0 +1,35 @@ +package io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.feature_settings_impl.SettingsRouter +import io.novafoundation.nova.feature_settings_impl.domain.AppearanceInteractor +import io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.AppearanceViewModel + +@Module(includes = [ViewModelModule::class]) +class AppearanceModule { + + @Provides + @IntoMap + @ViewModelKey(AppearanceViewModel::class) + fun provideViewModel( + interactor: AppearanceInteractor, + router: SettingsRouter + ): ViewModel { + return AppearanceViewModel( + interactor, + router + ) + } + + @Provides + fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AppearanceViewModel { + return ViewModelProvider(fragment, viewModelFactory).get(AppearanceViewModel::class.java) + } +} diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsFragment.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsFragment.kt index a1d8dac621..d0be400b66 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsFragment.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsFragment.kt @@ -19,6 +19,7 @@ import io.novafoundation.nova.feature_settings_impl.R import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent import kotlinx.android.synthetic.main.fragment_settings.accountView import kotlinx.android.synthetic.main.fragment_settings.settingsAppVersion +import kotlinx.android.synthetic.main.fragment_settings.settingsAppearance import kotlinx.android.synthetic.main.fragment_settings.settingsAvatar import kotlinx.android.synthetic.main.fragment_settings.settingsBiometricAuth import kotlinx.android.synthetic.main.fragment_settings.settingsCloudBackup @@ -63,6 +64,7 @@ class SettingsFragment : BaseFragment() { settingsPushNotifications.setOnClickListener { viewModel.pushNotificationsClicked() } settingsCurrency.setOnClickListener { viewModel.currenciesClicked() } settingsLanguage.setOnClickListener { viewModel.languagesClicked() } + settingsAppearance.setOnClickListener { viewModel.appearanceClicked() } settingsTelegram.setOnClickListener { viewModel.telegramClicked() } settingsTwitter.setOnClickListener { viewModel.twitterClicked() } diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsViewModel.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsViewModel.kt index 687b88dc25..ef9ac5f90d 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsViewModel.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsViewModel.kt @@ -144,6 +144,10 @@ class SettingsViewModel( router.openLanguages() } + fun appearanceClicked() { + router.openAppearance() + } + fun changeBiometricAuth() { launch { if (biometricService.isEnabled()) { diff --git a/feature-settings-impl/src/main/res/layout/fragment_appearance.xml b/feature-settings-impl/src/main/res/layout/fragment_appearance.xml new file mode 100644 index 0000000000..79ff49bfd3 --- /dev/null +++ b/feature-settings-impl/src/main/res/layout/fragment_appearance.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feature-settings-impl/src/main/res/layout/fragment_settings.xml b/feature-settings-impl/src/main/res/layout/fragment_settings.xml index 110b3b98b1..86dd39fc8e 100644 --- a/feature-settings-impl/src/main/res/layout/fragment_settings.xml +++ b/feature-settings-impl/src/main/res/layout/fragment_settings.xml @@ -110,6 +110,13 @@ app:title="@string/language_title" tools:settingValue="English" /> + + ): NetworkInfoItem { diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/view/SwapAssetView.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/view/SwapAssetView.kt index adfe97ee92..5bdeb3da91 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/view/SwapAssetView.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/view/SwapAssetView.kt @@ -8,13 +8,12 @@ import coil.ImageLoader import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.WithContextExtensions import io.novafoundation.nova.common.utils.images.Icon -import io.novafoundation.nova.common.utils.images.setIcon -import io.novafoundation.nova.common.utils.setImageTint import io.novafoundation.nova.common.utils.setTextColorRes import io.novafoundation.nova.common.utils.setTextOrHide import io.novafoundation.nova.common.view.shape.getInputBackground import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon +import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon import io.novafoundation.nova.feature_swap_api.R import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import kotlinx.android.synthetic.main.view_swap_asset.view.swapAssetAmount @@ -49,8 +48,7 @@ class SwapAssetView @JvmOverloads constructor( } private fun setAssetImageUrl(icon: Icon) { - swapAssetImage.setImageTint(context.getColor(R.color.icon_primary)) - swapAssetImage.setIcon(icon, imageLoader) + swapAssetImage.setTokenIcon(icon, imageLoader) swapAssetImage.setBackgroundResource(R.drawable.bg_token_container) } diff --git a/feature-swap-api/src/main/res/layout/view_swap_asset.xml b/feature-swap-api/src/main/res/layout/view_swap_asset.xml index ab2bbf7b30..854c4cf9ef 100644 --- a/feature-swap-api/src/main/res/layout/view_swap_asset.xml +++ b/feature-swap-api/src/main/res/layout/view_swap_asset.xml @@ -13,12 +13,10 @@ android:layout_height="40dp" android:layout_marginTop="16dp" android:background="@drawable/bg_token_container" - android:padding="5dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:tint="@color/icon_primary" - tools:src="@drawable/ic_token_ksm" /> + tools:src="@drawable/ic_token_dot_colored" /> { - swapAmountInputImage.setImageTint(context.getColor(R.color.icon_primary)) - swapAmountInputImage.loadTokenIcon(icon.assetIconUrl, imageLoader) + swapAmountInputImage.setImageTint(null) + swapAmountInputImage.setTokenIcon(icon.assetIcon, imageLoader) swapAmountInputImage.setBackgroundResource(R.drawable.bg_token_container) } diff --git a/feature-swap-impl/src/main/res/layout/view_swap_amount_input.xml b/feature-swap-impl/src/main/res/layout/view_swap_amount_input.xml index 0f70968c2c..160199a5fc 100644 --- a/feature-swap-impl/src/main/res/layout/view_swap_amount_input.xml +++ b/feature-swap-impl/src/main/res/layout/view_swap_amount_input.xml @@ -19,13 +19,12 @@ android:layout_marginStart="12dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" + android:scaleType="centerInside" android:background="@drawable/bg_token_container" - android:padding="5dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:tint="@color/icon_primary" - tools:src="@drawable/ic_token_ksm" /> + tools:src="@drawable/ic_token_dot_colored" /> diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Asset.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Asset.kt index 1bf7065461..481432c072 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Asset.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Asset.kt @@ -1,7 +1,10 @@ package io.novafoundation.nova.feature_wallet_api.data.mappers import androidx.annotation.StringRes +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount @@ -9,8 +12,10 @@ import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetModel import java.math.BigDecimal fun mapAssetToAssetModel( + assetIconProvider: AssetIconProvider, asset: Asset, resourceManager: ResourceManager, + icon: Icon = assetIconProvider.getAssetIconOrFallback(asset.token.configuration), retrieveAmount: (Asset) -> BigDecimal = Asset::transferable, @StringRes patternId: Int? = R.string.common_available_format ): AssetModel { @@ -21,7 +26,7 @@ fun mapAssetToAssetModel( AssetModel( chainId = asset.token.configuration.chainId, chainAssetId = asset.token.configuration.id, - imageUrl = token.configuration.iconUrl, + icon = icon, tokenName = token.configuration.name, tokenSymbol = token.configuration.symbol.value, assetBalance = formattedAmount diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/common/SelectableAssetUseCaseModule.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/common/SelectableAssetUseCaseModule.kt index 57c99077fe..ed7e85c500 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/common/SelectableAssetUseCaseModule.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/common/SelectableAssetUseCaseModule.kt @@ -4,6 +4,7 @@ import dagger.Binds import dagger.Module import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase @@ -32,10 +33,12 @@ class SelectableAssetUseCaseModule { @Provides @FeatureScope fun provideAssetSelectorMixinFactory( + assetIconProvider: AssetIconProvider, assetUseCase: SelectableAssetUseCase<*>, singleAssetSharedState: SelectableSingleAssetSharedState<*>, resourceManager: ResourceManager ) = AssetSelectorFactory( + assetIconProvider, assetUseCase, singleAssetSharedState, resourceManager diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/AmountChooserProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/AmountChooserProvider.kt index 764a3c289e..67fc2b642c 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/AmountChooserProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/AmountChooserProvider.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser import androidx.annotation.StringRes +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount @@ -15,7 +16,8 @@ import java.math.BigDecimal import java.math.BigInteger class AmountChooserProviderFactory( - private val resourceManager: ResourceManager + private val resourceManager: ResourceManager, + private val assetIconProvider: AssetIconProvider, ) : AmountChooserMixin.Factory { override fun create( @@ -29,6 +31,7 @@ class AmountChooserProviderFactory( usedAssetFlow = assetFlow, balanceLabel = balanceLabel, resourceManager = resourceManager, + assetIconProvider = assetIconProvider, maxActionProvider = SimpleMaxActionProvider( maxAvailableForDisplay = availableBalanceFlow, // TODO amount chooser max button @@ -56,6 +59,7 @@ class AmountChooserProvider( coroutineScope: CoroutineScope, override val usedAssetFlow: Flow, private val resourceManager: ResourceManager, + private val assetIconProvider: AssetIconProvider, @StringRes private val balanceLabel: Int?, maxActionProvider: MaxActionProvider ) : BaseAmountChooserProvider( @@ -66,7 +70,7 @@ class AmountChooserProvider( AmountChooserMixin.Presentation { override val assetModel = usedAssetFlow.map { asset -> - ChooseAmountModel(asset, resourceManager, balanceLabel) + ChooseAmountModel(asset, assetIconProvider, resourceManager, balanceLabel) } .shareInBackground() } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/assetSelector/AssetSelectorProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/assetSelector/AssetSelectorProvider.kt index 6cf92e820d..ecc5d7bdff 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/assetSelector/AssetSelectorProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/assetSelector/AssetSelectorProvider.kt @@ -1,10 +1,12 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.assetSelector import androidx.lifecycle.MutableLiveData +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.WithCoroutineScopeExtensions import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet +import io.novafoundation.nova.feature_account_api.presenatation.chain.iconOrFallback import io.novafoundation.nova.feature_wallet_api.data.mappers.mapAssetToAssetModel import io.novafoundation.nova.feature_wallet_api.domain.SelectableAssetAndOption import io.novafoundation.nova.feature_wallet_api.domain.SelectableAssetUseCase @@ -20,6 +22,7 @@ import kotlinx.coroutines.launch import java.math.BigDecimal class AssetSelectorFactory( + private val assetIconProvider: AssetIconProvider, private val assetUseCase: SelectableAssetUseCase<*>, private val singleAssetSharedState: SingleAssetSharedState, private val resourceManager: ResourceManager @@ -29,11 +32,12 @@ class AssetSelectorFactory( scope: CoroutineScope, amountProvider: (Asset) -> BigDecimal ): AssetSelectorMixin.Presentation { - return AssetSelectorProvider(assetUseCase, resourceManager, singleAssetSharedState, scope, amountProvider) + return AssetSelectorProvider(assetIconProvider, assetUseCase, resourceManager, singleAssetSharedState, scope, amountProvider) } } private class AssetSelectorProvider( + private val assetIconProvider: AssetIconProvider, private val assetUseCase: SelectableAssetUseCase<*>, private val resourceManager: ResourceManager, private val singleAssetSharedState: SingleAssetSharedState, @@ -79,7 +83,14 @@ private class AssetSelectorProvider( } private fun mapAssetAndOptionToSelectorModel(assetAndOption: SelectableAssetAndOption): AssetSelectorModel { - val assetModel = mapAssetToAssetModel(assetAndOption.asset, resourceManager, patternId = null, retrieveAmount = amountProvider) + val assetModel = mapAssetToAssetModel( + assetIconProvider, + assetAndOption.asset, + resourceManager, + icon = assetAndOption.option.assetWithChain.chain.iconOrFallback(), + patternId = null, + retrieveAmount = amountProvider + ) val title = assetAndOption.formatTitle() return AssetSelectorModel(assetModel, title, assetAndOption.option.additional.identifier) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountFormatters.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountFormatters.kt new file mode 100644 index 0000000000..ce9a752716 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountFormatters.kt @@ -0,0 +1,45 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.model + +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.AbsoluteSizeSpan +import android.text.style.ForegroundColorSpan +import androidx.annotation.DimenRes +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.formatting.toAmountWithFraction +import io.novafoundation.nova.feature_wallet_api.R + +interface AmountFormatter { + + fun formatBalanceWithFraction(unformattedAmount: CharSequence, @DimenRes floatAmountSize: Int): CharSequence +} + +class RealAmountFormatter( + private val resourceManager: ResourceManager +) : AmountFormatter { + + override fun formatBalanceWithFraction(unformattedAmount: CharSequence, @DimenRes floatAmountSize: Int): CharSequence { + val amountWithFraction = unformattedAmount.toAmountWithFraction() + + val textColor = resourceManager.getColor(R.color.text_secondary) + val colorSpan = ForegroundColorSpan(textColor) + val sizeSpan = AbsoluteSizeSpan(resourceManager.getDimensionPixelSize(floatAmountSize)) + + return with(amountWithFraction) { + val spannableBuilder = SpannableStringBuilder() + .append(amount) + if (fraction != null) { + spannableBuilder.append(separator + fraction) + val startIndex = amount.length + val endIndex = amount.length + separator.length + fraction!!.length + spannableBuilder.setSpan(colorSpan, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannableBuilder.setSpan(sizeSpan, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + spannableBuilder + } + } +} + +fun CharSequence.formatBalanceWithFraction(formatter: AmountFormatter, @DimenRes floatAmountSize: Int): CharSequence { + return formatter.formatBalanceWithFraction(this, floatAmountSize) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountModel.kt index 6a1b87a640..3bfadfdbc9 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountModel.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.presentation.model +import androidx.annotation.DimenRes import io.novafoundation.nova.common.utils.formatting.format import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -11,9 +12,17 @@ import java.math.BigInteger import java.math.RoundingMode data class AmountModel( - val token: String, - val fiat: String? -) + val token: CharSequence, + val fiat: CharSequence? +) { + + // Override it since SpannableString is not equals by content + override fun equals(other: Any?): Boolean { + return other is AmountModel && + other.token.toString() == token.toString() && + other.fiat?.toString() == fiat?.toString() + } +} enum class AmountSign(val signSymbol: String) { NONE(""), NEGATIVE("-"), POSITIVE("+") @@ -96,3 +105,10 @@ fun Asset.transferableFormat() = transferable.formatTokenAmount(token.configurat fun Asset.transferableAmountModel() = mapAmountToAmountModel(transferable, this) fun transferableAmountModelOf(asset: Asset) = mapAmountToAmountModel(asset.transferable, asset) + +fun AmountModel.formatBalanceWithFraction(amountFormatter: AmountFormatter, @DimenRes floatAmountSize: Int): AmountModel { + return AmountModel( + token = token.formatBalanceWithFraction(amountFormatter, floatAmountSize), + fiat = fiat + ) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AssetModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AssetModel.kt index aff6430b9e..4a20b6396b 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AssetModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AssetModel.kt @@ -1,9 +1,11 @@ package io.novafoundation.nova.feature_wallet_api.presentation.model +import io.novafoundation.nova.common.utils.images.Icon + data class AssetModel( val chainId: String, val chainAssetId: Int, - val imageUrl: String?, + val icon: Icon, val tokenName: String, val tokenSymbol: String, val assetBalance: String diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/ChooseAmountModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/ChooseAmountModel.kt index 49b9b8a133..ba8a3b74f8 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/ChooseAmountModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/ChooseAmountModel.kt @@ -1,8 +1,11 @@ package io.novafoundation.nova.feature_wallet_api.presentation.model import androidx.annotation.StringRes +import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.ensureSuffix +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -13,19 +16,20 @@ class ChooseAmountModel( class ChooseAmountInputModel( val tokenSymbol: String, - val tokenIcon: String?, + val tokenIcon: Icon, ) internal fun ChooseAmountModel( asset: Asset, + assetIconProvider: AssetIconProvider, resourceManager: ResourceManager, @StringRes balanceLabelRes: Int?, ): ChooseAmountModel = ChooseAmountModel( - input = ChooseAmountInputModel(asset.token.configuration), + input = ChooseAmountInputModel(asset.token.configuration, assetIconProvider), balanceLabel = balanceLabelRes?.let(resourceManager::getString)?.ensureSuffix(":"), ) -internal fun ChooseAmountInputModel(chainAsset: Chain.Asset): ChooseAmountInputModel = ChooseAmountInputModel( +internal fun ChooseAmountInputModel(chainAsset: Chain.Asset, assetIconProvider: AssetIconProvider): ChooseAmountInputModel = ChooseAmountInputModel( tokenSymbol = chainAsset.symbol.value, - tokenIcon = chainAsset.iconUrl, + tokenIcon = assetIconProvider.getAssetIconOrFallback(chainAsset), ) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/AssetSelectorBottomSheet.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/AssetSelectorBottomSheet.kt index fb069b2dc0..13fa413f1f 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/AssetSelectorBottomSheet.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/AssetSelectorBottomSheet.kt @@ -5,9 +5,8 @@ import android.os.Bundle import android.view.View import androidx.recyclerview.widget.DiffUtil import coil.ImageLoader -import coil.load +import io.novafoundation.nova.common.utils.images.setIcon import io.novafoundation.nova.common.utils.inflateChild -import io.novafoundation.nova.common.utils.setVisible import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.ClickHandler import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.DynamicListSheetAdapter @@ -15,8 +14,8 @@ import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.HolderCreator import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.presentation.mixin.assetSelector.AssetSelectorModel import kotlinx.android.synthetic.main.item_asset_selector.view.itemAssetSelectorBalance -import kotlinx.android.synthetic.main.item_asset_selector.view.itemAssetSelectorCheckmark import kotlinx.android.synthetic.main.item_asset_selector.view.itemAssetSelectorIcon +import kotlinx.android.synthetic.main.item_asset_selector.view.itemAssetSelectorRadioButton import kotlinx.android.synthetic.main.item_asset_selector.view.itemAssetSelectorTokenName class AssetSelectorBottomSheet( @@ -34,7 +33,8 @@ class AssetSelectorBottomSheet( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setTitle(R.string.wallet_assets) + setTitle(R.string.select_network_title) + setSubtitle(null) } override fun holderCreator(): HolderCreator = { parent -> @@ -57,8 +57,8 @@ private class AssetSelectorHolder( with(itemView) { itemAssetSelectorBalance.text = item.assetModel.assetBalance itemAssetSelectorTokenName.text = item.title - itemAssetSelectorIcon.load(item.assetModel.imageUrl, imageLoader) - itemAssetSelectorCheckmark.setVisible(isSelected, falseState = View.INVISIBLE) + itemAssetSelectorIcon.setIcon(item.assetModel.icon, imageLoader) + itemAssetSelectorRadioButton.isChecked = isSelected } } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/AssetSelectorView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/AssetSelectorView.kt index ede1dd55a5..6fc4786e5f 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/AssetSelectorView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/AssetSelectorView.kt @@ -6,9 +6,9 @@ import android.util.AttributeSet import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import coil.ImageLoader -import coil.load import io.novafoundation.nova.common.utils.WithContextExtensions import io.novafoundation.nova.common.utils.getEnum +import io.novafoundation.nova.common.utils.images.setIcon import io.novafoundation.nova.common.utils.useAttributes import io.novafoundation.nova.common.view.shape.addRipple import io.novafoundation.nova.common.view.shape.getBlockDrawable @@ -71,7 +71,7 @@ class AssetSelectorView @JvmOverloads constructor( with(assetSelectorModel) { assetSelectorBalance.text = assetModel.assetBalance assetSelectorTokenName.text = title - assetSelectorIcon.load(assetModel.imageUrl, imageLoader) + assetSelectorIcon.setIcon(assetModel.icon, imageLoader) } } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/PriceSectionView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/PriceSectionView.kt index c042ab3fbe..bbfa7522ab 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/PriceSectionView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/PriceSectionView.kt @@ -20,7 +20,7 @@ class PriceSectionView @JvmOverloads constructor( attrs?.let(::applyAttrs) } - fun setPrice(token: String, fiat: String?) { + fun setPrice(token: CharSequence, fiat: CharSequence?) { sectionPriceToken.text = token sectionPriceFiat.setTextOrHide(fiat) } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/TotalAmountView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/TotalAmountView.kt index e8dd0edfe5..38048f5bcc 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/TotalAmountView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/TotalAmountView.kt @@ -29,7 +29,7 @@ class TotalAmountView @JvmOverloads constructor( setAmount(amountModel?.token, amountModel?.fiat) } - fun setAmount(token: String?, fiat: String?) { + fun setAmount(token: CharSequence?, fiat: CharSequence?) { totalAmountToken.text = token totalAmountFiat.text = fiat } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/amount/ChooseAmountInputView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/amount/ChooseAmountInputView.kt index 3dbf57cfe3..a9a9102553 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/amount/ChooseAmountInputView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/amount/ChooseAmountInputView.kt @@ -7,9 +7,10 @@ import android.widget.EditText import androidx.constraintlayout.widget.ConstraintLayout import coil.ImageLoader import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.images.Icon import io.novafoundation.nova.common.utils.setTextOrHide import io.novafoundation.nova.common.view.shape.getInputBackground -import io.novafoundation.nova.feature_account_api.presenatation.chain.loadTokenIcon +import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.presentation.model.ChooseAmountInputModel import kotlinx.android.synthetic.main.view_choose_amount_input.view.chooseAmountInputFiat @@ -38,8 +39,8 @@ class ChooseAmountInputView @JvmOverloads constructor( background = context.getInputBackground() } - fun loadAssetImage(imageUrl: String?) { - chooseAmountInputImage.loadTokenIcon(imageUrl, imageLoader) + fun loadAssetImage(icon: Icon) { + chooseAmountInputImage.setTokenIcon(icon, imageLoader) } fun setAssetName(name: String) { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/amount/ChooseAmountView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/amount/ChooseAmountView.kt index 7ba63914d9..94ec888463 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/amount/ChooseAmountView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/amount/ChooseAmountView.kt @@ -5,6 +5,7 @@ import android.util.AttributeSet import android.view.View import android.widget.EditText import androidx.constraintlayout.widget.ConstraintLayout +import io.novafoundation.nova.common.utils.images.Icon import io.novafoundation.nova.common.utils.setTextOrHide import io.novafoundation.nova.common.utils.useAttributes import io.novafoundation.nova.feature_wallet_api.R @@ -37,8 +38,8 @@ class ChooseAmountView @JvmOverloads constructor( chooseAmountBalanceLabel.setTextOrHide(label) } - fun loadAssetImage(imageUrl: String) { - chooseAmountInput.loadAssetImage(imageUrl) + fun loadAssetImage(icon: Icon) { + chooseAmountInput.loadAssetImage(icon) } fun setTitle(title: String?) { diff --git a/feature-wallet-api/src/main/res/layout/item_asset_selector.xml b/feature-wallet-api/src/main/res/layout/item_asset_selector.xml index 55d9d464a9..01dd050e53 100644 --- a/feature-wallet-api/src/main/res/layout/item_asset_selector.xml +++ b/feature-wallet-api/src/main/res/layout/item_asset_selector.xml @@ -7,38 +7,31 @@ android:background="@drawable/bg_primary_list_item" tools:background="@color/secondary_screen_background"> - - + tools:src="@drawable/ic_fallback_network_icon" /> @@ -46,24 +39,29 @@ - + app:layout_constraintTop_toTopOf="parent" /> \ No newline at end of file diff --git a/feature-wallet-api/src/main/res/layout/view_asset_selector.xml b/feature-wallet-api/src/main/res/layout/view_asset_selector.xml index 85c0c7ba8f..c6e045a5dd 100644 --- a/feature-wallet-api/src/main/res/layout/view_asset_selector.xml +++ b/feature-wallet-api/src/main/res/layout/view_asset_selector.xml @@ -9,18 +9,15 @@ + tools:src="@drawable/ic_fallback_network_icon" /> 0 + "KSM" -> 1 + else -> 2 + } + +val TokenSymbol.alphabeticalOrder + get() = value + +fun TokenSymbol.Companion.defaultComparatorFrom(extractor: (K) -> TokenSymbol): Comparator = Comparator.comparing(extractor, defaultComparator()) + +fun TokenSymbol.Companion.defaultComparator(): Comparator = compareBy { it.mainTokensFirstAscendingOrder } + .thenBy { it.alphabeticalOrder } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt index fb90529a77..595ac2d3c8 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt @@ -97,7 +97,7 @@ fun mapChainAssetToLocal(asset: Chain.Asset, gson: Gson): ChainAssetLocal { source = mapAssetSourceToLocal(asset.source), buyProviders = gson.toJson(asset.buyProviders), typeExtras = gson.toJson(typeExtras), - icon = asset.iconUrl, + icon = asset.icon, enabled = asset.enabled ) } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt index 81ce937317..1c5c6ddab1 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt @@ -279,7 +279,7 @@ fun mapChainAssetLocalToAsset(local: ChainAssetLocal, gson: Gson): Chain.Asset { val buyProviders = local.buyProviders?.let?>(gson::fromJsonOrNull).orEmpty() return Chain.Asset( - iconUrl = local.icon, + icon = local.icon, id = local.id, symbol = local.symbol.asTokenSymbol(), precision = local.precision.asPrecision(), diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt index 76b61f9ae2..f4cd220ae6 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt @@ -65,7 +65,7 @@ data class Chain( ) data class Asset( - val iconUrl: String?, + val icon: String?, val id: ChainAssetId, val priceId: String?, val chainId: ChainId,