Updated on 2026-08-14
This commit is contained in:
parent
3a9bdb16ad
commit
aa621dd48f
10 changed files with 78 additions and 36 deletions
|
|
@ -294,9 +294,8 @@ internal object TokensDomainModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideIsAmountSubtractAvailableUseCase(
|
fun provideIsAmountSubtractAvailableUseCase(
|
||||||
currenciesRepository: CurrenciesRepository,
|
currenciesRepository: CurrenciesRepository,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
|
||||||
): IsAmountSubtractAvailableUseCase {
|
): IsAmountSubtractAvailableUseCase {
|
||||||
return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers)
|
return IsAmountSubtractAvailableUseCase(currenciesRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@ package com.tangem.domain.tokens
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use case for checking if currency amount can be subtracted.
|
* Use case for checking if currency amount can be subtracted.
|
||||||
|
|
@ -14,17 +12,18 @@ import kotlinx.coroutines.withContext
|
||||||
*/
|
*/
|
||||||
class IsAmountSubtractAvailableUseCase(
|
class IsAmountSubtractAvailableUseCase(
|
||||||
private val currenciesRepository: CurrenciesRepository,
|
private val currenciesRepository: CurrenciesRepository,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<Throwable, Boolean> =
|
suspend operator fun invoke(
|
||||||
Either.catch {
|
userWalletId: UserWalletId,
|
||||||
withContext(dispatchers.io) {
|
currency: CryptoCurrency,
|
||||||
when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency.network)) {
|
isGaslessEthTx: Boolean = false,
|
||||||
is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin
|
): Either<Throwable, Boolean> = Either.catch {
|
||||||
is FeePaidCurrency.SameCurrency -> true
|
if (isGaslessEthTx) return@catch true
|
||||||
is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId
|
when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency.network)) {
|
||||||
is FeePaidCurrency.FeeResource -> false
|
is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin
|
||||||
}
|
is FeePaidCurrency.SameCurrency -> true
|
||||||
}
|
is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId
|
||||||
|
is FeePaidCurrency.FeeResource -> false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -28,7 +28,6 @@ interface SendNotificationsComponent {
|
||||||
val analyticsCategoryName: String,
|
val analyticsCategoryName: String,
|
||||||
val userWalletId: UserWalletId,
|
val userWalletId: UserWalletId,
|
||||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
|
||||||
val appCurrency: AppCurrency,
|
val appCurrency: AppCurrency,
|
||||||
val notificationData: NotificationData,
|
val notificationData: NotificationData,
|
||||||
val callback: ModelCallback,
|
val callback: ModelCallback,
|
||||||
|
|
@ -41,6 +40,7 @@ interface SendNotificationsComponent {
|
||||||
val isIgnoreReduce: Boolean,
|
val isIgnoreReduce: Boolean,
|
||||||
val fee: Fee?,
|
val fee: Fee?,
|
||||||
val feeError: GetFeeError?,
|
val feeError: GetFeeError?,
|
||||||
|
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,6 @@ internal class SendConfirmComponent(
|
||||||
analyticsCategoryName = params.analyticsCategoryName,
|
analyticsCategoryName = params.analyticsCategoryName,
|
||||||
userWalletId = params.userWallet.walletId,
|
userWalletId = params.userWallet.walletId,
|
||||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
|
||||||
appCurrency = params.appCurrency,
|
appCurrency = params.appCurrency,
|
||||||
callback = model,
|
callback = model,
|
||||||
notificationData = NotificationData(
|
notificationData = NotificationData(
|
||||||
|
|
@ -116,6 +115,7 @@ internal class SendConfirmComponent(
|
||||||
isIgnoreReduce = model.confirmData.isIgnoreReduce,
|
isIgnoreReduce = model.confirmData.isIgnoreReduce,
|
||||||
fee = model.confirmData.fee,
|
fee = model.confirmData.fee,
|
||||||
feeError = model.confirmData.feeError,
|
feeError = model.confirmData.feeError,
|
||||||
|
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
import com.tangem.blockchain.common.AmountType
|
import com.tangem.blockchain.common.AmountType
|
||||||
import com.tangem.blockchain.common.TransactionData
|
import com.tangem.blockchain.common.TransactionData
|
||||||
|
import com.tangem.blockchain.common.transaction.Fee
|
||||||
import com.tangem.common.routing.AppRouter
|
import com.tangem.common.routing.AppRouter
|
||||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
|
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
|
||||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||||
|
|
@ -29,6 +30,7 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||||
|
|
@ -155,10 +157,7 @@ internal class SendConfirmModel @Inject constructor(
|
||||||
internal var feeStateConfiguration: FeeStateConfiguration = FeeStateConfiguration.None
|
internal var feeStateConfiguration: FeeStateConfiguration = FeeStateConfiguration.None
|
||||||
|
|
||||||
init {
|
init {
|
||||||
modelScope.launch {
|
updateAmountSubtractAvailability()
|
||||||
isAmountSubtractAvailable =
|
|
||||||
isAmountSubtractAvailableUseCase(userWallet.walletId, cryptoCurrency).getOrElse { false }
|
|
||||||
}
|
|
||||||
configConfirmNavigation()
|
configConfirmNavigation()
|
||||||
subscribeOnNotificationsUpdateTrigger()
|
subscribeOnNotificationsUpdateTrigger()
|
||||||
subscribeOnCheckFeeResultUpdates()
|
subscribeOnCheckFeeResultUpdates()
|
||||||
|
|
@ -357,7 +356,7 @@ internal class SendConfirmModel @Inject constructor(
|
||||||
|
|
||||||
val receivingAmount = checkAndCalculateSubtractedAmount(
|
val receivingAmount = checkAndCalculateSubtractedAmount(
|
||||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
cryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
|
||||||
amountValue = amountValue,
|
amountValue = amountValue,
|
||||||
feeValue = feeValue,
|
feeValue = feeValue,
|
||||||
reduceAmountBy = confirmData.reduceAmountBy.orZero(),
|
reduceAmountBy = confirmData.reduceAmountBy.orZero(),
|
||||||
|
|
@ -392,7 +391,9 @@ internal class SendConfirmModel @Inject constructor(
|
||||||
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
|
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
|
||||||
val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended
|
val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended
|
||||||
|
|
||||||
val result = if (feeExtended != null) {
|
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||||
|
|
||||||
|
val result = if (isFeeInTokenCurrency) {
|
||||||
createAndSendGaslessTransactionUseCase(
|
createAndSendGaslessTransactionUseCase(
|
||||||
userWallet = userWallet,
|
userWallet = userWallet,
|
||||||
transactionData = txData,
|
transactionData = txData,
|
||||||
|
|
@ -439,6 +440,16 @@ internal class SendConfirmModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getCurrencyStatusForFeePayment(): CryptoCurrencyStatus {
|
||||||
|
val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended
|
||||||
|
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||||
|
return if (isFeeInTokenCurrency) {
|
||||||
|
feeUMV2?.feeExtraInfo?.feeCryptoCurrencyStatus ?: cryptoCurrencyStatus
|
||||||
|
} else {
|
||||||
|
params.feeCryptoCurrencyStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun addTokenToWalletIfNeeded() {
|
private fun addTokenToWalletIfNeeded() {
|
||||||
if (cryptoCurrency !is CryptoCurrency.Token) return
|
if (cryptoCurrency !is CryptoCurrency.Token) return
|
||||||
val wallets = destinationUM?.wallets ?: return
|
val wallets = destinationUM?.wallets ?: return
|
||||||
|
|
@ -509,6 +520,7 @@ internal class SendConfirmModel @Inject constructor(
|
||||||
isIgnoreReduce = confirmData.isIgnoreReduce,
|
isIgnoreReduce = confirmData.isIgnoreReduce,
|
||||||
fee = confirmData.fee,
|
fee = confirmData.fee,
|
||||||
feeError = confirmData.feeError,
|
feeError = confirmData.feeError,
|
||||||
|
feeCryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
|
|
@ -610,9 +622,23 @@ internal class SendConfirmModel @Inject constructor(
|
||||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) {
|
override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) {
|
||||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||||
_uiState.update { it.copy(feeSelectorUM = feeSelectorUM) }
|
_uiState.update { it.copy(feeSelectorUM = feeSelectorUM) }
|
||||||
|
updateAmountSubtractAvailability()
|
||||||
updateConfirmNotifications()
|
updateConfirmNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateAmountSubtractAvailability() {
|
||||||
|
modelScope.launch {
|
||||||
|
val isGaslessEthTx =
|
||||||
|
feeUMV2?.feeExtraInfo?.transactionFeeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||||
|
isAmountSubtractAvailable =
|
||||||
|
isAmountSubtractAvailableUseCase(
|
||||||
|
userWalletId = userWallet.walletId,
|
||||||
|
currency = cryptoCurrency,
|
||||||
|
isGaslessEthTx = isGaslessEthTx,
|
||||||
|
).getOrElse { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val CHECK_FEE_UPDATE_DELAY = 10_000L
|
const val CHECK_FEE_UPDATE_DELAY = 10_000L
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,6 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
||||||
analyticsCategoryName = params.analyticsCategoryName,
|
analyticsCategoryName = params.analyticsCategoryName,
|
||||||
userWalletId = params.userWallet.walletId,
|
userWalletId = params.userWallet.walletId,
|
||||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
|
||||||
appCurrency = params.appCurrency,
|
appCurrency = params.appCurrency,
|
||||||
callback = model,
|
callback = model,
|
||||||
notificationData = SendNotificationsComponent.Params.NotificationData(
|
notificationData = SendNotificationsComponent.Params.NotificationData(
|
||||||
|
|
@ -113,6 +112,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
||||||
isIgnoreReduce = false,
|
isIgnoreReduce = false,
|
||||||
fee = model.confirmData.fee,
|
fee = model.confirmData.fee,
|
||||||
feeError = model.confirmData.feeError,
|
feeError = model.confirmData.feeError,
|
||||||
|
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,7 @@ internal class NFTSendConfirmModel @Inject constructor(
|
||||||
isIgnoreReduce = false,
|
isIgnoreReduce = false,
|
||||||
fee = confirmData.fee,
|
fee = confirmData.fee,
|
||||||
feeError = confirmData.feeError,
|
feeError = confirmData.feeError,
|
||||||
|
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, // TODO change when gasless implement
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ internal class NotificationsModel @Inject constructor(
|
||||||
private val analyticsCategoryName = params.analyticsCategoryName
|
private val analyticsCategoryName = params.analyticsCategoryName
|
||||||
private val userWalletId = params.userWalletId
|
private val userWalletId = params.userWalletId
|
||||||
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||||
private val feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus
|
private val feeCryptoCurrencyStatus = params.notificationData.feeCryptoCurrencyStatus
|
||||||
private val currency = cryptoCurrencyStatus.currency
|
private val currency = cryptoCurrencyStatus.currency
|
||||||
private val appCurrency = params.appCurrency
|
private val appCurrency = params.appCurrency
|
||||||
|
|
||||||
|
|
@ -92,7 +92,10 @@ internal class NotificationsModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
subscribeToNotificationUpdateTrigger()
|
subscribeToNotificationUpdateTrigger()
|
||||||
checkIfSubtractAvailable()
|
modelScope.launch {
|
||||||
|
checkIfSubtractAvailable()
|
||||||
|
buildNotifications()
|
||||||
|
}
|
||||||
incrementNotificationsShowCount()
|
incrementNotificationsShowCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,11 +105,12 @@ internal class NotificationsModel @Inject constructor(
|
||||||
.launchIn(modelScope)
|
.launchIn(modelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkIfSubtractAvailable() {
|
private suspend fun checkIfSubtractAvailable() {
|
||||||
modelScope.launch {
|
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(
|
||||||
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, currency).getOrElse { false }
|
userWalletId = userWalletId,
|
||||||
buildNotifications()
|
currency = currency,
|
||||||
}
|
isGaslessEthTx = notificationData.fee is Fee.Ethereum.TokenCurrency,
|
||||||
|
).getOrElse { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun incrementNotificationsShowCount() {
|
private fun incrementNotificationsShowCount() {
|
||||||
|
|
@ -115,16 +119,19 @@ internal class NotificationsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateState(data: NotificationData) {
|
private fun updateState(data: NotificationData) {
|
||||||
notificationData = data
|
notificationData = data
|
||||||
buildNotifications()
|
modelScope.launch {
|
||||||
|
checkIfSubtractAvailable()
|
||||||
|
buildNotifications()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildNotifications() = with(notificationData) {
|
private suspend fun buildNotifications() = with(notificationData) {
|
||||||
val feeValue = fee?.amount?.value
|
val feeValue = fee?.amount?.value
|
||||||
val sendingAmount = checkAndCalculateSubtractedAmount(
|
val sendingAmount = checkAndCalculateSubtractedAmount(
|
||||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
cryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
|
||||||
amountValue = amountValue,
|
amountValue = amountValue,
|
||||||
feeValue = feeValue,
|
feeValue = feeValue,
|
||||||
reduceAmountBy = reduceAmountBy,
|
reduceAmountBy = reduceAmountBy,
|
||||||
|
|
@ -369,6 +376,15 @@ internal class NotificationsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getCurrencyStatusForFeePayment(): CryptoCurrencyStatus {
|
||||||
|
val isFeeInTokenCurrency = notificationData.fee is Fee.Ethereum.TokenCurrency
|
||||||
|
return if (isFeeInTokenCurrency) {
|
||||||
|
notificationData.feeCryptoCurrencyStatus
|
||||||
|
} else {
|
||||||
|
cryptoCurrencyStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TRON_FEE_NOTIFICATION_MAX_SHOW_COUNT = 3
|
const val TRON_FEE_NOTIFICATION_MAX_SHOW_COUNT = 3
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
||||||
analyticsCategoryName = params.analyticsCategoryName,
|
analyticsCategoryName = params.analyticsCategoryName,
|
||||||
userWalletId = params.userWallet.walletId,
|
userWalletId = params.userWallet.walletId,
|
||||||
cryptoCurrencyStatus = model.primaryCurrencyStatus,
|
cryptoCurrencyStatus = model.primaryCurrencyStatus,
|
||||||
feeCryptoCurrencyStatus = model.primaryFeePaidCurrencyStatus,
|
|
||||||
appCurrency = params.appCurrency,
|
appCurrency = params.appCurrency,
|
||||||
callback = model,
|
callback = model,
|
||||||
notificationData = SendNotificationsComponent.Params.NotificationData(
|
notificationData = SendNotificationsComponent.Params.NotificationData(
|
||||||
|
|
@ -121,6 +120,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
||||||
isIgnoreReduce = model.confirmData.isIgnoreReduce,
|
isIgnoreReduce = model.confirmData.isIgnoreReduce,
|
||||||
fee = model.confirmData.fee,
|
fee = model.confirmData.fee,
|
||||||
feeError = model.confirmData.feeError,
|
feeError = model.confirmData.feeError,
|
||||||
|
feeCryptoCurrencyStatus = model.primaryFeePaidCurrencyStatus,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
||||||
isIgnoreReduce = confirmData.isIgnoreReduce,
|
isIgnoreReduce = confirmData.isIgnoreReduce,
|
||||||
fee = confirmData.fee,
|
fee = confirmData.fee,
|
||||||
feeError = confirmData.feeError,
|
feeError = confirmData.feeError,
|
||||||
|
feeCryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
swapNotificationsUpdateTrigger.triggerUpdate(
|
swapNotificationsUpdateTrigger.triggerUpdate(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue