diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/BalanceViewItem.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/BalanceViewItem.kt index d3d750f45e9..dcaeedf0927 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/BalanceViewItem.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/BalanceViewItem.kt @@ -2,9 +2,13 @@ package io.horizontalsystems.bankwallet.modules.balance import androidx.compose.runtime.Immutable import io.horizontalsystems.bankwallet.R -import io.horizontalsystems.bankwallet.core.* +import io.horizontalsystems.bankwallet.core.AdapterState +import io.horizontalsystems.bankwallet.core.App import io.horizontalsystems.bankwallet.core.adapters.zcash.ZcashAdapter +import io.horizontalsystems.bankwallet.core.iconPlaceholder +import io.horizontalsystems.bankwallet.core.imageUrl import io.horizontalsystems.bankwallet.core.providers.Translator +import io.horizontalsystems.bankwallet.core.swappable import io.horizontalsystems.bankwallet.entities.Currency import io.horizontalsystems.bankwallet.entities.Wallet import io.horizontalsystems.core.helpers.DateHelper @@ -24,7 +28,7 @@ data class BalanceViewItem( val exchangeValue: DeemedValue, val diff: BigDecimal?, val secondaryValue: DeemedValue, - val coinValueLocked: DeemedValue, + val coinValueLocked: DeemedValue, val fiatValueLocked: DeemedValue, val expanded: Boolean, val sendEnabled: Boolean = false, @@ -90,7 +94,7 @@ class BalanceViewItemFactory { } is AdapterState.SearchingTxs -> Translator.getString(R.string.Balance_SearchingTransactions) is AdapterState.Zcash -> { - when(state.zcashState){ + when (state.zcashState) { is ZcashAdapter.ZcashState.DownloadingBlocks -> Translator.getString(R.string.Balance_DownloadingBlocks) is ZcashAdapter.ZcashState.ScanningBlocks -> Translator.getString(R.string.Balance_ScanningBlocks) } @@ -151,8 +155,12 @@ class BalanceViewItemFactory { hideBalance: Boolean, coinDecimals: Int, token: Token - ): DeemedValue { - val visible = !hideBalance && balance > BigDecimal.ZERO + ): DeemedValue { + if (balance <= BigDecimal.ZERO) { + return DeemedValue(null, false, false) + } + + val visible = !hideBalance val deemed = state !is AdapterState.Synced val value = App.numberFormatter.formatCoinFull(balance, token.coin.code, coinDecimals) @@ -173,8 +181,7 @@ class BalanceViewItemFactory { val state = item.state val latestRate = item.coinPrice - val showSyncing = expanded && (state is AdapterState.Syncing || state is AdapterState.SearchingTxs || state is AdapterState.Zcash) - val balanceTotalVisibility = !hideBalance && !showSyncing + val balanceTotalVisibility = !hideBalance val fiatLockedVisibility = !hideBalance && item.balanceData.locked > BigDecimal.ZERO val (primaryValue, secondaryValue) = BalanceViewHelper.getPrimaryAndSecondaryValues( @@ -189,43 +196,43 @@ class BalanceViewItemFactory { ) return BalanceViewItem( - wallet = item.wallet, - currencySymbol = currency.symbol, - coinCode = coin.code, - coinTitle = coin.name, - coinIconUrl = coin.imageUrl, - coinIconPlaceholder = wallet.token.iconPlaceholder, - primaryValue = primaryValue, - secondaryValue = secondaryValue, - coinValueLocked = lockedCoinValue( - state, - item.balanceData.locked, - hideBalance, - wallet.decimal, - wallet.token - ), - fiatValueLocked = BalanceViewHelper.currencyValue( - item.balanceData.locked, - latestRate, - fiatLockedVisibility, - true, - currency, - state !is AdapterState.Synced - ), - exchangeValue = BalanceViewHelper.rateValue(latestRate, currency, !showSyncing), - diff = item.coinPrice?.diff, - expanded = expanded, - sendEnabled = state is AdapterState.Synced, - syncingProgress = getSyncingProgress(state, wallet.token.blockchainType), - syncingTextValue = getSyncingText(state, expanded), - syncedUntilTextValue = getSyncedUntilText(state, expanded), - failedIconVisible = state is AdapterState.NotSynced, - coinIconVisible = state !is AdapterState.NotSynced, - badge = wallet.badge, - swapVisible = wallet.token.swappable, - swapEnabled = state is AdapterState.Synced, - errorMessage = (state as? AdapterState.NotSynced)?.error?.message, - isWatchAccount = watchAccount + wallet = item.wallet, + currencySymbol = currency.symbol, + coinCode = coin.code, + coinTitle = coin.name, + coinIconUrl = coin.imageUrl, + coinIconPlaceholder = wallet.token.iconPlaceholder, + primaryValue = primaryValue, + secondaryValue = secondaryValue, + coinValueLocked = lockedCoinValue( + state, + item.balanceData.locked, + hideBalance, + wallet.decimal, + wallet.token + ), + fiatValueLocked = BalanceViewHelper.currencyValue( + item.balanceData.locked, + latestRate, + fiatLockedVisibility, + true, + currency, + state !is AdapterState.Synced + ), + exchangeValue = BalanceViewHelper.rateValue(latestRate, currency, true), + diff = item.coinPrice?.diff, + expanded = expanded, + sendEnabled = state is AdapterState.Synced, + syncingProgress = getSyncingProgress(state, wallet.token.blockchainType), + syncingTextValue = getSyncingText(state, expanded), + syncedUntilTextValue = getSyncedUntilText(state, expanded), + failedIconVisible = state is AdapterState.NotSynced, + coinIconVisible = state !is AdapterState.NotSynced, + badge = wallet.badge, + swapVisible = wallet.token.swappable, + swapEnabled = state is AdapterState.Synced, + errorMessage = (state as? AdapterState.NotSynced)?.error?.message, + isWatchAccount = watchAccount ) } } diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanScreen.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanScreen.kt new file mode 100644 index 00000000000..3050e5ba955 --- /dev/null +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanScreen.kt @@ -0,0 +1,371 @@ +package io.horizontalsystems.bankwallet.modules.balance.token + +import android.view.View +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.navigation.NavController +import io.horizontalsystems.bankwallet.R +import io.horizontalsystems.bankwallet.core.isCustom +import io.horizontalsystems.bankwallet.core.providers.Translator +import io.horizontalsystems.bankwallet.core.slideFromBottom +import io.horizontalsystems.bankwallet.core.slideFromRight +import io.horizontalsystems.bankwallet.modules.balance.BackupRequiredError +import io.horizontalsystems.bankwallet.modules.balance.BalanceViewItem +import io.horizontalsystems.bankwallet.modules.balance.BalanceViewModel +import io.horizontalsystems.bankwallet.modules.coin.CoinFragment +import io.horizontalsystems.bankwallet.modules.manageaccount.dialogs.BackupRequiredDialog +import io.horizontalsystems.bankwallet.modules.receive.address.ReceiveAddressFragment +import io.horizontalsystems.bankwallet.modules.send.SendFragment +import io.horizontalsystems.bankwallet.modules.swap.SwapMainModule +import io.horizontalsystems.bankwallet.modules.syncerror.SyncErrorDialog +import io.horizontalsystems.bankwallet.modules.transactions.TransactionViewItem +import io.horizontalsystems.bankwallet.modules.transactions.TransactionsViewModel +import io.horizontalsystems.bankwallet.modules.transactions.transactionList +import io.horizontalsystems.bankwallet.ui.compose.ComposeAppTheme +import io.horizontalsystems.bankwallet.ui.compose.TranslatableString +import io.horizontalsystems.bankwallet.ui.compose.components.AppBar +import io.horizontalsystems.bankwallet.ui.compose.components.ButtonPrimaryCircle +import io.horizontalsystems.bankwallet.ui.compose.components.ButtonPrimaryDefault +import io.horizontalsystems.bankwallet.ui.compose.components.ButtonPrimaryYellowWithIcon +import io.horizontalsystems.bankwallet.ui.compose.components.CoinImage +import io.horizontalsystems.bankwallet.ui.compose.components.HsBackButton +import io.horizontalsystems.bankwallet.ui.compose.components.ListEmptyView +import io.horizontalsystems.bankwallet.ui.compose.components.RowUniversal +import io.horizontalsystems.bankwallet.ui.compose.components.VSpacer +import io.horizontalsystems.bankwallet.ui.compose.components.body_grey +import io.horizontalsystems.bankwallet.ui.compose.components.subhead2_grey +import io.horizontalsystems.bankwallet.ui.extensions.RotatingCircleProgressView +import io.horizontalsystems.core.helpers.HudHelper + + +@Composable +fun TokenBalanceScreen( + viewModel: TokenBalanceViewModel, + transactionsViewModel: TransactionsViewModel, + navController: NavController +) { + val uiState = viewModel.uiState + + Scaffold( + backgroundColor = ComposeAppTheme.colors.tyler, + topBar = { + AppBar( + title = TranslatableString.PlainString(uiState.title), + navigationIcon = { + HsBackButton(onClick = { navController.popBackStack() }) + } + ) + } + ) { paddingValues -> + val transactionItems = uiState.transactions + if (transactionItems.isNullOrEmpty()) { + Column(Modifier.padding(paddingValues)) { + uiState.balanceViewItem?.let { + TokenBalanceHeader(balanceViewItem = it, navController = navController, viewModel = viewModel) + } + if (transactionItems == null) { + ListEmptyView( + text = stringResource(R.string.Transactions_WaitForSync), + icon = R.drawable.ic_clock + ) + } else { + ListEmptyView( + text = stringResource(R.string.Transactions_EmptyList), + icon = R.drawable.ic_outgoingraw + ) + } + } + } else { + val listState = rememberLazyListState() + LazyColumn(Modifier.padding(paddingValues), state = listState) { + item { + uiState.balanceViewItem?.let { + TokenBalanceHeader(balanceViewItem = it, navController = navController, viewModel = viewModel) + } + } + + transactionList( + transactionsMap = transactionItems, + willShow = { viewModel.willShow(it) }, + onClick = { onTransactionClick(it, viewModel, transactionsViewModel, navController) }, + onBottomReached = { viewModel.onBottomReached() } + ) + } + } + } + +} + + +private fun onTransactionClick( + transactionViewItem: TransactionViewItem, + tokenBalanceViewModel: TokenBalanceViewModel, + transactionsViewModel : TransactionsViewModel, + navController: NavController +) { + val transactionItem = tokenBalanceViewModel.getTransactionItem(transactionViewItem) ?: return + transactionsViewModel.tmpItemToShow = transactionItem + + navController.slideFromBottom(R.id.transactionInfoFragment) +} + +@Composable +private fun TokenBalanceHeader( + balanceViewItem: BalanceViewItem, + navController: NavController, + viewModel: TokenBalanceViewModel, +) { + val context = LocalContext.current + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 18.dp, end = 18.dp, top = 18.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + WalletIcon( + viewItem = balanceViewItem, + viewModel = viewModel, + navController = navController, + ) + VSpacer(height = 6.dp) + Text( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + viewModel.toggleBalanceVisibility() + HudHelper.vibrate(context) + } + ), + text = if (balanceViewItem.primaryValue.visible) balanceViewItem.primaryValue.value else "*****", + color = if (balanceViewItem.primaryValue.dimmed) ComposeAppTheme.colors.grey else ComposeAppTheme.colors.leah, + style = ComposeAppTheme.typography.title2R, + maxLines = 1, + ) + VSpacer(height = 6.dp) + if (balanceViewItem.syncingTextValue.visible && balanceViewItem.syncingTextValue.value != null) { + body_grey( + text = balanceViewItem.syncingTextValue.value + (balanceViewItem.syncedUntilTextValue.value?.let { " - $it" } ?: ""), + maxLines = 1, + ) + } else { + Text( + text = if (balanceViewItem.secondaryValue.visible) balanceViewItem.secondaryValue.value else "*****", + color = if (balanceViewItem.secondaryValue.dimmed) ComposeAppTheme.colors.grey50 else ComposeAppTheme.colors.grey, + style = ComposeAppTheme.typography.body, + maxLines = 1, + ) + } + VSpacer(height = 24.dp) + ButtonsRow(viewItem = balanceViewItem, navController = navController, viewModel = viewModel) + LockedBalanceCell(balanceViewItem) + } +} + +@Composable +private fun LockedBalanceCell(balanceViewItem: BalanceViewItem) { + if (balanceViewItem.coinValueLocked.value != null) { + VSpacer(height = 8.dp) + RowUniversal( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .border(1.dp, ComposeAppTheme.colors.steel20, RoundedCornerShape(12.dp)) + .padding(horizontal = 16.dp), + ) { + subhead2_grey( + text = stringResource(R.string.Balance_LockedAmount_Title), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.weight(1f)) + Text( + modifier = Modifier.padding(start = 6.dp), + text = if (balanceViewItem.coinValueLocked.visible) balanceViewItem.coinValueLocked.value else "*****", + color = if (balanceViewItem.coinValueLocked.dimmed) ComposeAppTheme.colors.grey50 else ComposeAppTheme.colors.leah, + style = ComposeAppTheme.typography.subhead2, + maxLines = 1, + ) + } + VSpacer(height = 16.dp) + } +} + +@Composable +private fun WalletIcon( + viewItem: BalanceViewItem, + viewModel: TokenBalanceViewModel, + navController: NavController +) { + Box( + modifier = Modifier + .height(52.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + viewItem.syncingProgress.progress?.let { progress -> + AndroidView( + modifier = Modifier + .size(52.dp), + factory = { context -> + RotatingCircleProgressView(context) + }, + update = { view -> + val color = when (viewItem.syncingProgress.dimmed) { + true -> R.color.grey_50 + false -> R.color.grey + } + view.setProgressColored(progress, view.context.getColor(color)) + } + ) + } + if (viewItem.failedIconVisible) { + val view = LocalView.current + Image( + modifier = Modifier + .size(32.dp) + .clickable { + onSyncErrorClicked(viewItem, viewModel, navController, view) + }, + painter = painterResource(id = R.drawable.ic_attention_24), + contentDescription = "coin icon", + colorFilter = ColorFilter.tint(ComposeAppTheme.colors.lucian) + ) + } else { + CoinImage( + iconUrl = viewItem.coinIconUrl, + placeholder = viewItem.coinIconPlaceholder, + modifier = Modifier + .size(32.dp) + ) + } + } +} + +private fun onSyncErrorClicked(viewItem: BalanceViewItem, viewModel: TokenBalanceViewModel, navController: NavController, view: View) { + when (val syncErrorDetails = viewModel.getSyncErrorDetails(viewItem)) { + is BalanceViewModel.SyncError.Dialog -> { + val wallet = syncErrorDetails.wallet + val errorMessage = syncErrorDetails.errorMessage + + navController.slideFromBottom( + R.id.syncErrorDialog, + SyncErrorDialog.prepareParams(wallet, errorMessage) + ) + } + + is BalanceViewModel.SyncError.NetworkNotAvailable -> { + HudHelper.showErrorMessage(view, R.string.Hud_Text_NoInternet) + } + } +} + + +@Composable +private fun ButtonsRow(viewItem: BalanceViewItem, navController: NavController, viewModel: TokenBalanceViewModel) { + val onClickReceive = { + try { + val params = ReceiveAddressFragment.params(viewModel.getWalletForReceive(viewItem)) + navController.slideFromBottom(R.id.receiveFragment, params) + } catch (e: BackupRequiredError) { + val text = Translator.getString( + R.string.ManageAccount_BackupRequired_Description, + e.account.name, + e.coinTitle + ) + navController.slideFromBottom( + R.id.backupRequiredDialog, + BackupRequiredDialog.prepareParams(e.account, text) + ) + } + } + + Row( + modifier = Modifier.padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (viewItem.isWatchAccount) { + ButtonPrimaryDefault( + modifier = Modifier.weight(1f), + title = stringResource(R.string.Balance_Address), + onClick = onClickReceive, + ) + } else { + ButtonPrimaryYellowWithIcon( + modifier = Modifier.weight(1f), + icon = R.drawable.ic_arrow_up_right_24, + title = stringResource(R.string.Balance_Send), + onClick = { + navController.slideFromBottom( + R.id.sendXFragment, + SendFragment.prepareParams(viewItem.wallet) + ) + }, + enabled = viewItem.sendEnabled + ) + Spacer(modifier = Modifier.width(8.dp)) + ButtonPrimaryCircle( + icon = R.drawable.ic_arrow_down_left_24, + contentDescription = stringResource(R.string.Balance_Receive), + onClick = onClickReceive, + ) + if (viewItem.swapVisible) { + Spacer(modifier = Modifier.width(8.dp)) + ButtonPrimaryCircle( + icon = R.drawable.ic_swap_24, + contentDescription = stringResource(R.string.Swap), + onClick = { + navController.slideFromBottom( + R.id.swapFragment, + SwapMainModule.prepareParams(viewItem.wallet.token) + ) + }, + enabled = viewItem.swapEnabled + ) + } + } + Spacer(modifier = Modifier.width(8.dp)) + ButtonPrimaryCircle( + icon = R.drawable.ic_chart_24, + contentDescription = stringResource(R.string.Coin_Info), + enabled = !viewItem.wallet.token.isCustom, + onClick = { + val coinUid = viewItem.wallet.coin.uid + val arguments = CoinFragment.prepareParams(coinUid) + + navController.slideFromRight(R.id.coinFragment, arguments) + }, + ) + } +} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceFragment.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceFragment.kt new file mode 100644 index 00000000000..316f5cd1699 --- /dev/null +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceFragment.kt @@ -0,0 +1,61 @@ +package io.horizontalsystems.bankwallet.modules.balance.token + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.navigation.navGraphViewModels +import io.horizontalsystems.bankwallet.R +import io.horizontalsystems.bankwallet.core.App +import io.horizontalsystems.bankwallet.core.BaseFragment +import io.horizontalsystems.bankwallet.entities.Wallet +import io.horizontalsystems.bankwallet.modules.transactions.TransactionsViewModel +import io.horizontalsystems.bankwallet.ui.compose.ComposeAppTheme +import io.horizontalsystems.core.findNavController +import io.horizontalsystems.core.parcelable + +class TokenBalanceFragment : BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner) + ) + + try { + val wallet = requireArguments().parcelable(WALLET_KEY) ?: throw IllegalStateException("Wallet is Null!") + val viewModel by viewModels { TokenBalanceModule.Factory(wallet) } + val transactionsViewModel by navGraphViewModels(R.id.mainFragment) + + setContent { + ComposeAppTheme { + TokenBalanceScreen(viewModel, transactionsViewModel, findNavController()) + } + } + + } catch (t: Throwable) { + Toast.makeText( + App.instance, t.message ?: t.javaClass.simpleName, Toast.LENGTH_SHORT + ).show() + findNavController().popBackStack() + } + } + } + + companion object { + private const val WALLET_KEY = "wallet_key" + + fun prepareParams(wallet: Wallet) = bundleOf( + WALLET_KEY to wallet + ) + } +} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceModule.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceModule.kt new file mode 100644 index 00000000000..d869910de24 --- /dev/null +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceModule.kt @@ -0,0 +1,56 @@ +package io.horizontalsystems.bankwallet.modules.balance.token + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.horizontalsystems.bankwallet.core.App +import io.horizontalsystems.bankwallet.entities.Wallet +import io.horizontalsystems.bankwallet.modules.balance.BalanceAdapterRepository +import io.horizontalsystems.bankwallet.modules.balance.BalanceCache +import io.horizontalsystems.bankwallet.modules.balance.BalanceViewItem +import io.horizontalsystems.bankwallet.modules.balance.BalanceViewItemFactory +import io.horizontalsystems.bankwallet.modules.balance.BalanceXRateRepository +import io.horizontalsystems.bankwallet.modules.transactions.NftMetadataService +import io.horizontalsystems.bankwallet.modules.transactions.TransactionRecordRepository +import io.horizontalsystems.bankwallet.modules.transactions.TransactionSyncStateRepository +import io.horizontalsystems.bankwallet.modules.transactions.TransactionViewItem +import io.horizontalsystems.bankwallet.modules.transactions.TransactionViewItemFactory +import io.horizontalsystems.bankwallet.modules.transactions.TransactionsRateRepository + +class TokenBalanceModule { + + class Factory(private val wallet: Wallet) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + val balanceService = TokenBalanceService( + wallet, + BalanceXRateRepository(App.currencyManager, App.marketKit), + BalanceAdapterRepository(App.adapterManager, BalanceCache(App.appDatabase.enabledWalletsCacheDao())), + ) + + val tokenTransactionsService = TokenTransactionsService( + wallet, + TransactionRecordRepository(App.transactionAdapterManager), + TransactionsRateRepository(App.currencyManager, App.marketKit), + TransactionSyncStateRepository(App.transactionAdapterManager), + App.contactsRepository, + NftMetadataService(App.nftMetadataManager) + ) + + return TokenBalanceViewModel( + wallet, + balanceService, + BalanceViewItemFactory(), + tokenTransactionsService, + TransactionViewItemFactory(App.evmLabelManager, App.contactsRepository), + App.balanceHiddenManager, + App.connectivityManager + ) as T + } + } + + data class TokenBalanceUiState( + val title: String, + val balanceViewItem: BalanceViewItem?, + val transactions: Map>?, + ) +} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceService.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceService.kt new file mode 100644 index 00000000000..e82c1ca7b13 --- /dev/null +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceService.kt @@ -0,0 +1,91 @@ +package io.horizontalsystems.bankwallet.modules.balance.token + +import io.horizontalsystems.bankwallet.core.Clearable +import io.horizontalsystems.bankwallet.core.subscribeIO +import io.horizontalsystems.bankwallet.entities.Wallet +import io.horizontalsystems.bankwallet.modules.balance.BalanceAdapterRepository +import io.horizontalsystems.bankwallet.modules.balance.BalanceModule +import io.horizontalsystems.bankwallet.modules.balance.BalanceXRateRepository +import io.horizontalsystems.marketkit.models.CoinPrice +import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class TokenBalanceService( + private val wallet: Wallet, + private val xRateRepository: BalanceXRateRepository, + private val balanceAdapterRepository: BalanceAdapterRepository +) : Clearable { + + private val _balanceItemFlow = MutableStateFlow(null) + val balanceItemFlow = _balanceItemFlow.asStateFlow() + + var balanceItem: BalanceModule.BalanceItem? = null + private set(value) { + field = value + + _balanceItemFlow.update { value } + } + + private val disposables = CompositeDisposable() + + val baseCurrency by xRateRepository::baseCurrency + + fun start() { + balanceAdapterRepository.setWallet(listOf(wallet)) + xRateRepository.setCoinUids(listOf(wallet.coin.uid)) + + val latestRates = xRateRepository.getLatestRates() + + balanceItem = BalanceModule.BalanceItem( + wallet, + balanceAdapterRepository.balanceData(wallet), + balanceAdapterRepository.state(wallet), + latestRates[wallet.coin.uid] + ) + + xRateRepository.itemObservable + .subscribeIO { + handleXRateUpdate(it) + } + .let { + disposables.add(it) + } + + balanceAdapterRepository.readyObservable + .subscribeIO { + handleAdapterUpdate() + } + .let { + disposables.add(it) + } + + balanceAdapterRepository.updatesObservable + .subscribeIO { + handleAdapterUpdate() + } + .let { + disposables.add(it) + } + + } + + private fun handleXRateUpdate(latestRates: Map) { + balanceItem = balanceItem?.copy( + coinPrice = latestRates[wallet.coin.uid] + ) + } + + private fun handleAdapterUpdate() { + balanceItem = balanceItem?.copy( + balanceData = balanceAdapterRepository.balanceData(wallet), + state = balanceAdapterRepository.state(wallet) + ) + } + + override fun clear() { + disposables.clear() + balanceAdapterRepository.clear() + } +} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceViewModel.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceViewModel.kt new file mode 100644 index 00000000000..660c3a11321 --- /dev/null +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenBalanceViewModel.kt @@ -0,0 +1,149 @@ +package io.horizontalsystems.bankwallet.modules.balance.token + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.horizontalsystems.bankwallet.core.managers.BalanceHiddenManager +import io.horizontalsystems.bankwallet.core.managers.ConnectivityManager +import io.horizontalsystems.bankwallet.core.subscribeIO +import io.horizontalsystems.bankwallet.entities.Wallet +import io.horizontalsystems.bankwallet.modules.balance.BackupRequiredError +import io.horizontalsystems.bankwallet.modules.balance.BalanceModule +import io.horizontalsystems.bankwallet.modules.balance.BalanceViewItem +import io.horizontalsystems.bankwallet.modules.balance.BalanceViewItemFactory +import io.horizontalsystems.bankwallet.modules.balance.BalanceViewModel +import io.horizontalsystems.bankwallet.modules.balance.BalanceViewType +import io.horizontalsystems.bankwallet.modules.transactions.TransactionItem +import io.horizontalsystems.bankwallet.modules.transactions.TransactionViewItem +import io.horizontalsystems.bankwallet.modules.transactions.TransactionViewItemFactory +import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class TokenBalanceViewModel( + private val wallet: Wallet, + private val balanceService: TokenBalanceService, + private val balanceViewItemFactory: BalanceViewItemFactory, + private val transactionsService: TokenTransactionsService, + private val transactionViewItem2Factory: TransactionViewItemFactory, + private val balanceHiddenManager: BalanceHiddenManager, + private val connectivityManager: ConnectivityManager, +) : ViewModel() { + + private val title = wallet.token.coin.code + private val disposables = CompositeDisposable() + + private var balanceViewItem: BalanceViewItem? = null + private var transactions: Map>? = null + + var uiState by mutableStateOf( + TokenBalanceModule.TokenBalanceUiState( + title = title, + balanceViewItem = balanceViewItem, + transactions = transactions, + ) + ) + private set + + init { + viewModelScope.launch(Dispatchers.IO) { + balanceService.balanceItemFlow.collect { balanceItem -> + balanceItem?.let { + updateBalanceViewItem(it) + } + } + } + + viewModelScope.launch(Dispatchers.IO) { + balanceHiddenManager.balanceHiddenFlow.collect { + balanceService.balanceItem?.let { + updateBalanceViewItem(it) + } + } + } + + transactionsService.itemsObservable + .subscribeIO { + updateTransactions(it) + } + .let { + disposables.add(it) + } + + viewModelScope.launch(Dispatchers.IO) { + balanceService.start() + delay(300) + transactionsService.start() + } + } + + private fun emitUiState() { + viewModelScope.launch { + uiState = TokenBalanceModule.TokenBalanceUiState( + title = title, + balanceViewItem = balanceViewItem, + transactions = transactions, + ) + } + } + + private fun updateTransactions(items: List) { + transactions = items + .map { transactionViewItem2Factory.convertToViewItemCached(it) } + .groupBy { it.formattedDate } + + emitUiState() + } + + private fun updateBalanceViewItem(balanceItem: BalanceModule.BalanceItem) { + val balanceViewItem = balanceViewItemFactory.viewItem( + balanceItem, + balanceService.baseCurrency, + true, + balanceHiddenManager.balanceHidden, + wallet.account.isWatchAccount, + BalanceViewType.CoinThenFiat + ) + + this.balanceViewItem = balanceViewItem.copy( + primaryValue = balanceViewItem.primaryValue.copy(value = balanceViewItem.primaryValue.value + " " + balanceViewItem.coinCode) + ) + + emitUiState() + } + + fun getWalletForReceive(viewItem: BalanceViewItem) = when { + viewItem.wallet.account.isBackedUp || viewItem.wallet.account.isFileBackedUp -> viewItem.wallet + else -> throw BackupRequiredError(viewItem.wallet.account, viewItem.coinTitle) + } + + fun onBottomReached() { + transactionsService.loadNext() + } + + fun willShow(viewItem: TransactionViewItem) { + transactionsService.fetchRateIfNeeded(viewItem.uid) + } + + fun getTransactionItem(viewItem: TransactionViewItem) = transactionsService.getTransactionItem(viewItem.uid) + + fun toggleBalanceVisibility() { + balanceHiddenManager.toggleBalanceHidden() + } + + fun getSyncErrorDetails(viewItem: BalanceViewItem): BalanceViewModel.SyncError = when { + connectivityManager.isConnected -> BalanceViewModel.SyncError.Dialog(viewItem.wallet, viewItem.errorMessage) + else -> BalanceViewModel.SyncError.NetworkNotAvailable() + } + + override fun onCleared() { + super.onCleared() + + disposables.clear() + balanceService.clear() + } + +} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenTransactionsService.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenTransactionsService.kt new file mode 100644 index 00000000000..f1395f51f0f --- /dev/null +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/token/TokenTransactionsService.kt @@ -0,0 +1,271 @@ +package io.horizontalsystems.bankwallet.modules.balance.token + +import io.horizontalsystems.bankwallet.core.Clearable +import io.horizontalsystems.bankwallet.core.subscribeIO +import io.horizontalsystems.bankwallet.entities.CurrencyValue +import io.horizontalsystems.bankwallet.entities.LastBlockInfo +import io.horizontalsystems.bankwallet.entities.Wallet +import io.horizontalsystems.bankwallet.entities.nft.NftAssetBriefMetadata +import io.horizontalsystems.bankwallet.entities.nft.NftUid +import io.horizontalsystems.bankwallet.entities.transactionrecords.TransactionRecord +import io.horizontalsystems.bankwallet.entities.transactionrecords.nftUids +import io.horizontalsystems.bankwallet.modules.contacts.ContactsRepository +import io.horizontalsystems.bankwallet.modules.transactions.FilterTransactionType +import io.horizontalsystems.bankwallet.modules.transactions.HistoricalRateKey +import io.horizontalsystems.bankwallet.modules.transactions.ITransactionRecordRepository +import io.horizontalsystems.bankwallet.modules.transactions.NftMetadataService +import io.horizontalsystems.bankwallet.modules.transactions.TransactionItem +import io.horizontalsystems.bankwallet.modules.transactions.TransactionSource +import io.horizontalsystems.bankwallet.modules.transactions.TransactionSyncStateRepository +import io.horizontalsystems.bankwallet.modules.transactions.TransactionWallet +import io.horizontalsystems.bankwallet.modules.transactions.TransactionsRateRepository +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.BehaviorSubject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.launch +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executors + +class TokenTransactionsService( + private val wallet: Wallet, + private val transactionRecordRepository: ITransactionRecordRepository, + private val rateRepository: TransactionsRateRepository, + private val transactionSyncStateRepository: TransactionSyncStateRepository, + private val contactsRepository: ContactsRepository, + private val nftMetadataService: NftMetadataService +) : Clearable { + private val disposables = CompositeDisposable() + private val transactionItems = CopyOnWriteArrayList() + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + private val itemsSubject = BehaviorSubject.create>() + val itemsObservable: Observable> get() = itemsSubject + + fun start() { + val transactionWallet = TransactionWallet(wallet.token, wallet.transactionSource, wallet.badge) + + transactionSyncStateRepository.setTransactionWallets(listOf(transactionWallet)) + transactionRecordRepository.setWallets( + listOf(transactionWallet), + transactionWallet, + FilterTransactionType.All, + null + ) + + transactionRecordRepository.itemsObservable + .subscribeIO { + handleUpdatedRecords(it) + } + .let { + disposables.add(it) + } + + rateRepository.dataExpiredObservable + .subscribeIO { + handleUpdatedHistoricalRates() + } + .let { + disposables.add(it) + } + + rateRepository.historicalRateObservable + .subscribeIO { + handleUpdatedHistoricalRate(it.first, it.second) + } + .let { + disposables.add(it) + } + + transactionSyncStateRepository.lastBlockInfoObservable + .subscribeIO { (source, lastBlockInfo) -> + handleLastBlockInfo(source, lastBlockInfo) + } + .let { + disposables.add(it) + } + + coroutineScope.launch { + nftMetadataService.assetsBriefMetadataFlow.collect { + handle(it) + } + } + + coroutineScope.launch { + contactsRepository.contactsFlow.drop(1).collect { + handleContactsUpdate() + } + } + } + + @Synchronized + private fun handleContactsUpdate() { + val tmpList = mutableListOf() + transactionItems.forEach { + tmpList.add(it.copy()) + } + + transactionItems.clear() + transactionItems.addAll(tmpList) + + itemsSubject.onNext(transactionItems) + } + + @Synchronized + private fun handle(assetBriefMetadataMap: Map) { + var updated = false + transactionItems.forEachIndexed { index, item -> + val tmpMetadata = item.nftMetadata.toMutableMap() + item.record.nftUids.forEach { nftUid -> + assetBriefMetadataMap[nftUid]?.let { + tmpMetadata[nftUid] = it + } + } + transactionItems[index] = item.copy(nftMetadata = tmpMetadata) + updated = true + } + + if (updated) { + itemsSubject.onNext(transactionItems) + } + } + + @Synchronized + private fun handleLastBlockInfo(source: TransactionSource, lastBlockInfo: LastBlockInfo) { + var updated = false + transactionItems.forEachIndexed { index, item -> + if (item.record.source == source && item.record.changedBy(item.lastBlockInfo, lastBlockInfo)) { + transactionItems[index] = item.copy(lastBlockInfo = lastBlockInfo) + updated = true + } + } + + if (updated) { + itemsSubject.onNext(transactionItems) + } + } + + @Synchronized + private fun handleUpdatedHistoricalRate(key: HistoricalRateKey, rate: CurrencyValue) { + var updated = false + for (i in 0 until transactionItems.size) { + val item = transactionItems[i] + + item.record.mainValue?.let { mainValue -> + mainValue.decimalValue?.let { decimalValue -> + if (mainValue.coin?.uid == key.coinUid && item.record.timestamp == key.timestamp) { + val currencyValue = CurrencyValue(rate.currency, decimalValue * rate.value) + + transactionItems[i] = item.copy(currencyValue = currencyValue) + updated = true + } + } + } + } + + if (updated) { + itemsSubject.onNext(transactionItems) + } + } + + @Synchronized + private fun handleUpdatedHistoricalRates() { + for (i in 0 until transactionItems.size) { + val item = transactionItems[i] + val currencyValue = getCurrencyValue(item.record) + + transactionItems[i] = item.copy(currencyValue = currencyValue) + } + + itemsSubject.onNext(transactionItems) + } + + @Synchronized + private fun handleUpdatedRecords(transactionRecords: List) { + val tmpList = mutableListOf() + + val nftUids = transactionRecords.nftUids + val nftMetadata = nftMetadataService.assetsBriefMetadata(nftUids) + + val missingNftUids = nftUids.subtract(nftMetadata.keys) + if (missingNftUids.isNotEmpty()) { + coroutineScope.launch { + nftMetadataService.fetch(missingNftUids) + } + } + + val newRecords = mutableListOf() + transactionRecords.forEach { record -> + var transactionItem = transactionItems.find { it.record == record } + if (transactionItem == null) { + newRecords.add(record) + } + + if (record.spam) return@forEach + + transactionItem = if (transactionItem == null) { + val lastBlockInfo = transactionSyncStateRepository.getLastBlockInfo(record.source) + val currencyValue = getCurrencyValue(record) + + TransactionItem(record, currencyValue, lastBlockInfo, nftMetadata) + } else { + transactionItem.copy(record = record) + } + + tmpList.add(transactionItem) + } + + if (newRecords.isNotEmpty() && newRecords.all { it.spam }) { + loadNext() + } else { + transactionItems.clear() + transactionItems.addAll(tmpList) + itemsSubject.onNext(transactionItems) + } + } + + private fun getCurrencyValue(record: TransactionRecord): CurrencyValue? { + val decimalValue = record.mainValue?.decimalValue ?: return null + val coinUid = record.mainValue?.coin?.uid ?: return null + + return rateRepository.getHistoricalRate(HistoricalRateKey(coinUid, record.timestamp)) + ?.let { rate -> CurrencyValue(rate.currency, decimalValue * rate.value) } + } + + private val executorService = Executors.newCachedThreadPool() + + fun loadNext() { + executorService.submit { + transactionRecordRepository.loadNext() + } + } + + fun fetchRateIfNeeded(recordUid: String) { + executorService.submit { + transactionItems.find { it.record.uid == recordUid }?.let { transactionItem -> + if (transactionItem.currencyValue == null) { + transactionItem.record.mainValue?.coin?.uid?.let { coinUid -> + rateRepository.fetchHistoricalRate(HistoricalRateKey(coinUid, transactionItem.record.timestamp)) + } + } + } + } + } + + fun getTransactionItem(recordUid: String): TransactionItem? { + return transactionItems.find { it.record.uid == recordUid } + } + + + override fun clear() { + disposables.clear() + transactionRecordRepository.clear() + rateRepository.clear() + transactionSyncStateRepository.clear() + coroutineScope.cancel() + executorService.shutdown() + } +} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/ui/BalanceCard.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/ui/BalanceCard.kt index e053d5b8853..7b04639b30d 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/ui/BalanceCard.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/ui/BalanceCard.kt @@ -47,6 +47,7 @@ import io.horizontalsystems.bankwallet.core.slideFromRight import io.horizontalsystems.bankwallet.modules.balance.BackupRequiredError import io.horizontalsystems.bankwallet.modules.balance.BalanceViewItem import io.horizontalsystems.bankwallet.modules.balance.BalanceViewModel +import io.horizontalsystems.bankwallet.modules.balance.token.TokenBalanceFragment import io.horizontalsystems.bankwallet.modules.coin.CoinFragment import io.horizontalsystems.bankwallet.modules.manageaccount.dialogs.BackupRequiredDialog import io.horizontalsystems.bankwallet.modules.receive.address.ReceiveAddressFragment @@ -305,7 +306,12 @@ private fun ButtonsRow(viewItem: BalanceViewItem, navController: NavController, ButtonPrimaryDefault( modifier = Modifier.weight(1f), title = stringResource(R.string.Balance_Address), - onClick = onClickReceive, + onClick = { + navController.slideFromRight( + R.id.tokenBalanceFragment, + TokenBalanceFragment.prepareParams(viewItem.wallet) + ) + }, ) } else { ButtonPrimaryYellow( @@ -324,7 +330,12 @@ private fun ButtonsRow(viewItem: BalanceViewItem, navController: NavController, ButtonPrimaryCircle( icon = R.drawable.ic_arrow_down_left_24, contentDescription = stringResource(R.string.Balance_Receive), - onClick = onClickReceive, + onClick = { + navController.slideFromRight( + R.id.tokenBalanceFragment, + TokenBalanceFragment.prepareParams(viewItem.wallet) + ) + }, ) Spacer(modifier = Modifier.width(8.dp)) ButtonPrimaryCircle( @@ -342,7 +353,12 @@ private fun ButtonsRow(viewItem: BalanceViewItem, navController: NavController, ButtonPrimaryDefault( modifier = Modifier.weight(1f), title = stringResource(R.string.Balance_Receive), - onClick = onClickReceive, + onClick = { + navController.slideFromRight( + R.id.tokenBalanceFragment, + TokenBalanceFragment.prepareParams(viewItem.wallet) + ) + }, ) } } @@ -363,6 +379,8 @@ private fun ButtonsRow(viewItem: BalanceViewItem, navController: NavController, @Composable private fun LockedValueRow(viewItem: BalanceViewItem) { + if (viewItem.coinValueLocked.value == null) return + AnimatedVisibility( visible = viewItem.coinValueLocked.visible, enter = expandVertically() + fadeIn(), diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/ui/BalanceForAccount.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/ui/BalanceForAccount.kt index f8499251162..6e07609c65b 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/ui/BalanceForAccount.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/balance/ui/BalanceForAccount.kt @@ -56,7 +56,7 @@ fun BalanceForAccount(navController: NavController, accountViewItem: AccountView val uiState = viewModel.uiState - Crossfade(uiState.viewState) { viewState -> + Crossfade(uiState.viewState, label = "") { viewState -> when (viewState) { ViewState.Success -> { val balanceViewItems = uiState.balanceViewItems diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/transactions/TransactionsScreen.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/transactions/TransactionsScreen.kt index cbac0a1f71d..62b14480c32 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/transactions/TransactionsScreen.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/transactions/TransactionsScreen.kt @@ -121,7 +121,7 @@ fun TransactionsScreen( } } - Crossfade(viewState) { viewState -> + Crossfade(viewState, label = "") { viewState -> when (viewState) { ViewState.Success -> { transactions?.let { transactionItems -> @@ -151,14 +151,14 @@ fun TransactionsScreen( ) { LazyListState(0, 0) } - - TransactionList( - listState = listState, - transactionsMap = transactionItems, - willShow = { viewModel.willShow(it) }, - onClick = { onTransactionClick(it, viewModel, navController) }, - onBottomReached = { viewModel.onBottomReached() } - ) + LazyColumn(state = listState) { + transactionList( + transactionsMap = transactionItems, + willShow = { viewModel.willShow(it) }, + onClick = { onTransactionClick(it, viewModel, navController) }, + onBottomReached = { viewModel.onBottomReached() } + ) + } } } } @@ -184,9 +184,7 @@ private fun onTransactionClick( } @OptIn(ExperimentalFoundationApi::class) -@Composable -fun TransactionList( - listState: LazyListState = rememberLazyListState(), +fun LazyListScope.transactionList( transactionsMap: Map>, willShow: (TransactionViewItem) -> Unit, onClick: (TransactionViewItem) -> Unit, @@ -194,43 +192,41 @@ fun TransactionList( ) { val bottomReachedUid = getBottomReachedUid(transactionsMap) - LazyColumn(state = listState) { - transactionsMap.forEach { (dateHeader, transactions) -> - stickyHeader { - DateHeader(dateHeader) - } - - val itemsCount = transactions.size - val singleElement = itemsCount == 1 - - itemsIndexed(transactions) { index, item -> - val position: SectionItemPosition = when { - singleElement -> SectionItemPosition.Single - index == 0 -> SectionItemPosition.First - index == itemsCount - 1 -> SectionItemPosition.Last - else -> SectionItemPosition.Middle - } + transactionsMap.forEach { (dateHeader, transactions) -> + stickyHeader { + DateHeader(dateHeader) + } - Box(modifier = Modifier.padding(horizontal = 16.dp)) { - TransactionCell(item, position) { onClick.invoke(item) } - } + val itemsCount = transactions.size + val singleElement = itemsCount == 1 - willShow.invoke(item) + itemsIndexed(transactions) { index, item -> + val position: SectionItemPosition = when { + singleElement -> SectionItemPosition.Single + index == 0 -> SectionItemPosition.First + index == itemsCount - 1 -> SectionItemPosition.Last + else -> SectionItemPosition.Middle + } - if (item.uid == bottomReachedUid) { - onBottomReached.invoke() - } + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + TransactionCell(item, position) { onClick.invoke(item) } } - item { - Spacer(modifier = Modifier.height(12.dp)) + willShow.invoke(item) + + if (item.uid == bottomReachedUid) { + onBottomReached.invoke() } } item { - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(12.dp)) } } + + item { + Spacer(modifier = Modifier.height(20.dp)) + } } private fun getBottomReachedUid(transactionsMap: Map>): String? { diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/Text.kt b/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/Text.kt index 64ac8543c8e..e33f59aa746 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/Text.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/Text.kt @@ -2942,6 +2942,27 @@ fun title2_leah( ) } +@Composable +fun title2R_grey( + text: String, + modifier: Modifier = Modifier, + textAlign: TextAlign? = null, + overflow: TextOverflow = TextOverflow.Clip, + maxLines: Int = Int.MAX_VALUE, + onTextLayout: (TextLayoutResult) -> Unit = {} +) { + Text( + text = text, + modifier = modifier, + textAlign = textAlign, + overflow = overflow, + maxLines = maxLines, + onTextLayout = onTextLayout, + style = ComposeAppTheme.typography.title2R, + color = ComposeAppTheme.colors.grey, + ) +} + @Composable fun title3_grey( text: String, diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index 7093b819357..b4b5b806b59 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -345,6 +345,9 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a153dd99a6c..a647f6b0962 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -370,6 +370,7 @@ The Native SegWit format is preferred in Bitcoin. While all address formats (Taproot, SegWit, Legacy) can be used to receive BTC, your Bitcoin balances for each address type are tracked separately. Account Not active + Locked Send