Updated on 2026-08-14
This commit is contained in:
parent
b9083752c6
commit
76f955568e
18 changed files with 134 additions and 92 deletions
|
|
@ -10,15 +10,13 @@ import com.tangem.LogFormat
|
|||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.common.BigDecimalAdapter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.tap.common.AndroidAssetReader
|
||||
import com.tangem.tap.common.AssetReader
|
||||
import com.tangem.tap.common.IntentHandler
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||
import com.tangem.tap.common.analytics.filters.BasicSignInFilter
|
||||
|
|
@ -52,7 +50,6 @@ import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
|||
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.persistence.CardBalanceStateAdapter
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.wallet.BuildConfig
|
||||
|
|
@ -137,14 +134,13 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
activityResultCaller = foregroundActivityObserver
|
||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
|
||||
initMoshiConverter()
|
||||
DomainLayer.init()
|
||||
NetworkConnectivity.createInstance(store, this)
|
||||
preferencesStorage = PreferencesStorage(this)
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
assetReader = AndroidAssetReader(this)
|
||||
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.INSTANCE.moshi)
|
||||
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi)
|
||||
initConfigManager(configLoader, ::initWithConfigDependency)
|
||||
initWarningMessagesManager()
|
||||
|
||||
|
|
@ -159,18 +155,6 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
appStateHolder.walletStoresManager = walletStoresManager
|
||||
}
|
||||
|
||||
//todo refactor: move to datasource and provide via DI
|
||||
private fun initMoshiConverter() {
|
||||
fun appAdapters(): List<Any> = listOf(
|
||||
BigDecimalAdapter(),
|
||||
CardBalanceStateAdapter(),
|
||||
)
|
||||
MoshiConverter.reInitInstance(
|
||||
adapters = appAdapters() + MoshiJsonConverter.getTangemSdkAdapters(),
|
||||
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return createCoilImageLoader(
|
||||
context = this,
|
||||
|
|
@ -207,7 +191,7 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
config = config,
|
||||
isDebug = BuildConfig.DEBUG,
|
||||
logConfig = LogConfig.analyticsHandlers,
|
||||
jsonConverter = MoshiConverter.INSTANCE,
|
||||
jsonConverter = MoshiConverter.sdkMoshiConverter,
|
||||
)
|
||||
factory.build(Analytics, buildData)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
|
|
@ -17,9 +16,8 @@ class LoadAvailableCoinsService(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val assetReader: AssetReader,
|
||||
) {
|
||||
private val moshi: Moshi by lazy { MoshiConverter.defaultMoshi() }
|
||||
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
|
||||
moshi.adapter(CurrenciesFromJson::class.java)
|
||||
MoshiConverter.networkMoshi.adapter(CurrenciesFromJson::class.java)
|
||||
|
||||
suspend fun getSupportedTokens(
|
||||
isTestNet: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class OldUserTokensRepository(
|
|||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val moshi = MoshiConverter.defaultMoshi()
|
||||
private val moshi = MoshiConverter.networkMoshi
|
||||
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, Blockchain::class.java),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,9 +13,8 @@ class UserTokensStorageService(
|
|||
private val oldUserTokensRepository: OldUserTokensRepository,
|
||||
private val fileReader: FileReader,
|
||||
) {
|
||||
private val moshi = MoshiConverter.defaultMoshi()
|
||||
private val userTokensAdapter: JsonAdapter<UserTokensResponse> =
|
||||
moshi.adapter(UserTokensResponse::class.java)
|
||||
MoshiConverter.networkMoshi.adapter(UserTokensResponse::class.java)
|
||||
|
||||
fun getUserTokens(userId: String): List<Currency>? {
|
||||
return try {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class TwinCardsManager(
|
|||
}
|
||||
|
||||
private fun getAdapter(): JsonAdapter<List<Issuer>> {
|
||||
return MoshiConverter.defaultMoshi().adapter(
|
||||
return MoshiConverter.sdkMoshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, Issuer::class.java),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ import timber.log.Timber
|
|||
import java.nio.charset.Charset
|
||||
|
||||
class WalletConnectRepository(val context: Application) {
|
||||
private val moshi = MoshiConverter.defaultMoshi()
|
||||
private val walletConnectAdapter: JsonAdapter<List<SessionDao>> = moshi.adapter(
|
||||
private val walletConnectAdapter: JsonAdapter<List<SessionDao>> = MoshiConverter.sdkMoshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, SessionDao::class.java),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.persistence
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
|
||||
class CardBalanceStateAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(src: AnalyticsParam.CardBalanceState): String = src.value
|
||||
|
||||
@FromJson
|
||||
fun fromJson(json: String): AnalyticsParam.CardBalanceState {
|
||||
return when (json) {
|
||||
AnalyticsParam.CardBalanceState.Empty.value -> AnalyticsParam.CardBalanceState.Empty
|
||||
AnalyticsParam.CardBalanceState.Full.value -> AnalyticsParam.CardBalanceState.Full
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,29 +4,35 @@ import android.app.Application
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.common.BigDecimalAdapter
|
||||
import java.util.*
|
||||
|
||||
class PreferencesStorage(applicationContext: Application) {
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
val appRatingLaunchObserver: AppRatingLaunchObserver
|
||||
val usedCardsPrefStorage: UsedCardsPrefStorage
|
||||
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
|
||||
val disclaimerPrefStorage: DisclaimerPrefStorage
|
||||
val toppedUpWalletStorage: ToppedUpWalletStorage
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
private val moshiConverter = MoshiJsonConverter(
|
||||
adapters = listOf(BigDecimalAdapter(), CardBalanceStateAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
|
||||
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
|
||||
)
|
||||
|
||||
init {
|
||||
incrementLaunchCounter()
|
||||
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
|
||||
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, MoshiConverter.INSTANCE)
|
||||
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, moshiConverter)
|
||||
usedCardsPrefStorage.migrate()
|
||||
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiConverter.INSTANCE)
|
||||
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, moshiConverter)
|
||||
fiatCurrenciesPrefStorage.migrate()
|
||||
disclaimerPrefStorage = DisclaimerPrefStorage(preferences)
|
||||
toppedUpWalletStorage = ToppedUpWalletStorage(preferences, MoshiConverter.INSTANCE)
|
||||
toppedUpWalletStorage = ToppedUpWalletStorage(preferences, moshiConverter)
|
||||
}
|
||||
|
||||
var chatFirstLaunchTime: Long?
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ package com.tangem.tap.persistence
|
|||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -59,18 +56,4 @@ class ToppedUpWalletStorage(
|
|||
companion object {
|
||||
private const val KEY = "userWalletsInfo"
|
||||
}
|
||||
}
|
||||
|
||||
class CardBalanceStateAdapter {
|
||||
@ToJson
|
||||
fun toJson(src: AnalyticsParam.CardBalanceState): String = src.value
|
||||
|
||||
@FromJson
|
||||
fun fromJson(json: String): AnalyticsParam.CardBalanceState {
|
||||
return when (json) {
|
||||
AnalyticsParam.CardBalanceState.Empty.value -> AnalyticsParam.CardBalanceState.Empty
|
||||
AnalyticsParam.CardBalanceState.Full.value -> AnalyticsParam.CardBalanceState.Full
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +1,28 @@
|
|||
package com.tangem.datasource.api.common
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import retrofit2.Converter
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
//todo needs to be refactored
|
||||
//TODO("Remove after removing Redux")
|
||||
@Deprecated("Provide by DI")
|
||||
object MoshiConverter {
|
||||
|
||||
//todo refactor: provide via DI
|
||||
var INSTANCE = MoshiJsonConverter()
|
||||
private set
|
||||
val networkMoshi: Moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.add(BigDecimalAdapter())
|
||||
.build()
|
||||
|
||||
fun reInitInstance(
|
||||
adapters: List<Any> = listOf(),
|
||||
typedAdapters: Map<Class<*>, JsonAdapter<*>> = mapOf(),
|
||||
) {
|
||||
INSTANCE = MoshiJsonConverter(adapters, typedAdapters)
|
||||
}
|
||||
val networkMoshiConverter: MoshiConverterFactory = MoshiConverterFactory.create(networkMoshi)
|
||||
|
||||
fun createFactory(moshi: Moshi = INSTANCE.moshi): Converter.Factory = MoshiConverterFactory.create(moshi)
|
||||
val sdkMoshiConverter: MoshiJsonConverter = MoshiJsonConverter(
|
||||
adapters = listOf(BigDecimalAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
|
||||
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
|
||||
)
|
||||
|
||||
//todo provide via DI using quealifiers
|
||||
fun defaultMoshi(): Moshi = INSTANCE.moshi
|
||||
|
||||
//todo provide via DI using quealifiers
|
||||
fun sdkMoshi(): Moshi = MoshiJsonConverter.INSTANCE.moshi
|
||||
val sdkMoshi: Moshi = sdkMoshiConverter.moshi
|
||||
}
|
||||
|
|
@ -3,16 +3,14 @@ package com.tangem.datasource.api.common
|
|||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// TODO: refactoring: make it better through factory
|
||||
@Deprecated("Create and provide by DI")
|
||||
fun createRetrofitInstance(
|
||||
baseUrl: String,
|
||||
okHttpBuilder: OkHttpClient.Builder = OkHttpClient.Builder(),
|
||||
interceptors: List<Interceptor> = emptyList(),
|
||||
converterFactory: Converter.Factory = MoshiConverter.createFactory(),
|
||||
logEnabled: Boolean,
|
||||
): Retrofit {
|
||||
okHttpBuilder.apply {
|
||||
|
|
@ -27,7 +25,7 @@ fun createRetrofitInstance(
|
|||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(converterFactory)
|
||||
.addConverterFactory(MoshiConverter.networkMoshiConverter)
|
||||
.client(okHttpBuilder.build())
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ package com.tangem.datasource.api.oneinch
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.oneinch.errors.OneIncResponseException
|
||||
import com.tangem.datasource.api.oneinch.models.SwapErrorDto
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
class OneInchErrorsHandler @Inject constructor(moshi: Moshi) {
|
||||
class OneInchErrorsHandler @Inject constructor(@NetworkMoshi moshi: Moshi) {
|
||||
|
||||
private val errorMoshiAdapter = moshi.adapter(SwapErrorDto::class.java)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ object PaymentologyApiService {
|
|||
|
||||
private fun createApi(): PaymentologyApi {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverter.createFactory(MoshiConverter.sdkMoshi()))
|
||||
.addConverterFactory(MoshiConverter.networkMoshiConverter)
|
||||
.baseUrl(PAYMENTOLOGY_BASE_URL)
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ object TangemTechService {
|
|||
private fun createApi(header: RequestHeader? = null): TangemTechApi {
|
||||
val headers = mutableListOf<RequestHeader>(CacheControlHeader).apply { header?.let(::add) }
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverter.createFactory())
|
||||
.addConverterFactory(MoshiConverter.networkMoshiConverter)
|
||||
.baseUrl(TANGEM_TECH_BASE_URL)
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@MustBeDocumented
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SdkMoshi
|
||||
|
||||
@Qualifier
|
||||
@MustBeDocumented
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class NetworkMoshi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.common.BigDecimalAdapter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class MoshiModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@NetworkMoshi
|
||||
fun provideNetworkMoshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.add(BigDecimalAdapter())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@SdkMoshi
|
||||
fun provideSdkMoshi(sdkMoshiJsonConverter: MoshiJsonConverter): Moshi {
|
||||
return sdkMoshiJsonConverter.moshi
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkMoshiConverter(): MoshiJsonConverter {
|
||||
return MoshiJsonConverter(
|
||||
adapters = listOf(BigDecimalAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
|
||||
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.datasource.api.common.BigDecimalAdapter
|
||||
import com.tangem.datasource.api.paymentology.PaymentologyApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.utils.RequestHeader.AuthenticationHeader
|
||||
import com.tangem.datasource.utils.RequestHeader.CacheControlHeader
|
||||
import com.tangem.datasource.utils.RequestHeader.*
|
||||
import com.tangem.datasource.utils.addHeaders
|
||||
import com.tangem.datasource.utils.allowLogging
|
||||
import com.tangem.lib.auth.AuthProvider
|
||||
|
|
@ -24,16 +22,7 @@ class NetworkModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.add(BigDecimalAdapter())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemTechApi(authProvider: AuthProvider, moshi: Moshi): TangemTechApi {
|
||||
fun provideTangemTechApi(authProvider: AuthProvider, @NetworkMoshi moshi: Moshi): TangemTechApi {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.baseUrl(PROD_TANGEM_TECH_BASE_URL)
|
||||
|
|
@ -50,6 +39,21 @@ class NetworkModule {
|
|||
.create(TangemTechApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePaymentologyApi(@NetworkMoshi moshi: Moshi): PaymentologyApi {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.baseUrl(PAYMENTOLOGY_BASE_URL)
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
.allowLogging()
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
.create(PaymentologyApi::class.java)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
|
||||
const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class OneInchApisModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOneInchApiFactory(moshi: Moshi): OneInchApiFactory {
|
||||
fun provideOneInchApiFactory(@NetworkMoshi moshi: Moshi): OneInchApiFactory {
|
||||
val apiFactory = OneInchApiFactory()
|
||||
apiFactory.putApi(ETH_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_ETH_PATH, moshi))
|
||||
apiFactory.putApi(BSC_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_BSC_PATH, moshi))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue