refactor: Convert from Gson to Moshi (#428)
Moshi is faster to decode JSON at runtime, is actively maintained, has a smaller memory and method footprint, and a slightly smaller APK size. Moshi also correctly creates default constructor arguments instead of leaving them null, which was a source of `NullPointerExceptions` when using Gson. The conversion broadly consisted of: - Adding `@JsonClass(generateAdapter = true)` to data classes that marshall to/from JSON. - Replacing `@SerializedName(value = ...)` with `@Json(name = ...)`. - Replacing Gson instances with Moshi in Retrofit, Hilt, and tests. - Using Moshi adapters to marshall to/from JSON instead of Gson `toJson` / `fromJson`. - Deleting `Rfc3339DateJsonAdapter` and related code, and using the equivalent adapter bundled with Moshi. - Rewriting `GuardedBooleanAdapter` as a more generic `GuardedAdapter`. - Deleting unused ProGuard rules; Moshi generates adapters using code generation, not runtime reflection. The conversion surfaced some bugs which have been fixed. - Not all audio attachments have attachment size metadata. Don't show the attachment preview if the metadata is missing. - Some `throwable` were not being logged correctly. - The wrong type was being used when parsing the response when sending a scheduled status. - Exceptions other than `HttpException` or `IoException` would also cause a status to be resent. If there's a JSON error parsing a response the status would be repeatedly sent. - In tests strings containing error responses were not valid JSON. - Workaround Mastodon a bug and ensure `filter.keywords` is populated, https://github.com/mastodon/mastodon/issues/29142
This commit is contained in:
parent
d01c72b7d7
commit
a3d45ca9ec
|
@ -143,7 +143,9 @@ dependencies {
|
|||
|
||||
implementation(libs.android.material)
|
||||
|
||||
implementation(libs.gson)
|
||||
implementation(libs.moshi)
|
||||
implementation(libs.moshi.adapters)
|
||||
ksp(libs.moshi.codegen)
|
||||
|
||||
implementation(libs.bundles.retrofit)
|
||||
|
||||
|
|
|
@ -57,19 +57,6 @@
|
|||
# error reports
|
||||
-keepnames class *
|
||||
|
||||
# https://github.com/google/gson/blob/master/examples/android-proguard-example/proguard.cfg
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
# Retain generic signatures of classes used in MastodonApi so Retrofit works
|
||||
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Single
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||
|
|
|
@ -19,7 +19,6 @@ package app.pachli.di
|
|||
|
||||
import app.pachli.updatecheck.FdroidService
|
||||
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -27,7 +26,7 @@ import dagger.hilt.components.SingletonComponent
|
|||
import javax.inject.Singleton
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.create
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
@ -37,11 +36,10 @@ object UpdateCheckModule {
|
|||
@Singleton
|
||||
fun providesFdroidService(
|
||||
httpClient: OkHttpClient,
|
||||
gson: Gson,
|
||||
): FdroidService = Retrofit.Builder()
|
||||
.baseUrl("https://f-droid.org")
|
||||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||
.build()
|
||||
.create()
|
||||
|
|
|
@ -19,16 +19,19 @@ package app.pachli.updatecheck
|
|||
|
||||
import androidx.annotation.Keep
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
@Keep
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FdroidPackageVersion(
|
||||
val versionName: String,
|
||||
val versionCode: Int,
|
||||
)
|
||||
|
||||
@Keep
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FdroidPackage(
|
||||
val packageName: String,
|
||||
val suggestedVersionCode: Int,
|
||||
|
|
|
@ -19,7 +19,6 @@ package app.pachli.di
|
|||
|
||||
import app.pachli.updatecheck.GitHubService
|
||||
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -27,7 +26,6 @@ import dagger.hilt.components.SingletonComponent
|
|||
import javax.inject.Singleton
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.create
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
@ -37,11 +35,10 @@ object UpdateCheckModule {
|
|||
@Singleton
|
||||
fun providesGitHubService(
|
||||
httpClient: OkHttpClient,
|
||||
gson: Gson,
|
||||
): GitHubService = Retrofit.Builder()
|
||||
.baseUrl("https://api.github.com")
|
||||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||
.build()
|
||||
.create()
|
||||
|
|
|
@ -19,23 +19,26 @@ package app.pachli.updatecheck
|
|||
|
||||
import androidx.annotation.Keep
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
@Keep
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GitHubReleaseAsset(
|
||||
/** File name for the asset, e.g., "113.apk" */
|
||||
val name: String,
|
||||
|
||||
/** MIME content type for the asset, e.g., "application/vnd.android.package-archive" */
|
||||
@SerializedName("content_type") val contentType: String,
|
||||
@Json(name = "content_type") val contentType: String,
|
||||
)
|
||||
|
||||
@Keep
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GitHubRelease(
|
||||
/** URL for the release's web page */
|
||||
@SerializedName("html_url") val htmlUrl: String,
|
||||
@Json(name = "html_url") val htmlUrl: String,
|
||||
val assets: List<GitHubReleaseAsset>,
|
||||
)
|
||||
|
||||
|
|
|
@ -37,10 +37,7 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
|
||||
import androidx.core.app.ActivityCompat.requestPermissions
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
@ -89,7 +86,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
var isToolbarVisible = true
|
||||
private set
|
||||
|
||||
private var attachments: ArrayList<AttachmentViewData>? = null
|
||||
private var attachments: List<AttachmentViewData>? = null
|
||||
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
||||
private var imageUrl: String? = null
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ class ReportNotificationViewHolder(
|
|||
binding.notificationSummary.text = itemView.context.getString(
|
||||
R.string.notification_summary_report_format,
|
||||
getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time),
|
||||
report.status_ids?.size ?: 0,
|
||||
report.statusIds?.size ?: 0,
|
||||
)
|
||||
binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
|
||||
|
||||
|
|
|
@ -901,6 +901,8 @@ abstract class StatusBaseViewHolder<T : IStatusViewData> protected constructor(i
|
|||
protected fun hasPreviewableAttachment(attachments: List<Attachment>): Boolean {
|
||||
for (attachment in attachments) {
|
||||
if (attachment.type == Attachment.Type.UNKNOWN) return false
|
||||
|
||||
if (attachment.meta?.original?.width == null && attachment.meta?.small?.width == null) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ package app.pachli.appstore
|
|||
|
||||
import app.pachli.core.accounts.AccountManager
|
||||
import app.pachli.core.database.dao.TimelineDao
|
||||
import com.google.gson.Gson
|
||||
import app.pachli.core.network.model.Poll
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -10,11 +12,12 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class CacheUpdater @Inject constructor(
|
||||
eventHub: EventHub,
|
||||
accountManager: AccountManager,
|
||||
timelineDao: TimelineDao,
|
||||
gson: Gson,
|
||||
moshi: Moshi,
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
|
@ -34,7 +37,7 @@ class CacheUpdater @Inject constructor(
|
|||
is StatusDeletedEvent ->
|
||||
timelineDao.delete(accountId, event.statusId)
|
||||
is PollVoteEvent -> {
|
||||
val pollString = gson.toJson(event.poll)
|
||||
val pollString = moshi.adapter<Poll>().toJson(event.poll)
|
||||
timelineDao.setVoted(accountId, event.statusId, pollString)
|
||||
}
|
||||
is PinEvent ->
|
||||
|
|
|
@ -18,7 +18,7 @@ data class StatusDeletedEvent(val statusId: String) : Event
|
|||
/** A status the user wrote was successfully sent */
|
||||
// TODO: Rename, calling it "Composed" does not imply anything about the sent state
|
||||
data class StatusComposedEvent(val status: Status) : Event
|
||||
data class StatusScheduledEvent(val status: Status) : Event
|
||||
data object StatusScheduledEvent : Event
|
||||
data class StatusEditedEvent(val originalId: String, val status: Status) : Event
|
||||
data class ProfileEditedEvent(val newProfileData: Account) : Event
|
||||
data class FilterChangedEvent(val filterKind: Filter.Kind) : Event
|
||||
|
|
|
@ -24,7 +24,8 @@ import app.pachli.core.network.model.Error
|
|||
import app.pachli.core.network.model.Links
|
||||
import app.pachli.core.network.model.Notification
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
@ -37,9 +38,10 @@ private val INVALID = LoadResult.Invalid<String, Notification>()
|
|||
/** [PagingSource] for Mastodon Notifications, identified by the Notification ID */
|
||||
class NotificationsPagingSource @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val gson: Gson,
|
||||
private val moshi: Moshi,
|
||||
private val notificationFilter: Set<Notification.Type>,
|
||||
) : PagingSource<String, Notification>() {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Notification> {
|
||||
Timber.d("load() with ${params.javaClass.simpleName} for key: ${params.key}")
|
||||
|
||||
|
@ -67,12 +69,12 @@ class NotificationsPagingSource @Inject constructor(
|
|||
if (errorBody.isBlank()) return@let "no reason given"
|
||||
|
||||
val error = try {
|
||||
gson.fromJson(errorBody, Error::class.java)
|
||||
moshi.adapter<Error>().fromJson(errorBody)!!
|
||||
} catch (e: Exception) {
|
||||
return@let "$errorBody ($e)"
|
||||
}
|
||||
|
||||
when (val desc = error.error_description) {
|
||||
when (val desc = error.errorDescription) {
|
||||
null -> error.error
|
||||
else -> "${error.error}: $desc"
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import androidx.paging.PagingSource
|
|||
import app.pachli.core.common.di.ApplicationScope
|
||||
import app.pachli.core.network.model.Notification
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -36,7 +36,7 @@ import timber.log.Timber
|
|||
|
||||
class NotificationsRepository @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val gson: Gson,
|
||||
private val moshi: Moshi,
|
||||
@ApplicationScope private val externalScope: CoroutineScope,
|
||||
) {
|
||||
private var factory: InvalidatingPagingSourceFactory<String, Notification>? = null
|
||||
|
@ -53,7 +53,7 @@ class NotificationsRepository @Inject constructor(
|
|||
Timber.d("getNotificationsStream(), filtering: $filter")
|
||||
|
||||
factory = InvalidatingPagingSourceFactory {
|
||||
NotificationsPagingSource(mastodonApi, gson, filter)
|
||||
NotificationsPagingSource(mastodonApi, moshi, filter)
|
||||
}
|
||||
|
||||
return Pager(
|
||||
|
|
|
@ -51,7 +51,7 @@ class ScheduledStatusViewModel @Inject constructor(
|
|||
pagingSourceFactory.remove(status)
|
||||
},
|
||||
{ throwable ->
|
||||
Timber.w("Error deleting scheduled status", throwable)
|
||||
Timber.w("Error deleting scheduled status: $throwable")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import app.pachli.components.search.SearchType
|
|||
import app.pachli.core.network.model.SearchResult
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import at.connyduck.calladapter.networkresult.getOrElse
|
||||
import timber.log.Timber
|
||||
|
||||
class SearchPagingSource<T : Any>(
|
||||
private val mastodonApi: MastodonApi,
|
||||
|
@ -61,7 +62,10 @@ class SearchPagingSource<T : Any>(
|
|||
limit = params.loadSize,
|
||||
offset = currentKey,
|
||||
following = false,
|
||||
).getOrElse { return LoadResult.Error(it) }
|
||||
).getOrElse {
|
||||
Timber.w(it)
|
||||
return LoadResult.Error(it)
|
||||
}
|
||||
|
||||
val res = parser(data)
|
||||
|
||||
|
|
|
@ -79,7 +79,6 @@ abstract class SearchFragment<T : Any> :
|
|||
}
|
||||
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
|
||||
if (loadState.refresh is LoadState.Error) {
|
||||
showError()
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ import app.pachli.util.EmptyPagingSource
|
|||
import app.pachli.viewdata.StatusViewData
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -66,7 +66,7 @@ class CachedTimelineRepository @Inject constructor(
|
|||
val timelineDao: TimelineDao,
|
||||
private val remoteKeyDao: RemoteKeyDao,
|
||||
private val translatedStatusDao: TranslatedStatusDao,
|
||||
private val gson: Gson,
|
||||
private val moshi: Moshi,
|
||||
@ApplicationScope private val externalScope: CoroutineScope,
|
||||
) {
|
||||
private var factory: InvalidatingPagingSourceFactory<Int, TimelineStatusWithAccount>? = null
|
||||
|
@ -118,7 +118,7 @@ class CachedTimelineRepository @Inject constructor(
|
|||
transactionProvider,
|
||||
timelineDao,
|
||||
remoteKeyDao,
|
||||
gson,
|
||||
moshi,
|
||||
),
|
||||
pagingSourceFactory = factory!!,
|
||||
).flow
|
||||
|
|
|
@ -36,7 +36,7 @@ import app.pachli.core.database.model.TimelineStatusWithAccount
|
|||
import app.pachli.core.network.model.Links
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
@ -54,7 +54,7 @@ class CachedTimelineRemoteMediator(
|
|||
private val transactionProvider: TransactionProvider,
|
||||
private val timelineDao: TimelineDao,
|
||||
private val remoteKeyDao: RemoteKeyDao,
|
||||
private val gson: Gson,
|
||||
private val moshi: Moshi,
|
||||
) : RemoteMediator<Int, TimelineStatusWithAccount>() {
|
||||
private val activeAccount = accountManager.activeAccount!!
|
||||
|
||||
|
@ -254,9 +254,9 @@ class CachedTimelineRemoteMediator(
|
|||
@Transaction
|
||||
private suspend fun insertStatuses(statuses: List<Status>) {
|
||||
for (status in statuses) {
|
||||
timelineDao.insertAccount(TimelineAccountEntity.from(status.account, activeAccount.id, gson))
|
||||
timelineDao.insertAccount(TimelineAccountEntity.from(status.account, activeAccount.id, moshi))
|
||||
status.reblog?.account?.let {
|
||||
val account = TimelineAccountEntity.from(it, activeAccount.id, gson)
|
||||
val account = TimelineAccountEntity.from(it, activeAccount.id, moshi)
|
||||
timelineDao.insertAccount(account)
|
||||
}
|
||||
|
||||
|
@ -264,7 +264,7 @@ class CachedTimelineRemoteMediator(
|
|||
TimelineStatusEntity.from(
|
||||
status,
|
||||
timelineUserId = activeAccount.id,
|
||||
gson = gson,
|
||||
moshi = moshi,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ import app.pachli.core.preferences.SharedPreferencesRepository
|
|||
import app.pachli.usecase.TimelineCases
|
||||
import app.pachli.util.StatusDisplayOptionsRepository
|
||||
import app.pachli.viewdata.StatusViewData
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -61,7 +61,7 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
accountManager: AccountManager,
|
||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
private val gson: Gson,
|
||||
private val moshi: Moshi,
|
||||
) : TimelineViewModel(
|
||||
savedStateHandle,
|
||||
timelineCases,
|
||||
|
@ -93,7 +93,7 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
.map {
|
||||
StatusViewData.from(
|
||||
it,
|
||||
gson,
|
||||
moshi,
|
||||
isExpanded = activeAccount.alwaysOpenSpoiler,
|
||||
isShowingContent = activeAccount.alwaysShowSensitiveMedia,
|
||||
)
|
||||
|
|
|
@ -48,7 +48,7 @@ import app.pachli.viewdata.StatusViewData
|
|||
import at.connyduck.calladapter.networkresult.fold
|
||||
import at.connyduck.calladapter.networkresult.getOrElse
|
||||
import at.connyduck.calladapter.networkresult.getOrThrow
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -69,7 +69,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||
eventHub: EventHub,
|
||||
accountManager: AccountManager,
|
||||
private val timelineDao: TimelineDao,
|
||||
private val gson: Gson,
|
||||
private val moshi: Moshi,
|
||||
private val repository: CachedTimelineRepository,
|
||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||
private val filtersRepository: FiltersRepository,
|
||||
|
@ -133,7 +133,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||
|
||||
var detailedStatus = if (timelineStatusWithAccount != null) {
|
||||
Timber.d("Loaded status from local timeline")
|
||||
val status = timelineStatusWithAccount.toStatus(gson)
|
||||
val status = timelineStatusWithAccount.toStatus(moshi)
|
||||
|
||||
// Return the correct status, depending on which one matched. If you do not do
|
||||
// this the status IDs will be different between the status that's displayed with
|
||||
|
@ -152,7 +152,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||
} else {
|
||||
StatusViewData.from(
|
||||
timelineStatusWithAccount,
|
||||
gson,
|
||||
moshi,
|
||||
isExpanded = alwaysOpenSpoiler,
|
||||
isShowingContent = (alwaysShowSensitiveMedia || !status.actionableStatus.sensitive),
|
||||
isDetailed = true,
|
||||
|
|
|
@ -39,6 +39,7 @@ import app.pachli.core.network.retrofit.MastodonApi
|
|||
import app.pachli.util.unsafeLazy
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
@ -222,12 +223,21 @@ class SendStatusService : Service() {
|
|||
)
|
||||
|
||||
val sendResult = if (isNew) {
|
||||
mastodonApi.createStatus(
|
||||
"Bearer " + account.accessToken,
|
||||
account.domain,
|
||||
statusToSend.idempotencyKey,
|
||||
newStatus,
|
||||
)
|
||||
if (newStatus.scheduledAt == null) {
|
||||
mastodonApi.createStatus(
|
||||
"Bearer " + account.accessToken,
|
||||
account.domain,
|
||||
statusToSend.idempotencyKey,
|
||||
newStatus,
|
||||
)
|
||||
} else {
|
||||
mastodonApi.createScheduledStatus(
|
||||
"Bearer " + account.accessToken,
|
||||
account.domain,
|
||||
statusToSend.idempotencyKey,
|
||||
newStatus,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
mastodonApi.editStatus(
|
||||
statusToSend.statusId!!,
|
||||
|
@ -250,16 +260,16 @@ class SendStatusService : Service() {
|
|||
val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
|
||||
|
||||
if (scheduled) {
|
||||
eventHub.dispatch(StatusScheduledEvent(sentStatus))
|
||||
eventHub.dispatch(StatusScheduledEvent)
|
||||
} else if (!isNew) {
|
||||
eventHub.dispatch(StatusEditedEvent(statusToSend.statusId!!, sentStatus))
|
||||
eventHub.dispatch(StatusEditedEvent(statusToSend.statusId!!, sentStatus as Status))
|
||||
} else {
|
||||
eventHub.dispatch(StatusComposedEvent(sentStatus))
|
||||
eventHub.dispatch(StatusComposedEvent(sentStatus as Status))
|
||||
}
|
||||
|
||||
notificationManager.cancel(statusId)
|
||||
}, { throwable ->
|
||||
Timber.w("failed sending status", throwable)
|
||||
Timber.w("failed sending status: $throwable")
|
||||
failOrRetry(throwable, statusId)
|
||||
})
|
||||
stopSelfWhenDone()
|
||||
|
@ -267,12 +277,13 @@ class SendStatusService : Service() {
|
|||
}
|
||||
|
||||
private suspend fun failOrRetry(throwable: Throwable, statusId: Int) {
|
||||
if (throwable is HttpException) {
|
||||
when (throwable) {
|
||||
// the server refused to accept, save status & show error message
|
||||
failSending(statusId)
|
||||
} else {
|
||||
is HttpException -> failSending(statusId)
|
||||
// a network problem occurred, let's retry sending the status
|
||||
retrySending(statusId)
|
||||
is IOException -> retrySending(statusId)
|
||||
// Some other problem, fail
|
||||
else -> failSending(statusId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,11 @@ private fun formatDuration(durationInSeconds: Double): String {
|
|||
fun List<Attachment>.aspectRatios(): List<Double> {
|
||||
return map { attachment ->
|
||||
// clamp ratio between 2:1 & 1:2, defaulting to 16:9
|
||||
val size = (attachment.meta?.small ?: attachment.meta?.original) ?: return@map 1.7778
|
||||
val aspect = if (size.aspect > 0) size.aspect else size.width.toDouble() / size.height
|
||||
aspect.coerceIn(0.5, 2.0)
|
||||
val (width, height, aspect) = (attachment.meta?.small ?: attachment.meta?.original) ?: return@map 1.7778
|
||||
width ?: return@map 1.778
|
||||
height ?: return@map 1.778
|
||||
aspect ?: return@map 1.778
|
||||
val adjustedAspect = if (aspect > 0) aspect else width.toDouble() / height
|
||||
adjustedAspect.coerceIn(0.5, 2.0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import app.pachli.core.network.model.Status
|
|||
import app.pachli.core.network.parseAsMastodonHtml
|
||||
import app.pachli.core.network.replaceCrashingCharacters
|
||||
import app.pachli.util.shouldTrimStatus
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
|
||||
/**
|
||||
* Interface for the data shown when viewing a status, or something that wraps
|
||||
|
@ -273,13 +273,13 @@ data class StatusViewData(
|
|||
|
||||
fun from(
|
||||
timelineStatusWithAccount: TimelineStatusWithAccount,
|
||||
gson: Gson,
|
||||
moshi: Moshi,
|
||||
isExpanded: Boolean,
|
||||
isShowingContent: Boolean,
|
||||
isDetailed: Boolean = false,
|
||||
translationState: TranslationState = TranslationState.SHOW_ORIGINAL,
|
||||
): StatusViewData {
|
||||
val status = timelineStatusWithAccount.toStatus(gson)
|
||||
val status = timelineStatusWithAccount.toStatus(moshi)
|
||||
return StatusViewData(
|
||||
status = status,
|
||||
translation = timelineStatusWithAccount.translatedStatus,
|
||||
|
|
|
@ -2,9 +2,13 @@ package app.pachli
|
|||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.pachli.core.database.model.TranslationState
|
||||
import app.pachli.core.network.json.GuardedAdapter.Companion.GuardedAdapterFactory
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.viewdata.StatusViewData
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||
import java.util.Date
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
|
@ -12,6 +16,9 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StatusComparisonTest {
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.add(GuardedAdapterFactory()).build()
|
||||
|
||||
@Test
|
||||
fun `two equal statuses - should be equal`() {
|
||||
|
@ -36,8 +43,6 @@ class StatusComparisonTest {
|
|||
assertNotEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
||||
}
|
||||
|
||||
private val gson = Gson()
|
||||
|
||||
@Test
|
||||
fun `two equal status view data - should be equal`() {
|
||||
val viewdata1 = StatusViewData(
|
||||
|
@ -95,6 +100,7 @@ class StatusComparisonTest {
|
|||
assertNotEquals(viewdata1, viewdata2)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun createStatus(
|
||||
id: String = "123456",
|
||||
content: String = """
|
||||
|
@ -206,6 +212,6 @@ class StatusComparisonTest {
|
|||
"poll": null
|
||||
}
|
||||
""".trimIndent()
|
||||
return gson.fromJson(statusJson, Status::class.java)
|
||||
return moshi.adapter<Status>().fromJson(statusJson)!!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import androidx.paging.PagingSource
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.pachli.core.network.model.Notification
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
|
@ -39,15 +39,15 @@ class NotificationsPagingSourceTest {
|
|||
@Test
|
||||
fun `load() returns error message on HTTP error`() = runTest {
|
||||
// Given
|
||||
val jsonError = "{error: 'This is an error'}".toResponseBody()
|
||||
val jsonError = """{"error": "This is an error"}""".toResponseBody()
|
||||
val mockApi: MastodonApi = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||
}
|
||||
|
||||
val filter = emptySet<Notification.Type>()
|
||||
val gson = Gson()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
||||
val moshi = Moshi.Builder().build()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, moshi, filter)
|
||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||
|
||||
// When
|
||||
|
@ -65,15 +65,15 @@ class NotificationsPagingSourceTest {
|
|||
@Test
|
||||
fun `load() returns extended error message on HTTP error`() = runTest {
|
||||
// Given
|
||||
val jsonError = "{error: 'This is an error', error_description: 'Description of the error'}".toResponseBody()
|
||||
val jsonError = """{"error": "This is an error", "error_description": "Description of the error"}""".toResponseBody()
|
||||
val mockApi: MastodonApi = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||
}
|
||||
|
||||
val filter = emptySet<Notification.Type>()
|
||||
val gson = Gson()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
||||
val moshi = Moshi.Builder().build()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, moshi, filter)
|
||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||
|
||||
// When
|
||||
|
@ -91,15 +91,15 @@ class NotificationsPagingSourceTest {
|
|||
@Test
|
||||
fun `load() returns default error message on empty HTTP error`() = runTest {
|
||||
// Given
|
||||
val jsonError = "{}".toResponseBody()
|
||||
val jsonError = "".toResponseBody()
|
||||
val mockApi: MastodonApi = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||
}
|
||||
|
||||
val filter = emptySet<Notification.Type>()
|
||||
val gson = Gson()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
||||
val moshi = Moshi.Builder().build()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, moshi, filter)
|
||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||
|
||||
// When
|
||||
|
@ -124,8 +124,8 @@ class NotificationsPagingSourceTest {
|
|||
}
|
||||
|
||||
val filter = emptySet<Notification.Type>()
|
||||
val gson = Gson()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
||||
val moshi = Moshi.Builder().build()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, moshi, filter)
|
||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||
|
||||
// When
|
||||
|
@ -134,7 +134,7 @@ class NotificationsPagingSourceTest {
|
|||
// Then
|
||||
assertTrue(loadResult is PagingSource.LoadResult.Error)
|
||||
assertEquals(
|
||||
"HTTP 429: {'malformedjson} (com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unterminated string at line 1 column 17 path \$.)",
|
||||
"HTTP 429: {'malformedjson} (com.squareup.moshi.JsonEncodingException: Use JsonReader.setLenient(true) to accept malformed JSON at path \$.)",
|
||||
(loadResult as PagingSource.LoadResult.Error).throwable.message,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,9 +20,12 @@ import app.pachli.core.database.model.AccountEntity
|
|||
import app.pachli.core.database.model.RemoteKeyEntity
|
||||
import app.pachli.core.database.model.RemoteKeyKind
|
||||
import app.pachli.core.database.model.TimelineStatusWithAccount
|
||||
import app.pachli.core.network.json.GuardedAdapter.Companion.GuardedAdapterFactory
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
@ -60,12 +63,17 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
private lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory<Int, TimelineStatusWithAccount>
|
||||
|
||||
private val moshi: Moshi = Moshi.Builder()
|
||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.add(GuardedAdapterFactory())
|
||||
.build()
|
||||
|
||||
@Before
|
||||
@ExperimentalCoroutinesApi
|
||||
fun setup() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(Gson()))
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.build()
|
||||
transactionProvider = TransactionProvider(db)
|
||||
|
||||
|
@ -91,7 +99,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
transactionProvider = transactionProvider,
|
||||
timelineDao = db.timelineDao(),
|
||||
remoteKeyDao = db.remoteKeyDao(),
|
||||
gson = Gson(),
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||
|
@ -114,7 +122,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
transactionProvider = transactionProvider,
|
||||
timelineDao = db.timelineDao(),
|
||||
remoteKeyDao = db.remoteKeyDao(),
|
||||
gson = Gson(),
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||
|
@ -134,7 +142,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
transactionProvider = transactionProvider,
|
||||
timelineDao = db.timelineDao(),
|
||||
remoteKeyDao = db.remoteKeyDao(),
|
||||
gson = Gson(),
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
@ -174,7 +182,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
transactionProvider = transactionProvider,
|
||||
timelineDao = db.timelineDao(),
|
||||
remoteKeyDao = db.remoteKeyDao(),
|
||||
gson = Gson(),
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
@ -228,7 +236,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
transactionProvider = transactionProvider,
|
||||
timelineDao = db.timelineDao(),
|
||||
remoteKeyDao = db.remoteKeyDao(),
|
||||
gson = Gson(),
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
@ -289,7 +297,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
transactionProvider = transactionProvider,
|
||||
timelineDao = db.timelineDao(),
|
||||
remoteKeyDao = db.remoteKeyDao(),
|
||||
gson = Gson(),
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
|
@ -35,7 +35,7 @@ import app.pachli.core.testing.rules.MainCoroutineRule
|
|||
import app.pachli.usecase.TimelineCases
|
||||
import app.pachli.util.StatusDisplayOptionsRepository
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.testing.CustomTestApplication
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
|
@ -92,6 +92,9 @@ abstract class CachedTimelineViewModelTestBase {
|
|||
@Inject
|
||||
lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var moshi: Moshi
|
||||
|
||||
protected lateinit var timelineCases: TimelineCases
|
||||
protected lateinit var viewModel: TimelineViewModel
|
||||
|
||||
|
@ -160,7 +163,7 @@ abstract class CachedTimelineViewModelTestBase {
|
|||
accountManager,
|
||||
statusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository,
|
||||
Gson(),
|
||||
moshi,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,12 @@ import app.pachli.core.database.model.TimelineAccountEntity
|
|||
import app.pachli.core.database.model.TimelineStatusEntity
|
||||
import app.pachli.core.database.model.TimelineStatusWithAccount
|
||||
import app.pachli.core.database.model.TranslationState
|
||||
import app.pachli.core.network.json.GuardedAdapter.Companion.GuardedAdapterFactory
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.model.TimelineAccount
|
||||
import app.pachli.viewdata.StatusViewData
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||
import java.util.Date
|
||||
|
||||
private val fixedDate = Date(1638889052000)
|
||||
|
@ -96,18 +98,21 @@ fun mockStatusEntityWithAccount(
|
|||
expanded: Boolean = false,
|
||||
): TimelineStatusWithAccount {
|
||||
val mockedStatus = mockStatus(id)
|
||||
val gson = Gson()
|
||||
val moshi = Moshi.Builder()
|
||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.add(GuardedAdapterFactory())
|
||||
.build()
|
||||
|
||||
return TimelineStatusWithAccount(
|
||||
status = TimelineStatusEntity.from(
|
||||
mockedStatus,
|
||||
timelineUserId = userId,
|
||||
gson = gson,
|
||||
moshi = moshi,
|
||||
),
|
||||
account = TimelineAccountEntity.from(
|
||||
mockedStatus.account,
|
||||
accountId = userId,
|
||||
gson = gson,
|
||||
moshi = moshi,
|
||||
),
|
||||
viewData = StatusViewDataEntity(
|
||||
serverId = id,
|
||||
|
|
|
@ -26,7 +26,7 @@ import app.pachli.core.preferences.SharedPreferencesRepository
|
|||
import app.pachli.usecase.TimelineCases
|
||||
import app.pachli.util.StatusDisplayOptionsRepository
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.testing.BindValue
|
||||
import dagger.hilt.android.testing.CustomTestApplication
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
|
@ -111,7 +111,7 @@ class ViewThreadViewModelTest {
|
|||
lateinit var timelineDao: TimelineDao
|
||||
|
||||
@Inject
|
||||
lateinit var gson: Gson
|
||||
lateinit var moshi: Moshi
|
||||
|
||||
@BindValue @JvmField
|
||||
val filtersRepository: FiltersRepository = mock()
|
||||
|
@ -188,7 +188,7 @@ class ViewThreadViewModelTest {
|
|||
eventHub,
|
||||
accountManager,
|
||||
timelineDao,
|
||||
gson,
|
||||
moshi,
|
||||
cachedTimelineRepository,
|
||||
statusDisplayOptionsRepository,
|
||||
filtersRepository,
|
||||
|
|
|
@ -19,9 +19,9 @@ package app.pachli.di
|
|||
|
||||
import app.pachli.components.compose.MediaUploader
|
||||
import app.pachli.core.network.di.NetworkModule
|
||||
import app.pachli.core.network.json.Rfc3339DateJsonAdapter
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import app.pachli.core.network.json.GuardedAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
@ -39,9 +39,10 @@ import org.mockito.kotlin.mock
|
|||
object FakeNetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesGson(): Gson = GsonBuilder()
|
||||
.registerTypeAdapter(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.create()
|
||||
fun providesMoshi(): Moshi = Moshi.Builder()
|
||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.add(GuardedAdapter.Companion.GuardedAdapterFactory())
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
@ -35,6 +35,8 @@ dependencies {
|
|||
implementation(projects.core.network)
|
||||
implementation(projects.core.preferences)
|
||||
|
||||
// Because of the use of @SerializedName in DraftEntity
|
||||
implementation(libs.gson)
|
||||
// Because of the use of @Json in DraftEntity
|
||||
implementation(libs.moshi)
|
||||
implementation(libs.moshi.adapters)
|
||||
ksp(libs.moshi.codegen)
|
||||
}
|
||||
|
|
|
@ -30,28 +30,29 @@ import app.pachli.core.network.model.Poll
|
|||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.model.TranslatedAttachment
|
||||
import app.pachli.core.network.model.TranslatedPoll
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@ProvidedTypeConverter
|
||||
@Singleton
|
||||
class Converters @Inject constructor(
|
||||
private val gson: Gson,
|
||||
private val moshi: Moshi,
|
||||
) {
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToEmojiList(emojiListJson: String?): List<Emoji>? {
|
||||
return gson.fromJson(emojiListJson, object : TypeToken<List<Emoji>>() {}.type)
|
||||
fun jsonToEmojiList(json: String?): List<Emoji>? {
|
||||
return json?.let { moshi.adapter<List<Emoji>>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun emojiListToJson(emojiList: List<Emoji>?): String {
|
||||
return gson.toJson(emojiList)
|
||||
return moshi.adapter<List<Emoji>?>().toJson(emojiList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
|
@ -81,52 +82,52 @@ class Converters @Inject constructor(
|
|||
|
||||
@TypeConverter
|
||||
fun accountToJson(account: ConversationAccountEntity?): String {
|
||||
return gson.toJson(account)
|
||||
return moshi.adapter<ConversationAccountEntity>().toJson(account)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccount(accountJson: String?): ConversationAccountEntity? {
|
||||
return gson.fromJson(accountJson, ConversationAccountEntity::class.java)
|
||||
return accountJson?.let { moshi.adapter<ConversationAccountEntity>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun accountListToJson(accountList: List<ConversationAccountEntity>?): String {
|
||||
return gson.toJson(accountList)
|
||||
return moshi.adapter<List<ConversationAccountEntity>>().toJson(accountList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccountList(accountListJson: String?): List<ConversationAccountEntity>? {
|
||||
return gson.fromJson(accountListJson, object : TypeToken<List<ConversationAccountEntity>>() {}.type)
|
||||
return accountListJson?.let { moshi.adapter<List<ConversationAccountEntity>?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun attachmentListToJson(attachmentList: List<Attachment>?): String {
|
||||
return gson.toJson(attachmentList)
|
||||
return moshi.adapter<List<Attachment>?>().toJson(attachmentList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAttachmentList(attachmentListJson: String?): List<Attachment>? {
|
||||
return gson.fromJson(attachmentListJson, object : TypeToken<List<Attachment>>() {}.type)
|
||||
return attachmentListJson?.let { moshi.adapter<List<Attachment>?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun mentionListToJson(mentionArray: List<Status.Mention>?): String? {
|
||||
return gson.toJson(mentionArray)
|
||||
return moshi.adapter<List<Status.Mention>?>().toJson(mentionArray)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToMentionArray(mentionListJson: String?): List<Status.Mention>? {
|
||||
return gson.fromJson(mentionListJson, object : TypeToken<List<Status.Mention>>() {}.type)
|
||||
return mentionListJson?.let { moshi.adapter<List<Status.Mention>?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun tagListToJson(tagArray: List<HashTag>?): String? {
|
||||
return gson.toJson(tagArray)
|
||||
return moshi.adapter<List<HashTag>?>().toJson(tagArray)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToTagArray(tagListJson: String?): List<HashTag>? {
|
||||
return gson.fromJson(tagListJson, object : TypeToken<List<HashTag>>() {}.type)
|
||||
return tagListJson?.let { moshi.adapter<List<HashTag>?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
|
@ -141,61 +142,61 @@ class Converters @Inject constructor(
|
|||
|
||||
@TypeConverter
|
||||
fun pollToJson(poll: Poll?): String? {
|
||||
return gson.toJson(poll)
|
||||
return moshi.adapter<Poll?>().toJson(poll)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToPoll(pollJson: String?): Poll? {
|
||||
return gson.fromJson(pollJson, Poll::class.java)
|
||||
return pollJson?.let { moshi.adapter<Poll?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun newPollToJson(newPoll: NewPoll?): String? {
|
||||
return gson.toJson(newPoll)
|
||||
return moshi.adapter<NewPoll?>().toJson(newPoll)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToNewPoll(newPollJson: String?): NewPoll? {
|
||||
return gson.fromJson(newPollJson, NewPoll::class.java)
|
||||
return newPollJson?.let { moshi.adapter<NewPoll?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>?): String? {
|
||||
return gson.toJson(draftAttachments)
|
||||
return moshi.adapter<List<DraftAttachment>?>().toJson(draftAttachments)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List<DraftAttachment>? {
|
||||
return gson.fromJson(draftAttachmentListJson, object : TypeToken<List<DraftAttachment>>() {}.type)
|
||||
return draftAttachmentListJson?.let { moshi.adapter<List<DraftAttachment>?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun filterResultListToJson(filterResults: List<FilterResult>?): String? {
|
||||
return gson.toJson(filterResults)
|
||||
return moshi.adapter<List<FilterResult>?>().toJson(filterResults)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToFilterResultList(filterResultListJson: String?): List<FilterResult>? {
|
||||
return gson.fromJson(filterResultListJson, object : TypeToken<List<FilterResult>>() {}.type)
|
||||
return filterResultListJson?.let { moshi.adapter<List<FilterResult>>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun translatedPolltoJson(translatedPoll: TranslatedPoll?): String? {
|
||||
return gson.toJson(translatedPoll)
|
||||
return moshi.adapter<TranslatedPoll?>().toJson(translatedPoll)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToTranslatedPoll(translatedPollJson: String?): TranslatedPoll? {
|
||||
return gson.fromJson(translatedPollJson, TranslatedPoll::class.java)
|
||||
return translatedPollJson?.let { moshi.adapter<TranslatedPoll?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun translatedAttachmentToJson(translatedAttachment: List<TranslatedAttachment>?): String {
|
||||
return gson.toJson(translatedAttachment)
|
||||
return moshi.adapter<List<TranslatedAttachment>?>().toJson(translatedAttachment)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToTranslatedAttachment(translatedAttachmentJson: String): List<TranslatedAttachment>? {
|
||||
return gson.fromJson(translatedAttachmentJson, object : TypeToken<List<TranslatedAttachment>>() {}.type)
|
||||
return moshi.adapter<List<TranslatedAttachment>?>().fromJson(translatedAttachmentJson)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import app.pachli.core.network.model.HashTag
|
|||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.model.TimelineAccount
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@Entity(primaryKeys = ["id", "accountId"])
|
||||
|
@ -65,6 +66,7 @@ data class ConversationEntity(
|
|||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ConversationAccountEntity(
|
||||
val id: String,
|
||||
val localUsername: String,
|
||||
|
|
|
@ -27,7 +27,8 @@ import app.pachli.core.database.Converters
|
|||
import app.pachli.core.network.model.Attachment
|
||||
import app.pachli.core.network.model.NewPoll
|
||||
import app.pachli.core.network.model.Status
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity
|
||||
|
@ -49,17 +50,13 @@ data class DraftEntity(
|
|||
val statusId: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
* The alternate names are here because we accidentally published versions were DraftAttachment was minified
|
||||
* Tusky 15: uriString = e, description = f, type = g
|
||||
* Tusky 16 beta: uriString = i, description = j, type = k
|
||||
*/
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DraftAttachment(
|
||||
@SerializedName(value = "uriString", alternate = ["e", "i"]) val uriString: String,
|
||||
@SerializedName(value = "description", alternate = ["f", "j"]) val description: String?,
|
||||
@SerializedName(value = "focus") val focus: Attachment.Focus?,
|
||||
@SerializedName(value = "type", alternate = ["g", "k"]) val type: Type,
|
||||
@Json(name = "uriString") val uriString: String,
|
||||
@Json(name = "description") val description: String?,
|
||||
@Json(name = "focus") val focus: Attachment.Focus?,
|
||||
@Json(name = "type") val type: Type,
|
||||
) : Parcelable {
|
||||
val uri: Uri
|
||||
get() = uriString.toUri()
|
||||
|
|
|
@ -32,9 +32,8 @@ import app.pachli.core.network.model.HashTag
|
|||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.model.TimelineAccount
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.lang.reflect.Type
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
|
@ -100,7 +99,8 @@ data class TimelineStatusEntity(
|
|||
val filtered: List<FilterResult>?,
|
||||
) {
|
||||
companion object {
|
||||
fun from(status: Status, timelineUserId: Long, gson: Gson) = TimelineStatusEntity(
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun from(status: Status, timelineUserId: Long, moshi: Moshi) = TimelineStatusEntity(
|
||||
serverId = status.id,
|
||||
url = status.actionableStatus.url,
|
||||
timelineUserId = timelineUserId,
|
||||
|
@ -110,7 +110,7 @@ data class TimelineStatusEntity(
|
|||
content = status.actionableStatus.content,
|
||||
createdAt = status.actionableStatus.createdAt.time,
|
||||
editedAt = status.actionableStatus.editedAt?.time,
|
||||
emojis = status.actionableStatus.emojis.let(gson::toJson),
|
||||
emojis = moshi.adapter<List<Emoji>>().toJson(status.actionableStatus.emojis),
|
||||
reblogsCount = status.actionableStatus.reblogsCount,
|
||||
favouritesCount = status.actionableStatus.favouritesCount,
|
||||
reblogged = status.actionableStatus.reblogged,
|
||||
|
@ -119,16 +119,16 @@ data class TimelineStatusEntity(
|
|||
sensitive = status.actionableStatus.sensitive,
|
||||
spoilerText = status.actionableStatus.spoilerText,
|
||||
visibility = status.actionableStatus.visibility,
|
||||
attachments = status.actionableStatus.attachments.let(gson::toJson),
|
||||
mentions = status.actionableStatus.mentions.let(gson::toJson),
|
||||
tags = status.actionableStatus.tags.let(gson::toJson),
|
||||
application = status.actionableStatus.application.let(gson::toJson),
|
||||
attachments = moshi.adapter<List<Attachment>>().toJson(status.actionableStatus.attachments),
|
||||
mentions = moshi.adapter<List<Status.Mention>>().toJson(status.actionableStatus.mentions),
|
||||
tags = moshi.adapter<List<HashTag>>().toJson(status.actionableStatus.tags),
|
||||
application = moshi.adapter<Status.Application?>().toJson(status.actionableStatus.application),
|
||||
reblogServerId = status.reblog?.id,
|
||||
reblogAccountId = status.reblog?.let { status.account.id },
|
||||
poll = status.actionableStatus.poll.let(gson::toJson),
|
||||
poll = moshi.adapter<Poll>().toJson(status.actionableStatus.poll),
|
||||
muted = status.actionableStatus.muted,
|
||||
pinned = status.actionableStatus.pinned == true,
|
||||
card = status.actionableStatus.card?.let(gson::toJson),
|
||||
card = moshi.adapter<Card?>().toJson(status.actionableStatus.card),
|
||||
repliesCount = status.actionableStatus.repliesCount,
|
||||
language = status.actionableStatus.language,
|
||||
filtered = status.actionableStatus.filtered,
|
||||
|
@ -150,7 +150,8 @@ data class TimelineAccountEntity(
|
|||
val emojis: String,
|
||||
val bot: Boolean,
|
||||
) {
|
||||
fun toTimelineAccount(gson: Gson): TimelineAccount {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun toTimelineAccount(moshi: Moshi): TimelineAccount {
|
||||
return TimelineAccount(
|
||||
id = serverId,
|
||||
localUsername = localUsername,
|
||||
|
@ -160,12 +161,13 @@ data class TimelineAccountEntity(
|
|||
url = url,
|
||||
avatar = avatar,
|
||||
bot = bot,
|
||||
emojis = gson.fromJson(emojis, emojisListType),
|
||||
emojis = moshi.adapter<List<Emoji>>().fromJson(emojis),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(timelineAccount: TimelineAccount, accountId: Long, gson: Gson) = TimelineAccountEntity(
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun from(timelineAccount: TimelineAccount, accountId: Long, moshi: Moshi) = TimelineAccountEntity(
|
||||
serverId = timelineAccount.id,
|
||||
timelineUserId = accountId,
|
||||
localUsername = timelineAccount.localUsername,
|
||||
|
@ -173,7 +175,7 @@ data class TimelineAccountEntity(
|
|||
displayName = timelineAccount.name,
|
||||
url = timelineAccount.url,
|
||||
avatar = timelineAccount.avatar,
|
||||
emojis = gson.toJson(timelineAccount.emojis),
|
||||
emojis = moshi.adapter<List<Emoji>>().toJson(timelineAccount.emojis),
|
||||
bot = timelineAccount.bot,
|
||||
)
|
||||
}
|
||||
|
@ -214,11 +216,6 @@ data class StatusViewDataEntity(
|
|||
val translationState: TranslationState,
|
||||
)
|
||||
|
||||
val attachmentArrayListType: Type = object : TypeToken<ArrayList<Attachment>>() {}.type
|
||||
val emojisListType: Type = object : TypeToken<List<Emoji>>() {}.type
|
||||
val mentionListType: Type = object : TypeToken<List<Status.Mention>>() {}.type
|
||||
val tagListType: Type = object : TypeToken<List<HashTag>>() {}.type
|
||||
|
||||
data class TimelineStatusWithAccount(
|
||||
@Embedded
|
||||
val status: TimelineStatusEntity,
|
||||
|
@ -232,32 +229,28 @@ data class TimelineStatusWithAccount(
|
|||
@Embedded(prefix = "t_")
|
||||
val translatedStatus: TranslatedStatusEntity? = null,
|
||||
) {
|
||||
fun toStatus(gson: Gson): Status {
|
||||
val attachments: ArrayList<Attachment> = gson.fromJson(
|
||||
status.attachments,
|
||||
attachmentArrayListType,
|
||||
) ?: arrayListOf()
|
||||
val mentions: List<Status.Mention> = gson.fromJson(
|
||||
status.mentions,
|
||||
mentionListType,
|
||||
) ?: emptyList()
|
||||
val tags: List<HashTag>? = gson.fromJson(
|
||||
status.tags,
|
||||
tagListType,
|
||||
)
|
||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||
val emojis: List<Emoji> = gson.fromJson(
|
||||
status.emojis,
|
||||
emojisListType,
|
||||
) ?: emptyList()
|
||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||
val card: Card? = gson.fromJson(status.card, Card::class.java)
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun toStatus(moshi: Moshi): Status {
|
||||
val attachments: List<Attachment> = status.attachments?.let {
|
||||
moshi.adapter<List<Attachment>?>().fromJson(it)
|
||||
} ?: emptyList()
|
||||
val mentions: List<Status.Mention> = status.mentions?.let {
|
||||
moshi.adapter<List<Status.Mention>?>().fromJson(it)
|
||||
} ?: emptyList()
|
||||
val tags: List<HashTag>? = status.tags?.let {
|
||||
moshi.adapter<List<HashTag>?>().fromJson(it)
|
||||
}
|
||||
val application = status.application?.let { moshi.adapter<Status.Application>().fromJson(it) }
|
||||
val emojis: List<Emoji> = status.emojis?.let { moshi.adapter<List<Emoji>?>().fromJson(it) }
|
||||
?: emptyList()
|
||||
val poll: Poll? = status.poll?.let { moshi.adapter<Poll?>().fromJson(it) }
|
||||
val card: Card? = status.card?.let { moshi.adapter<Card?>().fromJson(it) }
|
||||
|
||||
val reblog = status.reblogServerId?.let { id ->
|
||||
Status(
|
||||
id = id,
|
||||
url = status.url,
|
||||
account = account.toTimelineAccount(gson),
|
||||
account = account.toTimelineAccount(moshi),
|
||||
inReplyToId = status.inReplyToId,
|
||||
inReplyToAccountId = status.inReplyToAccountId,
|
||||
reblog = null,
|
||||
|
@ -291,7 +284,7 @@ data class TimelineStatusWithAccount(
|
|||
id = status.serverId,
|
||||
// no url for reblogs
|
||||
url = null,
|
||||
account = reblogAccount!!.toTimelineAccount(gson),
|
||||
account = reblogAccount!!.toTimelineAccount(moshi),
|
||||
inReplyToId = null,
|
||||
inReplyToAccountId = null,
|
||||
reblog = reblog,
|
||||
|
@ -308,7 +301,7 @@ data class TimelineStatusWithAccount(
|
|||
sensitive = false,
|
||||
spoilerText = "",
|
||||
visibility = status.visibility,
|
||||
attachments = ArrayList(),
|
||||
attachments = listOf(),
|
||||
mentions = listOf(),
|
||||
tags = listOf(),
|
||||
application = null,
|
||||
|
@ -324,7 +317,7 @@ data class TimelineStatusWithAccount(
|
|||
Status(
|
||||
id = status.serverId,
|
||||
url = status.url,
|
||||
account = account.toTimelineAccount(gson),
|
||||
account = account.toTimelineAccount(moshi),
|
||||
inReplyToId = status.inReplyToId,
|
||||
inReplyToAccountId = status.inReplyToAccountId,
|
||||
reblog = null,
|
||||
|
|
|
@ -21,7 +21,7 @@ import androidx.room.Room
|
|||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import app.pachli.core.database.AppDatabase
|
||||
import app.pachli.core.database.Converters
|
||||
import com.google.gson.Gson
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
@ -36,10 +36,10 @@ import javax.inject.Singleton
|
|||
object FakeDatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDatabase(gson: Gson): AppDatabase {
|
||||
fun providesDatabase(moshi: Moshi): AppDatabase {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
return Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(gson))
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ class ComposeActivityIntent(context: Context) : Intent() {
|
|||
/** Editing a status started as an existing draft */
|
||||
EDIT_DRAFT,
|
||||
|
||||
/** Editing an an existing scheduled status */
|
||||
/** Editing an existing scheduled status */
|
||||
EDIT_SCHEDULED,
|
||||
}
|
||||
|
||||
|
@ -210,7 +210,7 @@ class EditFilterActivityIntent(context: Context, filter: Filter? = null) : Inten
|
|||
/**
|
||||
* @param context
|
||||
* @param loginMode See [LoginMode]
|
||||
* @see [app.pachli.components.login.LoginActivity]
|
||||
* @see [app.pachli.feature.login.LoginActivity]
|
||||
*/
|
||||
class LoginActivityIntent(context: Context, loginMode: LoginMode = LoginMode.DEFAULT) : Intent() {
|
||||
/** How to log in */
|
||||
|
@ -472,7 +472,7 @@ class ViewMediaActivityIntent private constructor(context: Context) : Intent() {
|
|||
private const val EXTRA_SINGLE_IMAGE_URL = "singleImage"
|
||||
|
||||
/** @return the list of [AttachmentViewData] passed in this intent, or null */
|
||||
fun getAttachments(intent: Intent): ArrayList<AttachmentViewData>? = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java)
|
||||
fun getAttachments(intent: Intent): List<AttachmentViewData>? = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java)
|
||||
|
||||
/** @return the index of the attachment to show, or 0 */
|
||||
fun getAttachmentIndex(intent: Intent) = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
|
||||
|
|
|
@ -33,7 +33,10 @@ dependencies {
|
|||
implementation(projects.core.common)
|
||||
implementation(projects.core.preferences)
|
||||
|
||||
implementation(libs.gson)
|
||||
implementation(libs.moshi)
|
||||
implementation(libs.moshi.adapters)
|
||||
ksp(libs.moshi.codegen)
|
||||
|
||||
implementation(libs.bundles.retrofit)
|
||||
implementation(libs.bundles.okhttp)
|
||||
api(libs.networkresult.calladapter)
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.os.Build
|
|||
import app.pachli.core.common.util.versionName
|
||||
import app.pachli.core.mastodon.model.MediaUploadApi
|
||||
import app.pachli.core.network.BuildConfig
|
||||
import app.pachli.core.network.json.Rfc3339DateJsonAdapter
|
||||
import app.pachli.core.network.json.GuardedAdapter.Companion.GuardedAdapterFactory
|
||||
import app.pachli.core.network.retrofit.InstanceSwitchAuthInterceptor
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import app.pachli.core.preferences.PrefKeys.HTTP_PROXY_ENABLED
|
||||
|
@ -32,8 +32,8 @@ import app.pachli.core.preferences.ProxyConfiguration
|
|||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import app.pachli.core.preferences.getNonNullString
|
||||
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -50,7 +50,7 @@ import okhttp3.OkHttp
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.create
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -60,9 +60,10 @@ object NetworkModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesGson(): Gson = GsonBuilder()
|
||||
.registerTypeAdapter(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.create()
|
||||
fun providesMoshi(): Moshi = Moshi.Builder()
|
||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.add(GuardedAdapterFactory())
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
@ -116,11 +117,11 @@ object NetworkModule {
|
|||
@Singleton
|
||||
fun providesRetrofit(
|
||||
httpClient: OkHttpClient,
|
||||
gson: Gson,
|
||||
moshi: Moshi,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
|
||||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright 2024 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.core.network.json
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonQualifier
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Deserialize this field as the given type, or null if the field value is not
|
||||
* this type.
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@JsonQualifier
|
||||
annotation class Guarded
|
||||
|
||||
/**
|
||||
* Parse the given field as either the delegate type, or null if it does not parse
|
||||
* as that type.
|
||||
*/
|
||||
class GuardedAdapter(private val delegate: JsonAdapter<*>) : JsonAdapter<Any>() {
|
||||
override fun fromJson(reader: JsonReader): Any? {
|
||||
val peeked = reader.peekJson()
|
||||
val result = try {
|
||||
delegate.fromJson(peeked)
|
||||
} catch (_: JsonDataException) {
|
||||
null
|
||||
} finally {
|
||||
peeked.close()
|
||||
}
|
||||
reader.skipValue()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: Any?) {
|
||||
throw UnsupportedOperationException("@Guarded is only used to desererialize objects")
|
||||
}
|
||||
|
||||
companion object {
|
||||
class GuardedAdapterFactory : Factory {
|
||||
override fun create(
|
||||
type: Type,
|
||||
annotations: MutableSet<out Annotation>,
|
||||
moshi: Moshi,
|
||||
): JsonAdapter<*>? {
|
||||
val delegateAnnotations = Types.nextAnnotations(
|
||||
annotations,
|
||||
Guarded::class.java,
|
||||
) ?: return null
|
||||
val delegate = moshi.nextAdapter<Any>(
|
||||
this,
|
||||
type,
|
||||
delegateAnnotations,
|
||||
)
|
||||
return GuardedAdapter(delegate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/* Copyright 2022 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.core.network.json
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class GuardedBooleanAdapter : JsonDeserializer<Boolean?> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? {
|
||||
return if (json.isJsonObject) {
|
||||
null
|
||||
} else {
|
||||
json.asBoolean
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,327 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.core.network.json
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011 FasterXML, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.google.gson.JsonParseException
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
/*
|
||||
* Jackson’s date formatter, pruned to Moshi's needs. Forked from this file:
|
||||
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
||||
*
|
||||
* Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC
|
||||
* friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date
|
||||
* objects.
|
||||
*
|
||||
* Supported parse format:
|
||||
* `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]`
|
||||
*
|
||||
* @see [this specification](http://www.w3.org/TR/NOTE-datetime)
|
||||
*/
|
||||
|
||||
/** ID to represent the 'GMT' string */
|
||||
private const val GMT_ID = "GMT"
|
||||
|
||||
/** The GMT timezone, prefetched to avoid more lookups. */
|
||||
private val TIMEZONE_Z: TimeZone = TimeZone.getTimeZone(GMT_ID)
|
||||
|
||||
/** Returns `date` formatted as yyyy-MM-ddThh:mm:ss.sssZ */
|
||||
internal fun Date.formatIsoDate(): String {
|
||||
val calendar: Calendar = GregorianCalendar(TIMEZONE_Z, Locale.US)
|
||||
calendar.time = this
|
||||
|
||||
// estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
|
||||
val capacity = "yyyy-MM-ddThh:mm:ss.sssZ".length
|
||||
val formatted = StringBuilder(capacity)
|
||||
padInt(formatted, calendar[Calendar.YEAR], "yyyy".length)
|
||||
formatted.append('-')
|
||||
padInt(formatted, calendar[Calendar.MONTH] + 1, "MM".length)
|
||||
formatted.append('-')
|
||||
padInt(formatted, calendar[Calendar.DAY_OF_MONTH], "dd".length)
|
||||
formatted.append('T')
|
||||
padInt(formatted, calendar[Calendar.HOUR_OF_DAY], "hh".length)
|
||||
formatted.append(':')
|
||||
padInt(formatted, calendar[Calendar.MINUTE], "mm".length)
|
||||
formatted.append(':')
|
||||
padInt(formatted, calendar[Calendar.SECOND], "ss".length)
|
||||
formatted.append('.')
|
||||
padInt(formatted, calendar[Calendar.MILLISECOND], "sss".length)
|
||||
formatted.append('Z')
|
||||
return formatted.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date from ISO-8601 formatted string. It expects a format
|
||||
* `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]`
|
||||
*
|
||||
* @receiver ISO string to parse in the appropriate format.
|
||||
* @return the parsed date
|
||||
*/
|
||||
internal fun String.parseIsoDate(): Date {
|
||||
return try {
|
||||
var offset = 0
|
||||
|
||||
// extract year
|
||||
val year = parseInt(
|
||||
this,
|
||||
offset,
|
||||
4.let {
|
||||
offset += it
|
||||
offset
|
||||
},
|
||||
)
|
||||
if (checkOffset(this, offset, '-')) {
|
||||
offset += 1
|
||||
}
|
||||
|
||||
// extract month
|
||||
val month = parseInt(
|
||||
this,
|
||||
offset,
|
||||
2.let {
|
||||
offset += it
|
||||
offset
|
||||
},
|
||||
)
|
||||
if (checkOffset(this, offset, '-')) {
|
||||
offset += 1
|
||||
}
|
||||
|
||||
// extract day
|
||||
val day = parseInt(
|
||||
this,
|
||||
offset,
|
||||
2.let {
|
||||
offset += it
|
||||
offset
|
||||
},
|
||||
)
|
||||
// default time value
|
||||
var hour = 0
|
||||
var minutes = 0
|
||||
var seconds = 0
|
||||
// always use 0 otherwise returned date will include millis of current time
|
||||
var milliseconds = 0
|
||||
|
||||
// if the value has no time component (and no time zone), we are done
|
||||
val hasT = checkOffset(this, offset, 'T')
|
||||
if (!hasT && this.length <= offset) {
|
||||
return GregorianCalendar(year, month - 1, day).time
|
||||
}
|
||||
if (hasT) {
|
||||
// extract hours, minutes, seconds and milliseconds
|
||||
hour = parseInt(
|
||||
this,
|
||||
1.let {
|
||||
offset += it
|
||||
offset
|
||||
},
|
||||
2.let {
|
||||
offset += it
|
||||
offset
|
||||
},
|
||||
)
|
||||
if (checkOffset(this, offset, ':')) {
|
||||
offset += 1
|
||||
}
|
||||
minutes = parseInt(
|
||||
this, offset,
|
||||
2.let {
|
||||
offset += it
|
||||
offset
|
||||
},
|
||||
)
|
||||
if (checkOffset(this, offset, ':')) {
|
||||
offset += 1
|
||||
}
|
||||
// second and milliseconds can be optional
|
||||
if (this.length > offset) {
|
||||
val c = this[offset]
|
||||
if (c != 'Z' && c != '+' && c != '-') {
|
||||
seconds = parseInt(
|
||||
this, offset,
|
||||
2.let {
|
||||
offset += it
|
||||
offset
|
||||
},
|
||||
)
|
||||
if (seconds in 60..62) seconds = 59 // truncate up to 3 leap seconds
|
||||
// milliseconds can be optional in the format
|
||||
if (checkOffset(this, offset, '.')) {
|
||||
offset += 1
|
||||
val endOffset = indexOfNonDigit(this, offset + 1) // assume at least one digit
|
||||
val parseEndOffset = min(endOffset, offset + 3) // parse up to 3 digits
|
||||
val fraction = parseInt(this, offset, parseEndOffset)
|
||||
milliseconds =
|
||||
(10.0.pow((3 - (parseEndOffset - offset)).toDouble()) * fraction).toInt()
|
||||
offset = endOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract timezone
|
||||
require(this.length > offset) { "No time zone indicator" }
|
||||
val timezone: TimeZone
|
||||
val timezoneIndicator = this[offset]
|
||||
if (timezoneIndicator == 'Z') {
|
||||
timezone = TIMEZONE_Z
|
||||
} else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
|
||||
val timezoneOffset = this.substring(offset)
|
||||
// 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
|
||||
if ("+0000" == timezoneOffset || "+00:00" == timezoneOffset) {
|
||||
timezone = TIMEZONE_Z
|
||||
} else {
|
||||
// 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC...
|
||||
// not sure why, but it is what it is.
|
||||
val timezoneId = GMT_ID + timezoneOffset
|
||||
timezone = TimeZone.getTimeZone(timezoneId)
|
||||
val act = timezone.id
|
||||
if (act != timezoneId) {
|
||||
/*
|
||||
* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given
|
||||
* one without. If so, don't sweat.
|
||||
* Yes, very inefficient. Hopefully not hit often.
|
||||
* If it becomes a perf problem, add 'loose' comparison instead.
|
||||
*/
|
||||
val cleaned = act.replace(":", "")
|
||||
if (cleaned != timezoneId) {
|
||||
throw IndexOutOfBoundsException(
|
||||
"Mismatching time zone indicator: $timezoneId given, resolves to ${timezone.id}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw IndexOutOfBoundsException(
|
||||
"Invalid time zone indicator '$timezoneIndicator'",
|
||||
)
|
||||
}
|
||||
val calendar: Calendar = GregorianCalendar(timezone)
|
||||
calendar.isLenient = false
|
||||
calendar[Calendar.YEAR] = year
|
||||
calendar[Calendar.MONTH] = month - 1
|
||||
calendar[Calendar.DAY_OF_MONTH] = day
|
||||
calendar[Calendar.HOUR_OF_DAY] = hour
|
||||
calendar[Calendar.MINUTE] = minutes
|
||||
calendar[Calendar.SECOND] = seconds
|
||||
calendar[Calendar.MILLISECOND] = milliseconds
|
||||
calendar.time
|
||||
// If we get a ParseException it'll already have the right message/offset.
|
||||
// Other exception types can convert here.
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
throw JsonParseException("Not an RFC 3339 date: $this", e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw JsonParseException("Not an RFC 3339 date: $this", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the expected character exist at the given offset in the value.
|
||||
*
|
||||
* @param value the string to check at the specified offset
|
||||
* @param offset the offset to look for the expected character
|
||||
* @param expected the expected character
|
||||
* @return true if the expected character exist at the given offset
|
||||
*/
|
||||
private fun checkOffset(value: String, offset: Int, expected: Char): Boolean {
|
||||
return offset < value.length && value[offset] == expected
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an integer located between 2 given offsets in a string
|
||||
*
|
||||
* @param value the string to parse
|
||||
* @param beginIndex the start index for the integer in the string
|
||||
* @param endIndex the end index for the integer in the string
|
||||
* @return the int
|
||||
* @throws NumberFormatException if the value is not a number
|
||||
*/
|
||||
private fun parseInt(value: String, beginIndex: Int, endIndex: Int): Int {
|
||||
if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
|
||||
throw NumberFormatException(value)
|
||||
}
|
||||
// use same logic as in Integer.parseInt() but less generic we're not supporting negative values
|
||||
var i = beginIndex
|
||||
var result = 0
|
||||
var digit: Int
|
||||
if (i < endIndex) {
|
||||
digit = Character.digit(value[i++], 10)
|
||||
if (digit < 0) {
|
||||
throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex))
|
||||
}
|
||||
result = -digit
|
||||
}
|
||||
while (i < endIndex) {
|
||||
digit = Character.digit(value[i++], 10)
|
||||
if (digit < 0) {
|
||||
throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex))
|
||||
}
|
||||
result *= 10
|
||||
result -= digit
|
||||
}
|
||||
return -result
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero pad a number to a specified length
|
||||
*
|
||||
* @param buffer buffer to use for padding
|
||||
* @param value the integer value to pad if necessary.
|
||||
* @param length the length of the string we should zero pad
|
||||
*/
|
||||
private fun padInt(buffer: StringBuilder, value: Int, length: Int) {
|
||||
val strValue = value.toString()
|
||||
for (i in length - strValue.length downTo 1) {
|
||||
buffer.append('0')
|
||||
}
|
||||
buffer.append(strValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the first character in the string that is not a digit, starting at offset.
|
||||
*/
|
||||
private fun indexOfNonDigit(string: String, offset: Int): Int {
|
||||
for (i in offset until string.length) {
|
||||
val c = string[i]
|
||||
if (c < '0' || c > '9') return i
|
||||
}
|
||||
return string.length
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
// https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
|
||||
/*
|
||||
* Copyright (C) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package app.pachli.core.network.json
|
||||
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import timber.log.Timber
|
||||
|
||||
class Rfc3339DateJsonAdapter : TypeAdapter<Date?>() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(writer: JsonWriter, date: Date?) {
|
||||
if (date == null) {
|
||||
writer.nullValue()
|
||||
} else {
|
||||
writer.value(date.formatIsoDate())
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): Date? {
|
||||
return when (reader.peek()) {
|
||||
JsonToken.NULL -> {
|
||||
reader.nextNull()
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
try {
|
||||
reader.nextString().parseIsoDate()
|
||||
} catch (jpe: JsonParseException) {
|
||||
Timber.w(jpe)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,8 +17,10 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccessToken(
|
||||
@SerializedName("access_token") val accessToken: String,
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
)
|
||||
|
|
|
@ -16,25 +16,27 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Account(
|
||||
val id: String,
|
||||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String,
|
||||
@Json(name = "acct") val username: String,
|
||||
// should never be null per API definition, but some servers break the contract
|
||||
@SerializedName("display_name") val displayName: String?,
|
||||
@Json(name = "display_name") val displayName: String?,
|
||||
// should never be null per API definition, but some servers break the contract
|
||||
@SerializedName("created_at") val createdAt: Date?,
|
||||
@Json(name = "created_at") val createdAt: Date?,
|
||||
val note: String,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
val header: String,
|
||||
val locked: Boolean = false,
|
||||
@SerializedName("followers_count") val followersCount: Int = 0,
|
||||
@SerializedName("following_count") val followingCount: Int = 0,
|
||||
@SerializedName("statuses_count") val statusesCount: Int = 0,
|
||||
@Json(name = "followers_count") val followersCount: Int = 0,
|
||||
@Json(name = "following_count") val followingCount: Int = 0,
|
||||
@Json(name = "statuses_count") val statusesCount: Int = 0,
|
||||
val source: AccountSource? = null,
|
||||
val bot: Boolean = false,
|
||||
// nullable for backward compatibility
|
||||
|
@ -55,6 +57,7 @@ data class Account(
|
|||
fun isRemote(): Boolean = this.username != this.localUsername
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccountSource(
|
||||
val privacy: Status.Visibility?,
|
||||
val sensitive: Boolean?,
|
||||
|
@ -63,18 +66,21 @@ data class AccountSource(
|
|||
val language: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Field(
|
||||
val name: String,
|
||||
val value: String,
|
||||
@SerializedName("verified_at") val verifiedAt: Date?,
|
||||
@Json(name = "verified_at") val verifiedAt: Date?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StringField(
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
|
||||
/** [Mastodon Entities: Role](https://docs.joinmastodon.org/entities/Role) */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Role(
|
||||
/** Displayable name of the role */
|
||||
val name: String,
|
||||
|
|
|
@ -16,17 +16,19 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Announcement(
|
||||
val id: String,
|
||||
val content: String,
|
||||
@SerializedName("starts_at") val startsAt: Date?,
|
||||
@SerializedName("ends_at") val endsAt: Date?,
|
||||
@SerializedName("all_day") val allDay: Boolean,
|
||||
@SerializedName("published_at") val publishedAt: Date,
|
||||
@SerializedName("updated_at") val updatedAt: Date,
|
||||
@Json(name = "starts_at") val startsAt: Date?,
|
||||
@Json(name = "ends_at") val endsAt: Date?,
|
||||
@Json(name = "all_day") val allDay: Boolean,
|
||||
@Json(name = "published_at") val publishedAt: Date,
|
||||
@Json(name = "updated_at") val updatedAt: Date,
|
||||
val read: Boolean,
|
||||
val mentions: List<Status.Mention>,
|
||||
val statuses: List<Status>,
|
||||
|
@ -47,11 +49,12 @@ data class Announcement(
|
|||
return id.hashCode()
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Reaction(
|
||||
val name: String,
|
||||
val count: Int,
|
||||
val me: Boolean,
|
||||
val url: String?,
|
||||
@SerializedName("static_url") val staticUrl: String?,
|
||||
@Json(name = "static_url") val staticUrl: String?,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AppCredentials(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("client_secret") val clientSecret: String,
|
||||
@Json(name = "client_id") val clientId: String,
|
||||
@Json(name = "client_secret") val clientSecret: String,
|
||||
)
|
||||
|
|
|
@ -18,61 +18,45 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Attachment(
|
||||
val id: String,
|
||||
val url: String,
|
||||
// can be null for e.g. audio attachments
|
||||
@SerializedName("preview_url") val previewUrl: String?,
|
||||
@Json(name = "preview_url") val previewUrl: String?,
|
||||
val meta: MetaData?,
|
||||
val type: Type,
|
||||
val description: String?,
|
||||
val blurhash: String?,
|
||||
) : Parcelable {
|
||||
|
||||
@JsonAdapter(MediaTypeDeserializer::class)
|
||||
enum class Type {
|
||||
@SerializedName("image")
|
||||
@Json(name = "image")
|
||||
IMAGE,
|
||||
|
||||
@SerializedName("gifv")
|
||||
@Json(name = "gifv")
|
||||
GIFV,
|
||||
|
||||
@SerializedName("video")
|
||||
@Json(name = "video")
|
||||
VIDEO,
|
||||
|
||||
@SerializedName("audio")
|
||||
@Json(name = "audio")
|
||||
AUDIO,
|
||||
|
||||
@SerializedName("unknown")
|
||||
@Json(name = "unknown")
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
class MediaTypeDeserializer : JsonDeserializer<Type> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, classOfT: java.lang.reflect.Type, context: JsonDeserializationContext): Type {
|
||||
return when (json.toString()) {
|
||||
"\"image\"" -> Type.IMAGE
|
||||
"\"gifv\"" -> Type.GIFV
|
||||
"\"video\"" -> Type.VIDEO
|
||||
"\"audio\"" -> Type.AUDIO
|
||||
else -> Type.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The meta data of an [Attachment].
|
||||
*/
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MetaData(
|
||||
val focus: Focus?,
|
||||
val duration: Float?,
|
||||
|
@ -87,6 +71,7 @@ data class Attachment(
|
|||
* https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
||||
*/
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Focus(
|
||||
val x: Float,
|
||||
val y: Float,
|
||||
|
@ -98,9 +83,21 @@ data class Attachment(
|
|||
* The size of an image, used to specify the width/height.
|
||||
*/
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Size(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val aspect: Double,
|
||||
) : Parcelable
|
||||
val width: Int?,
|
||||
val height: Int?,
|
||||
// Not always present, see https://github.com/mastodon/mastodon/issues/29125
|
||||
@Json(name = "aspect")
|
||||
val _aspect: Double?,
|
||||
) : Parcelable {
|
||||
val aspect: Double
|
||||
get() {
|
||||
if (_aspect != null) return _aspect
|
||||
width ?: return 1.778
|
||||
height ?: return 1.778
|
||||
|
||||
return (width / height).toDouble()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,22 +17,24 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Card(
|
||||
override val url: String,
|
||||
override val title: String,
|
||||
override val description: String,
|
||||
@SerializedName("type") override val kind: PreviewCardKind,
|
||||
@SerializedName("author_name") override val authorName: String,
|
||||
@SerializedName("author_url") override val authorUrl: String,
|
||||
@SerializedName("provider_name") override val providerName: String,
|
||||
@SerializedName("provider_url") override val providerUrl: String,
|
||||
@Json(name = "type") override val kind: PreviewCardKind,
|
||||
@Json(name = "author_name") override val authorName: String,
|
||||
@Json(name = "author_url") override val authorUrl: String,
|
||||
@Json(name = "provider_name") override val providerName: String,
|
||||
@Json(name = "provider_url") override val providerUrl: String,
|
||||
override val html: String,
|
||||
override val width: Int,
|
||||
override val height: Int,
|
||||
override val image: String? = null,
|
||||
@SerializedName("embed_url") override val embedUrl: String,
|
||||
@Json(name = "embed_url") override val embedUrl: String,
|
||||
override val blurhash: String? = null,
|
||||
) : PreviewCard {
|
||||
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Conversation(
|
||||
val id: String,
|
||||
val accounts: List<TimelineAccount>,
|
||||
// should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
|
||||
@SerializedName("last_status") val lastStatus: Status?,
|
||||
@Json(name = "last_status") val lastStatus: Status?,
|
||||
val unread: Boolean,
|
||||
)
|
||||
|
|
|
@ -16,18 +16,20 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DeletedStatus(
|
||||
val text: String?,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String?,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val visibility: Status.Visibility,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("media_attachments") val attachments: List<Attachment>?,
|
||||
@Json(name = "media_attachments") val attachments: List<Attachment>?,
|
||||
val poll: Poll?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
val language: String?,
|
||||
) {
|
||||
fun isEmpty(): Boolean {
|
||||
|
|
|
@ -17,13 +17,15 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Emoji(
|
||||
val shortcode: String,
|
||||
val url: String,
|
||||
@SerializedName("static_url") val staticUrl: String,
|
||||
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?,
|
||||
@Json(name = "static_url") val staticUrl: String,
|
||||
@Json(name = "visible_in_picker") val visibleInPicker: Boolean?,
|
||||
) : Parcelable
|
||||
|
|
|
@ -17,8 +17,13 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** @see [Error](https://docs.joinmastodon.org/entities/Error/) */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Error(
|
||||
val error: String,
|
||||
val error_description: String?,
|
||||
@Json(name = "error_description")
|
||||
val errorDescription: String?,
|
||||
)
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Filter(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val context: List<String>,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@SerializedName("filter_action") private val filterAction: String,
|
||||
val keywords: List<FilterKeyword>,
|
||||
@Json(name = "expires_at") val expiresAt: Date?,
|
||||
@Json(name = "filter_action") val filterAction: String,
|
||||
// This should not normally be empty. However, Mastodon does not include
|
||||
// this in a status' `filtered.filter` property (it's not null or empty,
|
||||
// it's missing) which breaks deserialisation. Patch this by ensuring it's
|
||||
// always initialised to an empty list.
|
||||
// TODO: https://github.com/mastodon/mastodon/issues/29142
|
||||
val keywords: List<FilterKeyword> = emptyList(),
|
||||
// val statuses: List<FilterStatus>,
|
||||
) : Parcelable {
|
||||
enum class Action(val action: String) {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FilterKeyword(
|
||||
val id: String,
|
||||
val keyword: String,
|
||||
@SerializedName("whole_word") val wholeWord: Boolean,
|
||||
@Json(name = "whole_word") val wholeWord: Boolean,
|
||||
) : Parcelable
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FilterResult(
|
||||
val filter: Filter,
|
||||
@SerializedName("keyword_matches") val keywordMatches: List<String>?,
|
||||
@SerializedName("status_matches") val statusMatches: List<String>?,
|
||||
@Json(name = "keyword_matches") val keywordMatches: List<String>?,
|
||||
@Json(name = "status_matches") val statusMatches: List<String>?,
|
||||
)
|
||||
|
|
|
@ -16,16 +16,18 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FilterV1(
|
||||
val id: String,
|
||||
val phrase: String,
|
||||
val context: List<String>,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@Json(name = "expires_at") val expiresAt: Date?,
|
||||
val irreversible: Boolean,
|
||||
@SerializedName("whole_word") val wholeWord: Boolean,
|
||||
@Json(name = "whole_word") val wholeWord: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
const val HOME = "home"
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class HashTag(val name: String, val url: String, val following: Boolean? = null)
|
||||
|
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** https://docs.joinmastodon.org/entities/V1_Instance/ */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV1(
|
||||
val uri: String,
|
||||
// val title: String,
|
||||
|
@ -29,13 +31,13 @@ data class InstanceV1(
|
|||
// val stats: Map<String, Int>?,
|
||||
// val thumbnail: String?,
|
||||
// val languages: List<String>,
|
||||
// @SerializedName("contact_account") val contactAccount: Account,
|
||||
@SerializedName("max_toot_chars") val maxTootChars: Int?,
|
||||
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?,
|
||||
// @Json(name = "contact_account") val contactAccount: Account,
|
||||
@Json(name = "max_toot_chars") val maxTootChars: Int?,
|
||||
@Json(name = "poll_limits") val pollConfiguration: PollConfiguration?,
|
||||
val configuration: InstanceConfiguration?,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int?,
|
||||
val pleroma: PleromaConfiguration?,
|
||||
@SerializedName("upload_limit") val uploadLimit: Int?,
|
||||
@Json(name = "upload_limit") val uploadLimit: Int?,
|
||||
val rules: List<InstanceRules>?,
|
||||
) {
|
||||
override fun hashCode(): Int {
|
||||
|
@ -51,49 +53,57 @@ data class InstanceV1(
|
|||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollConfiguration(
|
||||
@SerializedName("max_options") val maxOptions: Int?,
|
||||
@SerializedName("max_option_chars") val maxOptionChars: Int?,
|
||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
|
||||
@SerializedName("min_expiration") val minExpiration: Int?,
|
||||
@SerializedName("max_expiration") val maxExpiration: Int?,
|
||||
@Json(name = "max_options") val maxOptions: Int?,
|
||||
@Json(name = "max_option_chars") val maxOptionChars: Int?,
|
||||
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int?,
|
||||
@Json(name = "min_expiration") val minExpiration: Int?,
|
||||
@Json(name = "max_expiration") val maxExpiration: Int?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceConfiguration(
|
||||
val statuses: StatusConfiguration?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
||||
@Json(name = "media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
||||
val polls: PollConfiguration?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusConfiguration(
|
||||
@SerializedName("max_characters") val maxCharacters: Int?,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?,
|
||||
@Json(name = "max_characters") val maxCharacters: Int?,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int?,
|
||||
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaAttachmentConfiguration(
|
||||
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>?,
|
||||
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
|
||||
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
|
||||
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
|
||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
||||
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?,
|
||||
@Json(name = "supported_mime_types") val supportedMimeTypes: List<String>?,
|
||||
@Json(name = "image_size_limit") val imageSizeLimit: Int?,
|
||||
@Json(name = "image_matrix_limit") val imageMatrixLimit: Int?,
|
||||
@Json(name = "video_size_limit") val videoSizeLimit: Int?,
|
||||
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
||||
@Json(name = "video_matrix_limit") val videoMatrixLimit: Int?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaConfiguration(
|
||||
val metadata: PleromaMetadata?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaMetadata(
|
||||
@SerializedName("fields_limits") val fieldLimits: PleromaFieldLimits,
|
||||
@Json(name = "fields_limits") val fieldLimits: PleromaFieldLimits,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaFieldLimits(
|
||||
@SerializedName("max_fields") val maxFields: Int?,
|
||||
@SerializedName("name_length") val nameLength: Int?,
|
||||
@SerializedName("value_length") val valueLength: Int?,
|
||||
@Json(name = "max_fields") val maxFields: Int?,
|
||||
@Json(name = "name_length") val nameLength: Int?,
|
||||
@Json(name = "value_length") val valueLength: Int?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceRules(
|
||||
val id: String,
|
||||
val text: String,
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** https://docs.joinmastodon.org/entities/Instance/ */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV2(
|
||||
/** The domain name of the instance */
|
||||
val domain: String,
|
||||
|
@ -34,7 +36,7 @@ data class InstanceV2(
|
|||
* The URL for the source code of the software running on this instance, in keeping with AGPL
|
||||
* license requirements.
|
||||
*/
|
||||
@SerializedName("source_url") val sourceUrl: String,
|
||||
@Json(name = "source_url") val sourceUrl: String,
|
||||
|
||||
/** A short, plain-text description defined by the admin. */
|
||||
val description: String,
|
||||
|
@ -61,16 +63,19 @@ data class InstanceV2(
|
|||
val rules: List<Rule>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Usage(
|
||||
/** Usage data related to users on this instance. */
|
||||
val users: Users,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Users(
|
||||
/** The number of active users in the past 4 weeks. */
|
||||
val activeMonth: Int = 0,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Thumbnail(
|
||||
/** The URL for the thumbnail image. */
|
||||
val url: String,
|
||||
|
@ -85,14 +90,16 @@ data class Thumbnail(
|
|||
val versions: ThumbnailVersions?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ThumbnailVersions(
|
||||
/** The URL for the thumbnail image at 1x resolution. */
|
||||
@SerializedName("@1x") val oneX: String?,
|
||||
@Json(name = "@1x") val oneX: String?,
|
||||
|
||||
/** The URL for the thumbnail image at 2x resolution. */
|
||||
@SerializedName("@2x") val twoX: String?,
|
||||
@Json(name = "@2x") val twoX: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Configuration(
|
||||
/** URLs of interest for clients apps. */
|
||||
val urls: InstanceV2Urls,
|
||||
|
@ -104,7 +111,7 @@ data class Configuration(
|
|||
val statuses: InstanceV2Statuses,
|
||||
|
||||
/** Hints for which attachments will be accepted. */
|
||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachments,
|
||||
@Json(name = "media_attachments") val mediaAttachments: MediaAttachments,
|
||||
|
||||
/** Limits related to polls. */
|
||||
val polls: InstanceV2Polls,
|
||||
|
@ -113,77 +120,94 @@ data class Configuration(
|
|||
val translation: InstanceV2Translation,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV2Urls(
|
||||
/** The Websockets URL for connecting to the streaming API. */
|
||||
@SerializedName("streaming_api") val streamingApi: String,
|
||||
/**
|
||||
* The Websockets URL for connecting to the streaming API. This is the
|
||||
* documented property name
|
||||
*/
|
||||
@Json(name = "streaming_api") val streamingApi: String? = null,
|
||||
|
||||
/**
|
||||
* The Websockets URL for connecting to the streaming API. This is the
|
||||
* undocumented property name, see https://github.com/mastodon/mastodon/pull/29124
|
||||
*/
|
||||
@Json(name = "streaming") val streaming: String? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV2Accounts(
|
||||
/** The maximum number of featured tags allowed for each account. */
|
||||
@SerializedName("max_featured_tags") val maxFeaturedTags: Int,
|
||||
@Json(name = "max_featured_tags") val maxFeaturedTags: Int,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV2Statuses(
|
||||
/** The maximum number of allowed characters per status. */
|
||||
@SerializedName("max_characters") val maxCharacters: Int,
|
||||
@Json(name = "max_characters") val maxCharacters: Int,
|
||||
|
||||
/** The maximum number of media attachments that can be added to a status. */
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int,
|
||||
|
||||
/** Each URL in a status will be assumed to be exactly this many characters. */
|
||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int,
|
||||
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaAttachments(
|
||||
/** Contains MIME types that can be uploaded. */
|
||||
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>,
|
||||
@Json(name = "supported_mime_types") val supportedMimeTypes: List<String>,
|
||||
|
||||
/** The maximum size of any uploaded image, in bytes. */
|
||||
@SerializedName("image_size_limit") val imageSizeLimit: Int,
|
||||
@Json(name = "image_size_limit") val imageSizeLimit: Int,
|
||||
|
||||
/** The maximum number of pixels (width times height) for image uploads. */
|
||||
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int,
|
||||
@Json(name = "image_matrix_limit") val imageMatrixLimit: Int,
|
||||
|
||||
/** The maximum size of any uploaded video, in bytes. */
|
||||
@SerializedName("video_size_limit") val videoSizeLimit: Int,
|
||||
@Json(name = "video_size_limit") val videoSizeLimit: Int,
|
||||
|
||||
/** The maximum frame rate for any uploaded video. */
|
||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int,
|
||||
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int,
|
||||
|
||||
/** The maximum number of pixels (width times height) for video uploads. */
|
||||
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int,
|
||||
@Json(name = "video_matrix_limit") val videoMatrixLimit: Int,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV2Polls(
|
||||
/** Each poll is allowed to have up to this many options. */
|
||||
@SerializedName("max_options") val maxOptions: Int,
|
||||
@Json(name = "max_options") val maxOptions: Int,
|
||||
|
||||
/** Each poll option is allowed to have this many characters. */
|
||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int,
|
||||
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int,
|
||||
|
||||
/** The shortest allowed poll duration, in seconds. */
|
||||
@SerializedName("min_expiration") val minExpiration: Int,
|
||||
@Json(name = "min_expiration") val minExpiration: Int,
|
||||
|
||||
/** The longest allowed poll duration, in seconds. */
|
||||
@SerializedName("max_expiration") val maxExpiration: Int,
|
||||
@Json(name = "max_expiration") val maxExpiration: Int,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV2Translation(
|
||||
/** Whether the Translations API is available on this instance. */
|
||||
val enabled: Boolean,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Registrations(
|
||||
/** Whether registrations are enabled. */
|
||||
val enabled: Boolean,
|
||||
|
||||
/** Whether registrations require moderator approval. */
|
||||
@SerializedName("approval_required") val approvalRequired: Boolean,
|
||||
@Json(name = "approval_required") val approvalRequired: Boolean,
|
||||
|
||||
/** A custom message to be shown when registrations are closed. */
|
||||
val message: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Contact(
|
||||
/** An email address that can be messaged regarding inquiries or issues. */
|
||||
val email: String,
|
||||
|
@ -193,6 +217,7 @@ data class Contact(
|
|||
)
|
||||
|
||||
/** https://docs.joinmastodon.org/entities/Rule/ */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Rule(
|
||||
/** An identifier for the rule. */
|
||||
val id: String,
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* API type for saving the scroll position of a timeline.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Marker(
|
||||
@SerializedName("last_read_id")
|
||||
@Json(name = "last_read_id")
|
||||
val lastReadId: String,
|
||||
val version: Int,
|
||||
@SerializedName("updated_at")
|
||||
@Json(name = "updated_at")
|
||||
val updatedAt: Date,
|
||||
)
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MastoList(
|
||||
val id: String,
|
||||
val title: String,
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* The same as Attachment, except the url is null - see https://docs.joinmastodon.org/methods/statuses/media/
|
||||
* We are only interested in the id, so other attributes are omitted
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaUploadResult(
|
||||
val id: String,
|
||||
)
|
||||
|
|
|
@ -17,32 +17,36 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewStatus(
|
||||
val status: String,
|
||||
@SerializedName("spoiler_text") val warningText: String,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@Json(name = "spoiler_text") val warningText: String,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String?,
|
||||
val visibility: String,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("media_ids") val mediaIds: List<String>?,
|
||||
@SerializedName("media_attributes") val mediaAttributes: List<MediaAttribute>?,
|
||||
@SerializedName("scheduled_at") val scheduledAt: String?,
|
||||
@Json(name = "media_ids") val mediaIds: List<String>?,
|
||||
@Json(name = "media_attributes") val mediaAttributes: List<MediaAttribute>?,
|
||||
@Json(name = "scheduled_at") val scheduledAt: String?,
|
||||
val poll: NewPoll?,
|
||||
val language: String?,
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewPoll(
|
||||
val options: List<String>,
|
||||
@SerializedName("expires_in") val expiresIn: Int,
|
||||
@Json(name = "expires_in") val expiresIn: Int,
|
||||
val multiple: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
// It would be nice if we could reuse MediaToSend,
|
||||
// but the server requires a different format for focus
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaAttribute(
|
||||
val id: String,
|
||||
val description: String?,
|
||||
|
|
|
@ -17,14 +17,12 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
// TODO: These should be different subclasses per type, so that each subclass can
|
||||
// carry the non-null data that it needs.
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Notification(
|
||||
val type: Type,
|
||||
val id: String,
|
||||
|
@ -34,38 +32,48 @@ data class Notification(
|
|||
) {
|
||||
|
||||
/** From https://docs.joinmastodon.org/entities/Notification/#type */
|
||||
@JsonAdapter(NotificationTypeAdapter::class)
|
||||
enum class Type(val presentation: String) {
|
||||
@Json(name = "unknown")
|
||||
UNKNOWN("unknown"),
|
||||
|
||||
/** Someone mentioned you */
|
||||
@Json(name = "mention")
|
||||
MENTION("mention"),
|
||||
|
||||
/** Someone boosted one of your statuses */
|
||||
@Json(name = "reblog")
|
||||
REBLOG("reblog"),
|
||||
|
||||
/** Someone favourited one of your statuses */
|
||||
@Json(name = "favourite")
|
||||
FAVOURITE("favourite"),
|
||||
|
||||
/** Someone followed you */
|
||||
@Json(name = "follow")
|
||||
FOLLOW("follow"),
|
||||
|
||||
/** Someone requested to follow you */
|
||||
@Json(name = "follow_request")
|
||||
FOLLOW_REQUEST("follow_request"),
|
||||
|
||||
/** A poll you have voted in or created has ended */
|
||||
@Json(name = "poll")
|
||||
POLL("poll"),
|
||||
|
||||
/** Someone you enabled notifications for has posted a status */
|
||||
@Json(name = "status")
|
||||
STATUS("status"),
|
||||
|
||||
/** Someone signed up (optionally sent to admins) */
|
||||
@Json(name = "admin.sign_up")
|
||||
SIGN_UP("admin.sign_up"),
|
||||
|
||||
/** A status you interacted with has been updated */
|
||||
@Json(name = "update")
|
||||
UPDATE("update"),
|
||||
|
||||
/** A new report has been filed */
|
||||
@Json(name = "admin.report")
|
||||
REPORT("admin.report"),
|
||||
;
|
||||
|
||||
|
@ -101,18 +109,6 @@ data class Notification(
|
|||
return notification?.id == this.id
|
||||
}
|
||||
|
||||
class NotificationTypeAdapter : JsonDeserializer<Type> {
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: java.lang.reflect.Type,
|
||||
context: JsonDeserializationContext,
|
||||
): Type {
|
||||
return Type.byString(json.asString)
|
||||
}
|
||||
}
|
||||
|
||||
// for Pleroma compatibility that uses Mention type
|
||||
fun rewriteToStatusTypeIfNeeded(accountId: String): Notification {
|
||||
if (type == Type.MENTION && status != null) {
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NotificationSubscribeResult(
|
||||
val id: Int,
|
||||
val endpoint: String,
|
||||
@SerializedName("server_key") val serverKey: String,
|
||||
@Json(name = "server_key") val serverKey: String,
|
||||
)
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Poll(
|
||||
val id: String,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@Json(name = "expires_at") val expiresAt: Date?,
|
||||
val expired: Boolean,
|
||||
val multiple: Boolean,
|
||||
@SerializedName("votes_count") val votesCount: Int,
|
||||
@Json(name = "votes_count") val votesCount: Int,
|
||||
// nullable for compatibility with Pleroma
|
||||
@SerializedName("voters_count") val votersCount: Int?,
|
||||
@Json(name = "voters_count") val votersCount: Int?,
|
||||
val options: List<PollOption>,
|
||||
val voted: Boolean,
|
||||
@SerializedName("own_votes") val ownVotes: List<Int>?,
|
||||
@Json(name = "own_votes") val ownVotes: List<Int>?,
|
||||
) {
|
||||
|
||||
fun votedCopy(choices: List<Int>): Poll {
|
||||
|
@ -42,7 +44,8 @@ data class Poll(
|
|||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollOption(
|
||||
val title: String,
|
||||
@SerializedName("votes_count") val votesCount: Int,
|
||||
@Json(name = "votes_count") val votesCount: Int,
|
||||
)
|
||||
|
|
|
@ -17,25 +17,30 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import app.pachli.core.network.json.GuardedBooleanAdapter
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import app.pachli.core.network.json.Guarded
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Relationship(
|
||||
val id: String,
|
||||
val following: Boolean,
|
||||
@SerializedName("followed_by") val followedBy: Boolean,
|
||||
@Json(name = "followed_by") val followedBy: Boolean,
|
||||
val blocking: Boolean,
|
||||
val muting: Boolean,
|
||||
@SerializedName("muting_notifications") val mutingNotifications: Boolean,
|
||||
@Json(name = "muting_notifications") val mutingNotifications: Boolean,
|
||||
val requested: Boolean,
|
||||
@SerializedName("showing_reblogs") val showingReblogs: Boolean,
|
||||
/* Pleroma extension, same as 'notifying' on Mastodon.
|
||||
* Some instances like qoto.org have a custom subscription feature where 'subscribing' is a json object,
|
||||
* so we use the custom GuardedBooleanAdapter to ignore the field if it is not a boolean.
|
||||
@Json(name = "showing_reblogs") val showingReblogs: Boolean,
|
||||
/**
|
||||
* Pleroma extension, same as 'notifying' on Mastodon.
|
||||
*
|
||||
* Some instances like qoto.org have a custom subscription feature where
|
||||
* 'subscribing' is a json object, so we use the custom `@Guarded` annotation
|
||||
* to ignore the field if it is not a boolean.
|
||||
*/
|
||||
@JsonAdapter(GuardedBooleanAdapter::class) val subscribing: Boolean? = null,
|
||||
@SerializedName("domain_blocking") val blockingDomain: Boolean,
|
||||
@Guarded
|
||||
val subscribing: Boolean? = null,
|
||||
@Json(name = "domain_blocking") val blockingDomain: Boolean,
|
||||
// nullable for backward compatibility / feature detection
|
||||
val note: String?,
|
||||
// since 3.3.0rc
|
||||
|
|
|
@ -17,13 +17,15 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Report(
|
||||
val id: String,
|
||||
val category: String,
|
||||
val status_ids: List<String>?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@SerializedName("target_account") val targetAccount: TimelineAccount,
|
||||
@Json(name = "status_ids") val statusIds: List<String>?,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
@Json(name = "target_account") val targetAccount: TimelineAccount,
|
||||
)
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ScheduledStatus(
|
||||
val id: String,
|
||||
@SerializedName("scheduled_at") val scheduledAt: String,
|
||||
@Json(name = "scheduled_at") val scheduledAt: String,
|
||||
val params: StatusParams,
|
||||
@SerializedName("media_attachments") val mediaAttachments: ArrayList<Attachment>,
|
||||
@Json(name = "media_attachments") val mediaAttachments: List<Attachment>,
|
||||
)
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SearchResult(
|
||||
val accounts: List<TimelineAccount>,
|
||||
val statuses: List<Status>,
|
||||
|
|
|
@ -20,31 +20,33 @@ package app.pachli.core.network.model
|
|||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.URLSpan
|
||||
import app.pachli.core.network.parseAsMastodonHtml
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Status(
|
||||
val id: String,
|
||||
// not present if it's reblog
|
||||
val url: String?,
|
||||
val account: TimelineAccount,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String?,
|
||||
@Json(name = "in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||
val reblog: Status?,
|
||||
val content: String,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@SerializedName("edited_at") val editedAt: Date?,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
@Json(name = "edited_at") val editedAt: Date?,
|
||||
val emojis: List<Emoji>,
|
||||
@SerializedName("reblogs_count") val reblogsCount: Int,
|
||||
@SerializedName("favourites_count") val favouritesCount: Int,
|
||||
@SerializedName("replies_count") val repliesCount: Int,
|
||||
@Json(name = "reblogs_count") val reblogsCount: Int,
|
||||
@Json(name = "favourites_count") val favouritesCount: Int,
|
||||
@Json(name = "replies_count") val repliesCount: Int,
|
||||
val reblogged: Boolean,
|
||||
val favourited: Boolean,
|
||||
val bookmarked: Boolean,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val visibility: Visibility,
|
||||
@SerializedName("media_attachments") val attachments: List<Attachment>,
|
||||
@Json(name = "media_attachments") val attachments: List<Attachment>,
|
||||
val mentions: List<Mention>,
|
||||
val tags: List<HashTag>?,
|
||||
val application: Application?,
|
||||
|
@ -65,16 +67,16 @@ data class Status(
|
|||
enum class Visibility(val num: Int) {
|
||||
UNKNOWN(0),
|
||||
|
||||
@SerializedName("public")
|
||||
@Json(name = "public")
|
||||
PUBLIC(1),
|
||||
|
||||
@SerializedName("unlisted")
|
||||
@Json(name = "unlisted")
|
||||
UNLISTED(2),
|
||||
|
||||
@SerializedName("private")
|
||||
@Json(name = "private")
|
||||
PRIVATE(3),
|
||||
|
||||
@SerializedName("direct")
|
||||
@Json(name = "direct")
|
||||
DIRECT(4),
|
||||
;
|
||||
|
||||
|
@ -156,13 +158,15 @@ data class Status(
|
|||
return builder.toString()
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Mention(
|
||||
val id: String,
|
||||
val url: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@SerializedName("username") val localUsername: String,
|
||||
@Json(name = "acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Application(
|
||||
val name: String,
|
||||
val website: String?,
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusContext(
|
||||
val ancestors: List<Status>,
|
||||
val descendants: List<Status>,
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusEdit(
|
||||
val content: String,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
val account: TimelineAccount,
|
||||
val poll: Poll?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: List<Attachment>,
|
||||
@Json(name = "media_attachments") val mediaAttachments: List<Attachment>,
|
||||
val emojis: List<Emoji>,
|
||||
)
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusParams(
|
||||
val text: String,
|
||||
val sensitive: Boolean,
|
||||
val visibility: Status.Visibility,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String?,
|
||||
)
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusSource(
|
||||
val id: String,
|
||||
val text: String,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
)
|
||||
|
|
|
@ -17,19 +17,21 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Same as [Account], but only with the attributes required in timelines.
|
||||
* Prefer this class over [Account] because it uses way less memory & deserializes faster from json.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TimelineAccount(
|
||||
val id: String,
|
||||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String,
|
||||
@Json(name = "acct") val username: String,
|
||||
// should never be null per Api definition, but some servers break the contract
|
||||
@Deprecated("prefer the `name` property, which is not-null and not-empty")
|
||||
@SerializedName("display_name") val displayName: String?,
|
||||
@Json(name = "display_name") val displayName: String?,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
val note: String,
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** https://docs.joinmastodon.org/entities/Translation/ */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Translation(
|
||||
/** The translated text of the status (HTML), equivalent to [Status.content] */
|
||||
val content: String,
|
||||
|
@ -28,14 +30,14 @@ data class Translation(
|
|||
* The language of the source text, as auto-detected by the machine translation
|
||||
* (ISO 639 language code)
|
||||
*/
|
||||
@SerializedName("detected_source_language") val detectedSourceLanguage: String,
|
||||
@Json(name = "detected_source_language") val detectedSourceLanguage: String,
|
||||
|
||||
// Not documented, see https://github.com/mastodon/documentation/issues/1248
|
||||
/**
|
||||
* The translated spoiler text of the status (text), if it exists, equivalent to
|
||||
* [Status.spoilerText]
|
||||
*/
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
|
||||
// Not documented, see https://github.com/mastodon/documentation/issues/1248
|
||||
/** The translated poll (if it exists) */
|
||||
|
@ -46,7 +48,7 @@ data class Translation(
|
|||
* Translated descriptions for media attachments, if any were attached. Other metadata has
|
||||
* to be determined from the original attachment.
|
||||
*/
|
||||
@SerializedName("media_attachments") val attachments: List<TranslatedAttachment>,
|
||||
@Json(name = "media_attachments") val attachments: List<TranslatedAttachment>,
|
||||
|
||||
/** The service that provided the machine translation */
|
||||
val provider: String,
|
||||
|
@ -56,17 +58,20 @@ data class Translation(
|
|||
* A translated poll. Does not contain all the poll data, only the translated text.
|
||||
* Vote counts and other metadata has to be determined from the original poll object.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TranslatedPoll(
|
||||
val id: String,
|
||||
val options: List<TranslatedPollOption>,
|
||||
)
|
||||
|
||||
/** A translated poll option. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TranslatedPollOption(
|
||||
val title: String,
|
||||
)
|
||||
|
||||
/** A translated attachment. Only the description is translated */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TranslatedAttachment(
|
||||
val id: String,
|
||||
val description: String,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
|
@ -27,6 +28,7 @@ import java.util.Date
|
|||
* @param history A list of [TrendingTagHistory]. Each element contains metrics per day for this hashtag.
|
||||
* (@param following This is not listed in the APIs at the time of writing, but an instance is delivering it.)
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TrendingTag(
|
||||
val name: String,
|
||||
val history: List<TrendingTagHistory>,
|
||||
|
@ -39,6 +41,7 @@ data class TrendingTag(
|
|||
* @param accounts The number of accounts that have posted with this hashtag.
|
||||
* @param uses The number of posts with this hashtag.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TrendingTagHistory(
|
||||
val day: String,
|
||||
val accounts: String,
|
||||
|
|
|
@ -17,19 +17,20 @@
|
|||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
enum class PreviewCardKind {
|
||||
@SerializedName("link")
|
||||
@Json(name = "link")
|
||||
LINK,
|
||||
|
||||
@SerializedName("photo")
|
||||
@Json(name = "photo")
|
||||
PHOTO,
|
||||
|
||||
@SerializedName("video")
|
||||
@Json(name = "video")
|
||||
VIDEO,
|
||||
|
||||
@SerializedName("rich")
|
||||
@Json(name = "rich")
|
||||
RICH,
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,7 @@ interface PreviewCard {
|
|||
val blurhash: String?
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LinkHistory(
|
||||
val day: String,
|
||||
val accounts: Int,
|
||||
|
@ -60,20 +62,21 @@ data class LinkHistory(
|
|||
)
|
||||
|
||||
/** Represents a https://docs.joinmastodon.org/entities/PreviewCard/#trends-link */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TrendsLink(
|
||||
override val url: String,
|
||||
override val title: String,
|
||||
override val description: String,
|
||||
@SerializedName("type") override val kind: PreviewCardKind,
|
||||
@SerializedName("author_name") override val authorName: String,
|
||||
@SerializedName("author_url") override val authorUrl: String,
|
||||
@SerializedName("provider_name") override val providerName: String,
|
||||
@SerializedName("provider_url") override val providerUrl: String,
|
||||
@Json(name = "type") override val kind: PreviewCardKind,
|
||||
@Json(name = "author_name") override val authorName: String,
|
||||
@Json(name = "author_url") override val authorUrl: String,
|
||||
@Json(name = "provider_name") override val providerName: String,
|
||||
@Json(name = "provider_url") override val providerUrl: String,
|
||||
override val html: String,
|
||||
override val width: Int,
|
||||
override val height: Int,
|
||||
override val image: String? = null,
|
||||
@SerializedName("embed_url") override val embedUrl: String,
|
||||
@Json(name = "embed_url") override val embedUrl: String,
|
||||
override val blurhash: String? = null,
|
||||
val history: List<LinkHistory>,
|
||||
) : PreviewCard
|
||||
|
|
|
@ -26,13 +26,16 @@ import app.pachli.core.network.model.nodeinfo.NodeInfo.Error.NoSoftwareVersion
|
|||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* The JRD document that links to one or more URLs that contain the schema.
|
||||
*
|
||||
* See https://nodeinfo.diaspora.software/protocol.html.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UnvalidatedJrd(val links: List<Link>) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Link(val rel: String, val href: String)
|
||||
}
|
||||
|
||||
|
@ -42,7 +45,9 @@ data class UnvalidatedJrd(val links: List<Link>) {
|
|||
* See https://nodeinfo.diaspora.software/protocol.html and
|
||||
* https://nodeinfo.diaspora.software/schema.html.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UnvalidatedNodeInfo(val software: Software?) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Software(val name: String?, val version: String?)
|
||||
}
|
||||
|
||||
|
|
|
@ -202,6 +202,14 @@ interface MastodonApi {
|
|||
@Body status: NewStatus,
|
||||
): NetworkResult<Status>
|
||||
|
||||
@POST("api/v1/statuses")
|
||||
suspend fun createScheduledStatus(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Header("Idempotency-Key") idempotencyKey: String,
|
||||
@Body status: NewStatus,
|
||||
): NetworkResult<ScheduledStatus>
|
||||
|
||||
@GET("api/v1/statuses/{id}")
|
||||
suspend fun status(
|
||||
@Path("id") statusId: String,
|
||||
|
|
|
@ -44,9 +44,10 @@ import app.pachli.core.network.model.Users
|
|||
import app.pachli.core.network.model.nodeinfo.NodeInfo
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.google.common.reflect.TypeToken
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import io.github.z4kn4fein.semver.toVersion
|
||||
import java.io.BufferedReader
|
||||
import org.junit.Test
|
||||
|
@ -274,8 +275,8 @@ class ServerTest(
|
|||
}
|
||||
}
|
||||
|
||||
class ServerVersionTest() {
|
||||
private val gson = Gson()
|
||||
class ServerVersionTest {
|
||||
private val moshi = Moshi.Builder().build()
|
||||
|
||||
private fun loadJsonAsString(fileName: String): String {
|
||||
return javaClass.getResourceAsStream("/$fileName")!!
|
||||
|
@ -290,15 +291,15 @@ class ServerVersionTest() {
|
|||
* that have been seen by Fediverse Observer. These version strings
|
||||
* are then parsed and are all expected to parse correctly.
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Test
|
||||
fun parseVersionString() {
|
||||
val mapType: TypeToken<Map<String, Set<String>>> =
|
||||
object : TypeToken<Map<String, Set<String>>>() {}
|
||||
|
||||
val serverVersions = gson.fromJson(
|
||||
loadJsonAsString("server-versions.json5"),
|
||||
mapType,
|
||||
)
|
||||
val serverVersions = moshi.adapter<Map<String, Set<String>>>()
|
||||
.lenient()
|
||||
.fromJson(loadJsonAsString("server-versions.json5"))!!
|
||||
|
||||
// Sanity check that data was loaded correctly. Expect at least 5 distinct keys
|
||||
assertWithMessage("number of server types in server-versions.json5 is too low")
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package app.pachli.core.network.json
|
||||
|
||||
import app.pachli.core.network.json.GuardedAdapter.Companion.GuardedAdapterFactory
|
||||
import app.pachli.core.network.model.Relationship
|
||||
import com.google.gson.Gson
|
||||
import org.junit.Assert.assertEquals
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import org.junit.Test
|
||||
|
||||
class GuardedBooleanAdapterTest {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class GuardedAdapterTest {
|
||||
|
||||
private val gson = Gson()
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(GuardedAdapterFactory())
|
||||
.build()
|
||||
|
||||
@Test
|
||||
fun `should deserialize Relationship when attribute 'subscribing' is a boolean`() {
|
||||
|
@ -30,7 +35,7 @@ class GuardedBooleanAdapterTest {
|
|||
}
|
||||
""".trimIndent()
|
||||
|
||||
assertEquals(
|
||||
assertThat(moshi.adapter<Relationship>().fromJson(jsonInput)).isEqualTo(
|
||||
Relationship(
|
||||
id = "1",
|
||||
following = true,
|
||||
|
@ -45,7 +50,6 @@ class GuardedBooleanAdapterTest {
|
|||
note = "Hi",
|
||||
notifying = false,
|
||||
),
|
||||
gson.fromJson(jsonInput, Relationship::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -70,7 +74,7 @@ class GuardedBooleanAdapterTest {
|
|||
}
|
||||
""".trimIndent()
|
||||
|
||||
assertEquals(
|
||||
assertThat(moshi.adapter<Relationship>().fromJson(jsonInput)).isEqualTo(
|
||||
Relationship(
|
||||
id = "2",
|
||||
following = true,
|
||||
|
@ -85,7 +89,6 @@ class GuardedBooleanAdapterTest {
|
|||
note = "Hi",
|
||||
notifying = false,
|
||||
),
|
||||
gson.fromJson(jsonInput, Relationship::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -109,7 +112,7 @@ class GuardedBooleanAdapterTest {
|
|||
}
|
||||
""".trimIndent()
|
||||
|
||||
assertEquals(
|
||||
assertThat(moshi.adapter<Relationship>().fromJson(jsonInput)).isEqualTo(
|
||||
Relationship(
|
||||
id = "3",
|
||||
following = true,
|
||||
|
@ -124,7 +127,6 @@ class GuardedBooleanAdapterTest {
|
|||
note = "Hi",
|
||||
notifying = false,
|
||||
),
|
||||
gson.fromJson(jsonInput, Relationship::class.java),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -39,7 +39,6 @@ filemoji-compat = "3.2.7"
|
|||
glide = "4.16.0"
|
||||
# Deliberate downgrade, https://github.com/tuskyapp/Tusky/issues/3631
|
||||
glide-animation-plugin = "2.23.0"
|
||||
gson = "2.10.1"
|
||||
hilt = "2.50"
|
||||
junit = "4.13.2"
|
||||
kotlin = "1.9.22"
|
||||
|
@ -53,6 +52,7 @@ material-drawer = "9.0.2"
|
|||
material-typeface = "4.0.0.2-kotlin"
|
||||
mockito-inline = "5.2.0"
|
||||
mockito-kotlin = "5.2.1"
|
||||
moshi = "1.15.1"
|
||||
networkresult-calladapter = "1.0.0"
|
||||
okhttp = "4.12.0"
|
||||
quadrant = "1.9.1"
|
||||
|
@ -152,7 +152,6 @@ glide-animation-plugin = { module = "com.github.penfeizhou.android.animation:gli
|
|||
glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
|
||||
glide-core = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
||||
glide-okhttp3-integration = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glide" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
|
||||
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
|
||||
|
@ -175,10 +174,13 @@ material-typeface = { module = "com.mikepenz:google-material-typeface", version.
|
|||
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" }
|
||||
mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito-inline" }
|
||||
mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
||||
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" }
|
||||
moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
|
||||
networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" }
|
||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
|
||||
retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
|
||||
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
rxjava3-android = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid3" }
|
||||
|
@ -217,7 +219,7 @@ lint-tests = ["junit", "lint-cli", "lint-tests"]
|
|||
material-drawer = ["material-drawer-core", "material-drawer-iconics"]
|
||||
mockito = ["mockito-kotlin", "mockito-inline"]
|
||||
okhttp = ["okhttp-core", "okhttp-logging-interceptor"]
|
||||
retrofit = ["retrofit-core", "retrofit-converter-gson"]
|
||||
retrofit = ["retrofit-core", "retrofit-converter-moshi"]
|
||||
room = ["androidx-room-ktx", "androidx-room-paging"]
|
||||
rxjava3 = ["rxjava3-core", "rxjava3-android", "rxjava3-kotlin"]
|
||||
xmldiff = ["diffx", "xmlwriter"]
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
|
||||
plugins {
|
||||
id("com.google.devtools.ksp")
|
||||
id("com.apollographql.apollo3") version "3.8.2"
|
||||
}
|
||||
|
||||
|
@ -31,7 +32,9 @@ dependencies {
|
|||
implementation("io.github.oshai:kotlin-logging-jvm:5.1.0")
|
||||
implementation("ch.qos.logback:logback-classic:1.4.11")
|
||||
|
||||
implementation(libs.gson)
|
||||
// Moshi
|
||||
implementation(libs.moshi)
|
||||
ksp(libs.moshi.codegen)
|
||||
|
||||
// Testing
|
||||
testImplementation(kotlin("test"))
|
||||
|
|
|
@ -25,7 +25,8 @@ import com.github.ajalt.clikt.core.CliktCommand
|
|||
import com.github.ajalt.clikt.core.UsageError
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import io.github.oshai.kotlinlogging.DelegatingKLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import java.nio.file.Path
|
||||
|
@ -65,6 +66,7 @@ class App : CliktCommand(help = """Update server-versions.json5""") {
|
|||
return null
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override fun run() = runBlocking {
|
||||
System.setProperty("file.encoding", "UTF8")
|
||||
((log as? DelegatingKLogger<*>)?.underlyingLogger as Logger).level = if (verbose) Level.INFO else Level.WARN
|
||||
|
@ -102,8 +104,8 @@ class App : CliktCommand(help = """Update server-versions.json5""") {
|
|||
}
|
||||
}
|
||||
|
||||
val gson = GsonBuilder().setPrettyPrinting().create()
|
||||
w.print(gson.toJson(m))
|
||||
val moshi = Moshi.Builder().build()
|
||||
w.print(moshi.adapter<Map<String, Set<String>>>().indent(" ").toJson(m))
|
||||
w.close()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue