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.android.material)
|
||||||
|
|
||||||
implementation(libs.gson)
|
implementation(libs.moshi)
|
||||||
|
implementation(libs.moshi.adapters)
|
||||||
|
ksp(libs.moshi.codegen)
|
||||||
|
|
||||||
implementation(libs.bundles.retrofit)
|
implementation(libs.bundles.retrofit)
|
||||||
|
|
||||||
|
|
|
@ -57,19 +57,6 @@
|
||||||
# error reports
|
# error reports
|
||||||
-keepnames class *
|
-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
|
# Retain generic signatures of classes used in MastodonApi so Retrofit works
|
||||||
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Single
|
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Single
|
||||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||||
|
|
|
@ -19,7 +19,6 @@ package app.pachli.di
|
||||||
|
|
||||||
import app.pachli.updatecheck.FdroidService
|
import app.pachli.updatecheck.FdroidService
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||||
import com.google.gson.Gson
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -27,7 +26,7 @@ import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import retrofit2.create
|
import retrofit2.create
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@ -37,11 +36,10 @@ object UpdateCheckModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesFdroidService(
|
fun providesFdroidService(
|
||||||
httpClient: OkHttpClient,
|
httpClient: OkHttpClient,
|
||||||
gson: Gson,
|
|
||||||
): FdroidService = Retrofit.Builder()
|
): FdroidService = Retrofit.Builder()
|
||||||
.baseUrl("https://f-droid.org")
|
.baseUrl("https://f-droid.org")
|
||||||
.client(httpClient)
|
.client(httpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
.addConverterFactory(MoshiConverterFactory.create())
|
||||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
.create()
|
.create()
|
||||||
|
|
|
@ -19,16 +19,19 @@ package app.pachli.updatecheck
|
||||||
|
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class FdroidPackageVersion(
|
data class FdroidPackageVersion(
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
val versionCode: Int,
|
val versionCode: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class FdroidPackage(
|
data class FdroidPackage(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val suggestedVersionCode: Int,
|
val suggestedVersionCode: Int,
|
||||||
|
|
|
@ -19,7 +19,6 @@ package app.pachli.di
|
||||||
|
|
||||||
import app.pachli.updatecheck.GitHubService
|
import app.pachli.updatecheck.GitHubService
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||||
import com.google.gson.Gson
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -27,7 +26,6 @@ import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import retrofit2.create
|
import retrofit2.create
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@ -37,11 +35,10 @@ object UpdateCheckModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGitHubService(
|
fun providesGitHubService(
|
||||||
httpClient: OkHttpClient,
|
httpClient: OkHttpClient,
|
||||||
gson: Gson,
|
|
||||||
): GitHubService = Retrofit.Builder()
|
): GitHubService = Retrofit.Builder()
|
||||||
.baseUrl("https://api.github.com")
|
.baseUrl("https://api.github.com")
|
||||||
.client(httpClient)
|
.client(httpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
.addConverterFactory(MoshiConverterFactory.create())
|
||||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
.create()
|
.create()
|
||||||
|
|
|
@ -19,23 +19,26 @@ package app.pachli.updatecheck
|
||||||
|
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
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.GET
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class GitHubReleaseAsset(
|
data class GitHubReleaseAsset(
|
||||||
/** File name for the asset, e.g., "113.apk" */
|
/** File name for the asset, e.g., "113.apk" */
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
||||||
/** MIME content type for the asset, e.g., "application/vnd.android.package-archive" */
|
/** 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
|
@Keep
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class GitHubRelease(
|
data class GitHubRelease(
|
||||||
/** URL for the release's web page */
|
/** URL for the release's web page */
|
||||||
@SerializedName("html_url") val htmlUrl: String,
|
@Json(name = "html_url") val htmlUrl: String,
|
||||||
val assets: List<GitHubReleaseAsset>,
|
val assets: List<GitHubReleaseAsset>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -37,10 +37,7 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
|
|
||||||
import androidx.core.app.ActivityCompat.requestPermissions
|
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
@ -89,7 +86,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
var isToolbarVisible = true
|
var isToolbarVisible = true
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var attachments: ArrayList<AttachmentViewData>? = null
|
private var attachments: List<AttachmentViewData>? = null
|
||||||
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
||||||
private var imageUrl: String? = null
|
private var imageUrl: String? = null
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ class ReportNotificationViewHolder(
|
||||||
binding.notificationSummary.text = itemView.context.getString(
|
binding.notificationSummary.text = itemView.context.getString(
|
||||||
R.string.notification_summary_report_format,
|
R.string.notification_summary_report_format,
|
||||||
getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time),
|
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)
|
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 {
|
protected fun hasPreviewableAttachment(attachments: List<Attachment>): Boolean {
|
||||||
for (attachment in attachments) {
|
for (attachment in attachments) {
|
||||||
if (attachment.type == Attachment.Type.UNKNOWN) return false
|
if (attachment.type == Attachment.Type.UNKNOWN) return false
|
||||||
|
|
||||||
|
if (attachment.meta?.original?.width == null && attachment.meta?.small?.width == null) return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ package app.pachli.appstore
|
||||||
|
|
||||||
import app.pachli.core.accounts.AccountManager
|
import app.pachli.core.accounts.AccountManager
|
||||||
import app.pachli.core.database.dao.TimelineDao
|
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 javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -10,11 +12,12 @@ import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
class CacheUpdater @Inject constructor(
|
class CacheUpdater @Inject constructor(
|
||||||
eventHub: EventHub,
|
eventHub: EventHub,
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
timelineDao: TimelineDao,
|
timelineDao: TimelineDao,
|
||||||
gson: Gson,
|
moshi: Moshi,
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
|
@ -34,7 +37,7 @@ class CacheUpdater @Inject constructor(
|
||||||
is StatusDeletedEvent ->
|
is StatusDeletedEvent ->
|
||||||
timelineDao.delete(accountId, event.statusId)
|
timelineDao.delete(accountId, event.statusId)
|
||||||
is PollVoteEvent -> {
|
is PollVoteEvent -> {
|
||||||
val pollString = gson.toJson(event.poll)
|
val pollString = moshi.adapter<Poll>().toJson(event.poll)
|
||||||
timelineDao.setVoted(accountId, event.statusId, pollString)
|
timelineDao.setVoted(accountId, event.statusId, pollString)
|
||||||
}
|
}
|
||||||
is PinEvent ->
|
is PinEvent ->
|
||||||
|
|
|
@ -18,7 +18,7 @@ data class StatusDeletedEvent(val statusId: String) : Event
|
||||||
/** A status the user wrote was successfully sent */
|
/** A status the user wrote was successfully sent */
|
||||||
// TODO: Rename, calling it "Composed" does not imply anything about the sent state
|
// TODO: Rename, calling it "Composed" does not imply anything about the sent state
|
||||||
data class StatusComposedEvent(val status: Status) : Event
|
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 StatusEditedEvent(val originalId: String, val status: Status) : Event
|
||||||
data class ProfileEditedEvent(val newProfileData: Account) : Event
|
data class ProfileEditedEvent(val newProfileData: Account) : Event
|
||||||
data class FilterChangedEvent(val filterKind: Filter.Kind) : 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.Links
|
||||||
import app.pachli.core.network.model.Notification
|
import app.pachli.core.network.model.Notification
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
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 javax.inject.Inject
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
@ -37,9 +38,10 @@ private val INVALID = LoadResult.Invalid<String, Notification>()
|
||||||
/** [PagingSource] for Mastodon Notifications, identified by the Notification ID */
|
/** [PagingSource] for Mastodon Notifications, identified by the Notification ID */
|
||||||
class NotificationsPagingSource @Inject constructor(
|
class NotificationsPagingSource @Inject constructor(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val gson: Gson,
|
private val moshi: Moshi,
|
||||||
private val notificationFilter: Set<Notification.Type>,
|
private val notificationFilter: Set<Notification.Type>,
|
||||||
) : PagingSource<String, Notification>() {
|
) : PagingSource<String, Notification>() {
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Notification> {
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, Notification> {
|
||||||
Timber.d("load() with ${params.javaClass.simpleName} for key: ${params.key}")
|
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"
|
if (errorBody.isBlank()) return@let "no reason given"
|
||||||
|
|
||||||
val error = try {
|
val error = try {
|
||||||
gson.fromJson(errorBody, Error::class.java)
|
moshi.adapter<Error>().fromJson(errorBody)!!
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return@let "$errorBody ($e)"
|
return@let "$errorBody ($e)"
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val desc = error.error_description) {
|
when (val desc = error.errorDescription) {
|
||||||
null -> error.error
|
null -> error.error
|
||||||
else -> "${error.error}: $desc"
|
else -> "${error.error}: $desc"
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ import androidx.paging.PagingSource
|
||||||
import app.pachli.core.common.di.ApplicationScope
|
import app.pachli.core.common.di.ApplicationScope
|
||||||
import app.pachli.core.network.model.Notification
|
import app.pachli.core.network.model.Notification
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
@ -36,7 +36,7 @@ import timber.log.Timber
|
||||||
|
|
||||||
class NotificationsRepository @Inject constructor(
|
class NotificationsRepository @Inject constructor(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val gson: Gson,
|
private val moshi: Moshi,
|
||||||
@ApplicationScope private val externalScope: CoroutineScope,
|
@ApplicationScope private val externalScope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
private var factory: InvalidatingPagingSourceFactory<String, Notification>? = null
|
private var factory: InvalidatingPagingSourceFactory<String, Notification>? = null
|
||||||
|
@ -53,7 +53,7 @@ class NotificationsRepository @Inject constructor(
|
||||||
Timber.d("getNotificationsStream(), filtering: $filter")
|
Timber.d("getNotificationsStream(), filtering: $filter")
|
||||||
|
|
||||||
factory = InvalidatingPagingSourceFactory {
|
factory = InvalidatingPagingSourceFactory {
|
||||||
NotificationsPagingSource(mastodonApi, gson, filter)
|
NotificationsPagingSource(mastodonApi, moshi, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pager(
|
return Pager(
|
||||||
|
|
|
@ -51,7 +51,7 @@ class ScheduledStatusViewModel @Inject constructor(
|
||||||
pagingSourceFactory.remove(status)
|
pagingSourceFactory.remove(status)
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ 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.model.SearchResult
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import at.connyduck.calladapter.networkresult.getOrElse
|
import at.connyduck.calladapter.networkresult.getOrElse
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class SearchPagingSource<T : Any>(
|
class SearchPagingSource<T : Any>(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
|
@ -61,7 +62,10 @@ class SearchPagingSource<T : Any>(
|
||||||
limit = params.loadSize,
|
limit = params.loadSize,
|
||||||
offset = currentKey,
|
offset = currentKey,
|
||||||
following = false,
|
following = false,
|
||||||
).getOrElse { return LoadResult.Error(it) }
|
).getOrElse {
|
||||||
|
Timber.w(it)
|
||||||
|
return LoadResult.Error(it)
|
||||||
|
}
|
||||||
|
|
||||||
val res = parser(data)
|
val res = parser(data)
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,6 @@ abstract class SearchFragment<T : Any> :
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.addLoadStateListener { loadState ->
|
adapter.addLoadStateListener { loadState ->
|
||||||
|
|
||||||
if (loadState.refresh is LoadState.Error) {
|
if (loadState.refresh is LoadState.Error) {
|
||||||
showError()
|
showError()
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ import app.pachli.util.EmptyPagingSource
|
||||||
import app.pachli.viewdata.StatusViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -66,7 +66,7 @@ class CachedTimelineRepository @Inject constructor(
|
||||||
val timelineDao: TimelineDao,
|
val timelineDao: TimelineDao,
|
||||||
private val remoteKeyDao: RemoteKeyDao,
|
private val remoteKeyDao: RemoteKeyDao,
|
||||||
private val translatedStatusDao: TranslatedStatusDao,
|
private val translatedStatusDao: TranslatedStatusDao,
|
||||||
private val gson: Gson,
|
private val moshi: Moshi,
|
||||||
@ApplicationScope private val externalScope: CoroutineScope,
|
@ApplicationScope private val externalScope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
private var factory: InvalidatingPagingSourceFactory<Int, TimelineStatusWithAccount>? = null
|
private var factory: InvalidatingPagingSourceFactory<Int, TimelineStatusWithAccount>? = null
|
||||||
|
@ -118,7 +118,7 @@ class CachedTimelineRepository @Inject constructor(
|
||||||
transactionProvider,
|
transactionProvider,
|
||||||
timelineDao,
|
timelineDao,
|
||||||
remoteKeyDao,
|
remoteKeyDao,
|
||||||
gson,
|
moshi,
|
||||||
),
|
),
|
||||||
pagingSourceFactory = factory!!,
|
pagingSourceFactory = factory!!,
|
||||||
).flow
|
).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.Links
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
@ -54,7 +54,7 @@ class CachedTimelineRemoteMediator(
|
||||||
private val transactionProvider: TransactionProvider,
|
private val transactionProvider: TransactionProvider,
|
||||||
private val timelineDao: TimelineDao,
|
private val timelineDao: TimelineDao,
|
||||||
private val remoteKeyDao: RemoteKeyDao,
|
private val remoteKeyDao: RemoteKeyDao,
|
||||||
private val gson: Gson,
|
private val moshi: Moshi,
|
||||||
) : RemoteMediator<Int, TimelineStatusWithAccount>() {
|
) : RemoteMediator<Int, TimelineStatusWithAccount>() {
|
||||||
private val activeAccount = accountManager.activeAccount!!
|
private val activeAccount = accountManager.activeAccount!!
|
||||||
|
|
||||||
|
@ -254,9 +254,9 @@ class CachedTimelineRemoteMediator(
|
||||||
@Transaction
|
@Transaction
|
||||||
private suspend fun insertStatuses(statuses: List<Status>) {
|
private suspend fun insertStatuses(statuses: List<Status>) {
|
||||||
for (status in statuses) {
|
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 {
|
status.reblog?.account?.let {
|
||||||
val account = TimelineAccountEntity.from(it, activeAccount.id, gson)
|
val account = TimelineAccountEntity.from(it, activeAccount.id, moshi)
|
||||||
timelineDao.insertAccount(account)
|
timelineDao.insertAccount(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +264,7 @@ class CachedTimelineRemoteMediator(
|
||||||
TimelineStatusEntity.from(
|
TimelineStatusEntity.from(
|
||||||
status,
|
status,
|
||||||
timelineUserId = activeAccount.id,
|
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.usecase.TimelineCases
|
||||||
import app.pachli.util.StatusDisplayOptionsRepository
|
import app.pachli.util.StatusDisplayOptionsRepository
|
||||||
import app.pachli.viewdata.StatusViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -61,7 +61,7 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||||
private val gson: Gson,
|
private val moshi: Moshi,
|
||||||
) : TimelineViewModel(
|
) : TimelineViewModel(
|
||||||
savedStateHandle,
|
savedStateHandle,
|
||||||
timelineCases,
|
timelineCases,
|
||||||
|
@ -93,7 +93,7 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
.map {
|
.map {
|
||||||
StatusViewData.from(
|
StatusViewData.from(
|
||||||
it,
|
it,
|
||||||
gson,
|
moshi,
|
||||||
isExpanded = activeAccount.alwaysOpenSpoiler,
|
isExpanded = activeAccount.alwaysOpenSpoiler,
|
||||||
isShowingContent = activeAccount.alwaysShowSensitiveMedia,
|
isShowingContent = activeAccount.alwaysShowSensitiveMedia,
|
||||||
)
|
)
|
||||||
|
|
|
@ -48,7 +48,7 @@ import app.pachli.viewdata.StatusViewData
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import at.connyduck.calladapter.networkresult.getOrElse
|
import at.connyduck.calladapter.networkresult.getOrElse
|
||||||
import at.connyduck.calladapter.networkresult.getOrThrow
|
import at.connyduck.calladapter.networkresult.getOrThrow
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -69,7 +69,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
eventHub: EventHub,
|
eventHub: EventHub,
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
private val timelineDao: TimelineDao,
|
private val timelineDao: TimelineDao,
|
||||||
private val gson: Gson,
|
private val moshi: Moshi,
|
||||||
private val repository: CachedTimelineRepository,
|
private val repository: CachedTimelineRepository,
|
||||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||||
private val filtersRepository: FiltersRepository,
|
private val filtersRepository: FiltersRepository,
|
||||||
|
@ -133,7 +133,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
|
|
||||||
var detailedStatus = if (timelineStatusWithAccount != null) {
|
var detailedStatus = if (timelineStatusWithAccount != null) {
|
||||||
Timber.d("Loaded status from local timeline")
|
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
|
// 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
|
// this the status IDs will be different between the status that's displayed with
|
||||||
|
@ -152,7 +152,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
StatusViewData.from(
|
StatusViewData.from(
|
||||||
timelineStatusWithAccount,
|
timelineStatusWithAccount,
|
||||||
gson,
|
moshi,
|
||||||
isExpanded = alwaysOpenSpoiler,
|
isExpanded = alwaysOpenSpoiler,
|
||||||
isShowingContent = (alwaysShowSensitiveMedia || !status.actionableStatus.sensitive),
|
isShowingContent = (alwaysShowSensitiveMedia || !status.actionableStatus.sensitive),
|
||||||
isDetailed = true,
|
isDetailed = true,
|
||||||
|
|
|
@ -39,6 +39,7 @@ import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import app.pachli.util.unsafeLazy
|
import app.pachli.util.unsafeLazy
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -222,12 +223,21 @@ class SendStatusService : Service() {
|
||||||
)
|
)
|
||||||
|
|
||||||
val sendResult = if (isNew) {
|
val sendResult = if (isNew) {
|
||||||
mastodonApi.createStatus(
|
if (newStatus.scheduledAt == null) {
|
||||||
"Bearer " + account.accessToken,
|
mastodonApi.createStatus(
|
||||||
account.domain,
|
"Bearer " + account.accessToken,
|
||||||
statusToSend.idempotencyKey,
|
account.domain,
|
||||||
newStatus,
|
statusToSend.idempotencyKey,
|
||||||
)
|
newStatus,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mastodonApi.createScheduledStatus(
|
||||||
|
"Bearer " + account.accessToken,
|
||||||
|
account.domain,
|
||||||
|
statusToSend.idempotencyKey,
|
||||||
|
newStatus,
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mastodonApi.editStatus(
|
mastodonApi.editStatus(
|
||||||
statusToSend.statusId!!,
|
statusToSend.statusId!!,
|
||||||
|
@ -250,16 +260,16 @@ class SendStatusService : Service() {
|
||||||
val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
|
val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
|
||||||
|
|
||||||
if (scheduled) {
|
if (scheduled) {
|
||||||
eventHub.dispatch(StatusScheduledEvent(sentStatus))
|
eventHub.dispatch(StatusScheduledEvent)
|
||||||
} else if (!isNew) {
|
} else if (!isNew) {
|
||||||
eventHub.dispatch(StatusEditedEvent(statusToSend.statusId!!, sentStatus))
|
eventHub.dispatch(StatusEditedEvent(statusToSend.statusId!!, sentStatus as Status))
|
||||||
} else {
|
} else {
|
||||||
eventHub.dispatch(StatusComposedEvent(sentStatus))
|
eventHub.dispatch(StatusComposedEvent(sentStatus as Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.cancel(statusId)
|
notificationManager.cancel(statusId)
|
||||||
}, { throwable ->
|
}, { throwable ->
|
||||||
Timber.w("failed sending status", throwable)
|
Timber.w("failed sending status: $throwable")
|
||||||
failOrRetry(throwable, statusId)
|
failOrRetry(throwable, statusId)
|
||||||
})
|
})
|
||||||
stopSelfWhenDone()
|
stopSelfWhenDone()
|
||||||
|
@ -267,12 +277,13 @@ class SendStatusService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun failOrRetry(throwable: Throwable, statusId: Int) {
|
private suspend fun failOrRetry(throwable: Throwable, statusId: Int) {
|
||||||
if (throwable is HttpException) {
|
when (throwable) {
|
||||||
// the server refused to accept, save status & show error message
|
// the server refused to accept, save status & show error message
|
||||||
failSending(statusId)
|
is HttpException -> failSending(statusId)
|
||||||
} else {
|
|
||||||
// a network problem occurred, let's retry sending the status
|
// 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> {
|
fun List<Attachment>.aspectRatios(): List<Double> {
|
||||||
return map { attachment ->
|
return map { attachment ->
|
||||||
// clamp ratio between 2:1 & 1:2, defaulting to 16:9
|
// clamp ratio between 2:1 & 1:2, defaulting to 16:9
|
||||||
val size = (attachment.meta?.small ?: attachment.meta?.original) ?: return@map 1.7778
|
val (width, height, aspect) = (attachment.meta?.small ?: attachment.meta?.original) ?: return@map 1.7778
|
||||||
val aspect = if (size.aspect > 0) size.aspect else size.width.toDouble() / size.height
|
width ?: return@map 1.778
|
||||||
aspect.coerceIn(0.5, 2.0)
|
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.parseAsMastodonHtml
|
||||||
import app.pachli.core.network.replaceCrashingCharacters
|
import app.pachli.core.network.replaceCrashingCharacters
|
||||||
import app.pachli.util.shouldTrimStatus
|
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
|
* Interface for the data shown when viewing a status, or something that wraps
|
||||||
|
@ -273,13 +273,13 @@ data class StatusViewData(
|
||||||
|
|
||||||
fun from(
|
fun from(
|
||||||
timelineStatusWithAccount: TimelineStatusWithAccount,
|
timelineStatusWithAccount: TimelineStatusWithAccount,
|
||||||
gson: Gson,
|
moshi: Moshi,
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
isShowingContent: Boolean,
|
isShowingContent: Boolean,
|
||||||
isDetailed: Boolean = false,
|
isDetailed: Boolean = false,
|
||||||
translationState: TranslationState = TranslationState.SHOW_ORIGINAL,
|
translationState: TranslationState = TranslationState.SHOW_ORIGINAL,
|
||||||
): StatusViewData {
|
): StatusViewData {
|
||||||
val status = timelineStatusWithAccount.toStatus(gson)
|
val status = timelineStatusWithAccount.toStatus(moshi)
|
||||||
return StatusViewData(
|
return StatusViewData(
|
||||||
status = status,
|
status = status,
|
||||||
translation = timelineStatusWithAccount.translatedStatus,
|
translation = timelineStatusWithAccount.translatedStatus,
|
||||||
|
|
|
@ -2,9 +2,13 @@ package app.pachli
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import app.pachli.core.database.model.TranslationState
|
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.Status
|
||||||
import app.pachli.viewdata.StatusViewData
|
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.assertEquals
|
||||||
import org.junit.Assert.assertNotEquals
|
import org.junit.Assert.assertNotEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -12,6 +16,9 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class StatusComparisonTest {
|
class StatusComparisonTest {
|
||||||
|
private val moshi = Moshi.Builder()
|
||||||
|
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||||
|
.add(GuardedAdapterFactory()).build()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `two equal statuses - should be equal`() {
|
fun `two equal statuses - should be equal`() {
|
||||||
|
@ -36,8 +43,6 @@ class StatusComparisonTest {
|
||||||
assertNotEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
assertNotEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val gson = Gson()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `two equal status view data - should be equal`() {
|
fun `two equal status view data - should be equal`() {
|
||||||
val viewdata1 = StatusViewData(
|
val viewdata1 = StatusViewData(
|
||||||
|
@ -95,6 +100,7 @@ class StatusComparisonTest {
|
||||||
assertNotEquals(viewdata1, viewdata2)
|
assertNotEquals(viewdata1, viewdata2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
private fun createStatus(
|
private fun createStatus(
|
||||||
id: String = "123456",
|
id: String = "123456",
|
||||||
content: String = """
|
content: String = """
|
||||||
|
@ -206,6 +212,6 @@ class StatusComparisonTest {
|
||||||
"poll": null
|
"poll": null
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".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 androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import app.pachli.core.network.model.Notification
|
import app.pachli.core.network.model.Notification
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -39,15 +39,15 @@ class NotificationsPagingSourceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `load() returns error message on HTTP error`() = runTest {
|
fun `load() returns error message on HTTP error`() = runTest {
|
||||||
// Given
|
// Given
|
||||||
val jsonError = "{error: 'This is an error'}".toResponseBody()
|
val jsonError = """{"error": "This is an error"}""".toResponseBody()
|
||||||
val mockApi: MastodonApi = mock {
|
val mockApi: MastodonApi = mock {
|
||||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||||
}
|
}
|
||||||
|
|
||||||
val filter = emptySet<Notification.Type>()
|
val filter = emptySet<Notification.Type>()
|
||||||
val gson = Gson()
|
val moshi = Moshi.Builder().build()
|
||||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
val pagingSource = NotificationsPagingSource(mockApi, moshi, filter)
|
||||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
@ -65,15 +65,15 @@ class NotificationsPagingSourceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `load() returns extended error message on HTTP error`() = runTest {
|
fun `load() returns extended error message on HTTP error`() = runTest {
|
||||||
// Given
|
// 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 {
|
val mockApi: MastodonApi = mock {
|
||||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||||
}
|
}
|
||||||
|
|
||||||
val filter = emptySet<Notification.Type>()
|
val filter = emptySet<Notification.Type>()
|
||||||
val gson = Gson()
|
val moshi = Moshi.Builder().build()
|
||||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
val pagingSource = NotificationsPagingSource(mockApi, moshi, filter)
|
||||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
@ -91,15 +91,15 @@ class NotificationsPagingSourceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `load() returns default error message on empty HTTP error`() = runTest {
|
fun `load() returns default error message on empty HTTP error`() = runTest {
|
||||||
// Given
|
// Given
|
||||||
val jsonError = "{}".toResponseBody()
|
val jsonError = "".toResponseBody()
|
||||||
val mockApi: MastodonApi = mock {
|
val mockApi: MastodonApi = mock {
|
||||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||||
}
|
}
|
||||||
|
|
||||||
val filter = emptySet<Notification.Type>()
|
val filter = emptySet<Notification.Type>()
|
||||||
val gson = Gson()
|
val moshi = Moshi.Builder().build()
|
||||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
val pagingSource = NotificationsPagingSource(mockApi, moshi, filter)
|
||||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
@ -124,8 +124,8 @@ class NotificationsPagingSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
val filter = emptySet<Notification.Type>()
|
val filter = emptySet<Notification.Type>()
|
||||||
val gson = Gson()
|
val moshi = Moshi.Builder().build()
|
||||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
val pagingSource = NotificationsPagingSource(mockApi, moshi, filter)
|
||||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
@ -134,7 +134,7 @@ class NotificationsPagingSourceTest {
|
||||||
// Then
|
// Then
|
||||||
assertTrue(loadResult is PagingSource.LoadResult.Error)
|
assertTrue(loadResult is PagingSource.LoadResult.Error)
|
||||||
assertEquals(
|
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,
|
(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.RemoteKeyEntity
|
||||||
import app.pachli.core.database.model.RemoteKeyKind
|
import app.pachli.core.database.model.RemoteKeyKind
|
||||||
import app.pachli.core.database.model.TimelineStatusWithAccount
|
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.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.io.IOException
|
||||||
|
import java.util.Date
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
@ -60,12 +63,17 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
|
|
||||||
private lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory<Int, TimelineStatusWithAccount>
|
private lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory<Int, TimelineStatusWithAccount>
|
||||||
|
|
||||||
|
private val moshi: Moshi = Moshi.Builder()
|
||||||
|
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||||
|
.add(GuardedAdapterFactory())
|
||||||
|
.build()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||||
.addTypeConverter(Converters(Gson()))
|
.addTypeConverter(Converters(moshi))
|
||||||
.build()
|
.build()
|
||||||
transactionProvider = TransactionProvider(db)
|
transactionProvider = TransactionProvider(db)
|
||||||
|
|
||||||
|
@ -91,7 +99,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
transactionProvider = transactionProvider,
|
transactionProvider = transactionProvider,
|
||||||
timelineDao = db.timelineDao(),
|
timelineDao = db.timelineDao(),
|
||||||
remoteKeyDao = db.remoteKeyDao(),
|
remoteKeyDao = db.remoteKeyDao(),
|
||||||
gson = Gson(),
|
moshi = moshi,
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||||
|
@ -114,7 +122,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
transactionProvider = transactionProvider,
|
transactionProvider = transactionProvider,
|
||||||
timelineDao = db.timelineDao(),
|
timelineDao = db.timelineDao(),
|
||||||
remoteKeyDao = db.remoteKeyDao(),
|
remoteKeyDao = db.remoteKeyDao(),
|
||||||
gson = Gson(),
|
moshi = moshi,
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||||
|
@ -134,7 +142,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
transactionProvider = transactionProvider,
|
transactionProvider = transactionProvider,
|
||||||
timelineDao = db.timelineDao(),
|
timelineDao = db.timelineDao(),
|
||||||
remoteKeyDao = db.remoteKeyDao(),
|
remoteKeyDao = db.remoteKeyDao(),
|
||||||
gson = Gson(),
|
moshi = moshi,
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -174,7 +182,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
transactionProvider = transactionProvider,
|
transactionProvider = transactionProvider,
|
||||||
timelineDao = db.timelineDao(),
|
timelineDao = db.timelineDao(),
|
||||||
remoteKeyDao = db.remoteKeyDao(),
|
remoteKeyDao = db.remoteKeyDao(),
|
||||||
gson = Gson(),
|
moshi = moshi,
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -228,7 +236,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
transactionProvider = transactionProvider,
|
transactionProvider = transactionProvider,
|
||||||
timelineDao = db.timelineDao(),
|
timelineDao = db.timelineDao(),
|
||||||
remoteKeyDao = db.remoteKeyDao(),
|
remoteKeyDao = db.remoteKeyDao(),
|
||||||
gson = Gson(),
|
moshi = moshi,
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -289,7 +297,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
transactionProvider = transactionProvider,
|
transactionProvider = transactionProvider,
|
||||||
timelineDao = db.timelineDao(),
|
timelineDao = db.timelineDao(),
|
||||||
remoteKeyDao = db.remoteKeyDao(),
|
remoteKeyDao = db.remoteKeyDao(),
|
||||||
gson = Gson(),
|
moshi = moshi,
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
|
|
@ -35,7 +35,7 @@ import app.pachli.core.testing.rules.MainCoroutineRule
|
||||||
import app.pachli.usecase.TimelineCases
|
import app.pachli.usecase.TimelineCases
|
||||||
import app.pachli.util.StatusDisplayOptionsRepository
|
import app.pachli.util.StatusDisplayOptionsRepository
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
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.CustomTestApplication
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
@ -92,6 +92,9 @@ abstract class CachedTimelineViewModelTestBase {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
|
lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var moshi: Moshi
|
||||||
|
|
||||||
protected lateinit var timelineCases: TimelineCases
|
protected lateinit var timelineCases: TimelineCases
|
||||||
protected lateinit var viewModel: TimelineViewModel
|
protected lateinit var viewModel: TimelineViewModel
|
||||||
|
|
||||||
|
@ -160,7 +163,7 @@ abstract class CachedTimelineViewModelTestBase {
|
||||||
accountManager,
|
accountManager,
|
||||||
statusDisplayOptionsRepository,
|
statusDisplayOptionsRepository,
|
||||||
sharedPreferencesRepository,
|
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.TimelineStatusEntity
|
||||||
import app.pachli.core.database.model.TimelineStatusWithAccount
|
import app.pachli.core.database.model.TimelineStatusWithAccount
|
||||||
import app.pachli.core.database.model.TranslationState
|
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.Status
|
||||||
import app.pachli.core.network.model.TimelineAccount
|
import app.pachli.core.network.model.TimelineAccount
|
||||||
import app.pachli.viewdata.StatusViewData
|
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
|
import java.util.Date
|
||||||
|
|
||||||
private val fixedDate = Date(1638889052000)
|
private val fixedDate = Date(1638889052000)
|
||||||
|
@ -96,18 +98,21 @@ fun mockStatusEntityWithAccount(
|
||||||
expanded: Boolean = false,
|
expanded: Boolean = false,
|
||||||
): TimelineStatusWithAccount {
|
): TimelineStatusWithAccount {
|
||||||
val mockedStatus = mockStatus(id)
|
val mockedStatus = mockStatus(id)
|
||||||
val gson = Gson()
|
val moshi = Moshi.Builder()
|
||||||
|
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||||
|
.add(GuardedAdapterFactory())
|
||||||
|
.build()
|
||||||
|
|
||||||
return TimelineStatusWithAccount(
|
return TimelineStatusWithAccount(
|
||||||
status = TimelineStatusEntity.from(
|
status = TimelineStatusEntity.from(
|
||||||
mockedStatus,
|
mockedStatus,
|
||||||
timelineUserId = userId,
|
timelineUserId = userId,
|
||||||
gson = gson,
|
moshi = moshi,
|
||||||
),
|
),
|
||||||
account = TimelineAccountEntity.from(
|
account = TimelineAccountEntity.from(
|
||||||
mockedStatus.account,
|
mockedStatus.account,
|
||||||
accountId = userId,
|
accountId = userId,
|
||||||
gson = gson,
|
moshi = moshi,
|
||||||
),
|
),
|
||||||
viewData = StatusViewDataEntity(
|
viewData = StatusViewDataEntity(
|
||||||
serverId = id,
|
serverId = id,
|
||||||
|
|
|
@ -26,7 +26,7 @@ import app.pachli.core.preferences.SharedPreferencesRepository
|
||||||
import app.pachli.usecase.TimelineCases
|
import app.pachli.usecase.TimelineCases
|
||||||
import app.pachli.util.StatusDisplayOptionsRepository
|
import app.pachli.util.StatusDisplayOptionsRepository
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
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.BindValue
|
||||||
import dagger.hilt.android.testing.CustomTestApplication
|
import dagger.hilt.android.testing.CustomTestApplication
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
@ -111,7 +111,7 @@ class ViewThreadViewModelTest {
|
||||||
lateinit var timelineDao: TimelineDao
|
lateinit var timelineDao: TimelineDao
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var gson: Gson
|
lateinit var moshi: Moshi
|
||||||
|
|
||||||
@BindValue @JvmField
|
@BindValue @JvmField
|
||||||
val filtersRepository: FiltersRepository = mock()
|
val filtersRepository: FiltersRepository = mock()
|
||||||
|
@ -188,7 +188,7 @@ class ViewThreadViewModelTest {
|
||||||
eventHub,
|
eventHub,
|
||||||
accountManager,
|
accountManager,
|
||||||
timelineDao,
|
timelineDao,
|
||||||
gson,
|
moshi,
|
||||||
cachedTimelineRepository,
|
cachedTimelineRepository,
|
||||||
statusDisplayOptionsRepository,
|
statusDisplayOptionsRepository,
|
||||||
filtersRepository,
|
filtersRepository,
|
||||||
|
|
|
@ -19,9 +19,9 @@ package app.pachli.di
|
||||||
|
|
||||||
import app.pachli.components.compose.MediaUploader
|
import app.pachli.components.compose.MediaUploader
|
||||||
import app.pachli.core.network.di.NetworkModule
|
import app.pachli.core.network.di.NetworkModule
|
||||||
import app.pachli.core.network.json.Rfc3339DateJsonAdapter
|
import app.pachli.core.network.json.GuardedAdapter
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import com.google.gson.GsonBuilder
|
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
@ -39,9 +39,10 @@ import org.mockito.kotlin.mock
|
||||||
object FakeNetworkModule {
|
object FakeNetworkModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGson(): Gson = GsonBuilder()
|
fun providesMoshi(): Moshi = Moshi.Builder()
|
||||||
.registerTypeAdapter(Date::class.java, Rfc3339DateJsonAdapter())
|
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||||
.create()
|
.add(GuardedAdapter.Companion.GuardedAdapterFactory())
|
||||||
|
.build()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
@ -35,6 +35,8 @@ dependencies {
|
||||||
implementation(projects.core.network)
|
implementation(projects.core.network)
|
||||||
implementation(projects.core.preferences)
|
implementation(projects.core.preferences)
|
||||||
|
|
||||||
// Because of the use of @SerializedName in DraftEntity
|
// Because of the use of @Json in DraftEntity
|
||||||
implementation(libs.gson)
|
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.Status
|
||||||
import app.pachli.core.network.model.TranslatedAttachment
|
import app.pachli.core.network.model.TranslatedAttachment
|
||||||
import app.pachli.core.network.model.TranslatedPoll
|
import app.pachli.core.network.model.TranslatedPoll
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.squareup.moshi.adapter
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
@ProvidedTypeConverter
|
@ProvidedTypeConverter
|
||||||
@Singleton
|
@Singleton
|
||||||
class Converters @Inject constructor(
|
class Converters @Inject constructor(
|
||||||
private val gson: Gson,
|
private val moshi: Moshi,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToEmojiList(emojiListJson: String?): List<Emoji>? {
|
fun jsonToEmojiList(json: String?): List<Emoji>? {
|
||||||
return gson.fromJson(emojiListJson, object : TypeToken<List<Emoji>>() {}.type)
|
return json?.let { moshi.adapter<List<Emoji>>().fromJson(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun emojiListToJson(emojiList: List<Emoji>?): String {
|
fun emojiListToJson(emojiList: List<Emoji>?): String {
|
||||||
return gson.toJson(emojiList)
|
return moshi.adapter<List<Emoji>?>().toJson(emojiList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
|
@ -81,52 +82,52 @@ class Converters @Inject constructor(
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun accountToJson(account: ConversationAccountEntity?): String {
|
fun accountToJson(account: ConversationAccountEntity?): String {
|
||||||
return gson.toJson(account)
|
return moshi.adapter<ConversationAccountEntity>().toJson(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToAccount(accountJson: String?): ConversationAccountEntity? {
|
fun jsonToAccount(accountJson: String?): ConversationAccountEntity? {
|
||||||
return gson.fromJson(accountJson, ConversationAccountEntity::class.java)
|
return accountJson?.let { moshi.adapter<ConversationAccountEntity>().fromJson(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun accountListToJson(accountList: List<ConversationAccountEntity>?): String {
|
fun accountListToJson(accountList: List<ConversationAccountEntity>?): String {
|
||||||
return gson.toJson(accountList)
|
return moshi.adapter<List<ConversationAccountEntity>>().toJson(accountList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToAccountList(accountListJson: String?): List<ConversationAccountEntity>? {
|
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
|
@TypeConverter
|
||||||
fun attachmentListToJson(attachmentList: List<Attachment>?): String {
|
fun attachmentListToJson(attachmentList: List<Attachment>?): String {
|
||||||
return gson.toJson(attachmentList)
|
return moshi.adapter<List<Attachment>?>().toJson(attachmentList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToAttachmentList(attachmentListJson: String?): List<Attachment>? {
|
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
|
@TypeConverter
|
||||||
fun mentionListToJson(mentionArray: List<Status.Mention>?): String? {
|
fun mentionListToJson(mentionArray: List<Status.Mention>?): String? {
|
||||||
return gson.toJson(mentionArray)
|
return moshi.adapter<List<Status.Mention>?>().toJson(mentionArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToMentionArray(mentionListJson: String?): List<Status.Mention>? {
|
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
|
@TypeConverter
|
||||||
fun tagListToJson(tagArray: List<HashTag>?): String? {
|
fun tagListToJson(tagArray: List<HashTag>?): String? {
|
||||||
return gson.toJson(tagArray)
|
return moshi.adapter<List<HashTag>?>().toJson(tagArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToTagArray(tagListJson: String?): List<HashTag>? {
|
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
|
@TypeConverter
|
||||||
|
@ -141,61 +142,61 @@ class Converters @Inject constructor(
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun pollToJson(poll: Poll?): String? {
|
fun pollToJson(poll: Poll?): String? {
|
||||||
return gson.toJson(poll)
|
return moshi.adapter<Poll?>().toJson(poll)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToPoll(pollJson: String?): Poll? {
|
fun jsonToPoll(pollJson: String?): Poll? {
|
||||||
return gson.fromJson(pollJson, Poll::class.java)
|
return pollJson?.let { moshi.adapter<Poll?>().fromJson(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun newPollToJson(newPoll: NewPoll?): String? {
|
fun newPollToJson(newPoll: NewPoll?): String? {
|
||||||
return gson.toJson(newPoll)
|
return moshi.adapter<NewPoll?>().toJson(newPoll)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToNewPoll(newPollJson: String?): NewPoll? {
|
fun jsonToNewPoll(newPollJson: String?): NewPoll? {
|
||||||
return gson.fromJson(newPollJson, NewPoll::class.java)
|
return newPollJson?.let { moshi.adapter<NewPoll?>().fromJson(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>?): String? {
|
fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>?): String? {
|
||||||
return gson.toJson(draftAttachments)
|
return moshi.adapter<List<DraftAttachment>?>().toJson(draftAttachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List<DraftAttachment>? {
|
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
|
@TypeConverter
|
||||||
fun filterResultListToJson(filterResults: List<FilterResult>?): String? {
|
fun filterResultListToJson(filterResults: List<FilterResult>?): String? {
|
||||||
return gson.toJson(filterResults)
|
return moshi.adapter<List<FilterResult>?>().toJson(filterResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToFilterResultList(filterResultListJson: String?): List<FilterResult>? {
|
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
|
@TypeConverter
|
||||||
fun translatedPolltoJson(translatedPoll: TranslatedPoll?): String? {
|
fun translatedPolltoJson(translatedPoll: TranslatedPoll?): String? {
|
||||||
return gson.toJson(translatedPoll)
|
return moshi.adapter<TranslatedPoll?>().toJson(translatedPoll)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToTranslatedPoll(translatedPollJson: String?): TranslatedPoll? {
|
fun jsonToTranslatedPoll(translatedPollJson: String?): TranslatedPoll? {
|
||||||
return gson.fromJson(translatedPollJson, TranslatedPoll::class.java)
|
return translatedPollJson?.let { moshi.adapter<TranslatedPoll?>().fromJson(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun translatedAttachmentToJson(translatedAttachment: List<TranslatedAttachment>?): String {
|
fun translatedAttachmentToJson(translatedAttachment: List<TranslatedAttachment>?): String {
|
||||||
return gson.toJson(translatedAttachment)
|
return moshi.adapter<List<TranslatedAttachment>?>().toJson(translatedAttachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun jsonToTranslatedAttachment(translatedAttachmentJson: String): List<TranslatedAttachment>? {
|
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.Poll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.network.model.TimelineAccount
|
import app.pachli.core.network.model.TimelineAccount
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Entity(primaryKeys = ["id", "accountId"])
|
@Entity(primaryKeys = ["id", "accountId"])
|
||||||
|
@ -65,6 +66,7 @@ data class ConversationEntity(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class ConversationAccountEntity(
|
data class ConversationAccountEntity(
|
||||||
val id: String,
|
val id: String,
|
||||||
val localUsername: 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.Attachment
|
||||||
import app.pachli.core.network.model.NewPoll
|
import app.pachli.core.network.model.NewPoll
|
||||||
import app.pachli.core.network.model.Status
|
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
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -49,17 +50,13 @@ data class DraftEntity(
|
||||||
val statusId: String?,
|
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
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class DraftAttachment(
|
data class DraftAttachment(
|
||||||
@SerializedName(value = "uriString", alternate = ["e", "i"]) val uriString: String,
|
@Json(name = "uriString") val uriString: String,
|
||||||
@SerializedName(value = "description", alternate = ["f", "j"]) val description: String?,
|
@Json(name = "description") val description: String?,
|
||||||
@SerializedName(value = "focus") val focus: Attachment.Focus?,
|
@Json(name = "focus") val focus: Attachment.Focus?,
|
||||||
@SerializedName(value = "type", alternate = ["g", "k"]) val type: Type,
|
@Json(name = "type") val type: Type,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
val uri: Uri
|
val uri: Uri
|
||||||
get() = uriString.toUri()
|
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.Poll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.network.model.TimelineAccount
|
import app.pachli.core.network.model.TimelineAccount
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.squareup.moshi.adapter
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,7 +99,8 @@ data class TimelineStatusEntity(
|
||||||
val filtered: List<FilterResult>?,
|
val filtered: List<FilterResult>?,
|
||||||
) {
|
) {
|
||||||
companion object {
|
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,
|
serverId = status.id,
|
||||||
url = status.actionableStatus.url,
|
url = status.actionableStatus.url,
|
||||||
timelineUserId = timelineUserId,
|
timelineUserId = timelineUserId,
|
||||||
|
@ -110,7 +110,7 @@ data class TimelineStatusEntity(
|
||||||
content = status.actionableStatus.content,
|
content = status.actionableStatus.content,
|
||||||
createdAt = status.actionableStatus.createdAt.time,
|
createdAt = status.actionableStatus.createdAt.time,
|
||||||
editedAt = status.actionableStatus.editedAt?.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,
|
reblogsCount = status.actionableStatus.reblogsCount,
|
||||||
favouritesCount = status.actionableStatus.favouritesCount,
|
favouritesCount = status.actionableStatus.favouritesCount,
|
||||||
reblogged = status.actionableStatus.reblogged,
|
reblogged = status.actionableStatus.reblogged,
|
||||||
|
@ -119,16 +119,16 @@ data class TimelineStatusEntity(
|
||||||
sensitive = status.actionableStatus.sensitive,
|
sensitive = status.actionableStatus.sensitive,
|
||||||
spoilerText = status.actionableStatus.spoilerText,
|
spoilerText = status.actionableStatus.spoilerText,
|
||||||
visibility = status.actionableStatus.visibility,
|
visibility = status.actionableStatus.visibility,
|
||||||
attachments = status.actionableStatus.attachments.let(gson::toJson),
|
attachments = moshi.adapter<List<Attachment>>().toJson(status.actionableStatus.attachments),
|
||||||
mentions = status.actionableStatus.mentions.let(gson::toJson),
|
mentions = moshi.adapter<List<Status.Mention>>().toJson(status.actionableStatus.mentions),
|
||||||
tags = status.actionableStatus.tags.let(gson::toJson),
|
tags = moshi.adapter<List<HashTag>>().toJson(status.actionableStatus.tags),
|
||||||
application = status.actionableStatus.application.let(gson::toJson),
|
application = moshi.adapter<Status.Application?>().toJson(status.actionableStatus.application),
|
||||||
reblogServerId = status.reblog?.id,
|
reblogServerId = status.reblog?.id,
|
||||||
reblogAccountId = status.reblog?.let { status.account.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,
|
muted = status.actionableStatus.muted,
|
||||||
pinned = status.actionableStatus.pinned == true,
|
pinned = status.actionableStatus.pinned == true,
|
||||||
card = status.actionableStatus.card?.let(gson::toJson),
|
card = moshi.adapter<Card?>().toJson(status.actionableStatus.card),
|
||||||
repliesCount = status.actionableStatus.repliesCount,
|
repliesCount = status.actionableStatus.repliesCount,
|
||||||
language = status.actionableStatus.language,
|
language = status.actionableStatus.language,
|
||||||
filtered = status.actionableStatus.filtered,
|
filtered = status.actionableStatus.filtered,
|
||||||
|
@ -150,7 +150,8 @@ data class TimelineAccountEntity(
|
||||||
val emojis: String,
|
val emojis: String,
|
||||||
val bot: Boolean,
|
val bot: Boolean,
|
||||||
) {
|
) {
|
||||||
fun toTimelineAccount(gson: Gson): TimelineAccount {
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
fun toTimelineAccount(moshi: Moshi): TimelineAccount {
|
||||||
return TimelineAccount(
|
return TimelineAccount(
|
||||||
id = serverId,
|
id = serverId,
|
||||||
localUsername = localUsername,
|
localUsername = localUsername,
|
||||||
|
@ -160,12 +161,13 @@ data class TimelineAccountEntity(
|
||||||
url = url,
|
url = url,
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
bot = bot,
|
bot = bot,
|
||||||
emojis = gson.fromJson(emojis, emojisListType),
|
emojis = moshi.adapter<List<Emoji>>().fromJson(emojis),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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,
|
serverId = timelineAccount.id,
|
||||||
timelineUserId = accountId,
|
timelineUserId = accountId,
|
||||||
localUsername = timelineAccount.localUsername,
|
localUsername = timelineAccount.localUsername,
|
||||||
|
@ -173,7 +175,7 @@ data class TimelineAccountEntity(
|
||||||
displayName = timelineAccount.name,
|
displayName = timelineAccount.name,
|
||||||
url = timelineAccount.url,
|
url = timelineAccount.url,
|
||||||
avatar = timelineAccount.avatar,
|
avatar = timelineAccount.avatar,
|
||||||
emojis = gson.toJson(timelineAccount.emojis),
|
emojis = moshi.adapter<List<Emoji>>().toJson(timelineAccount.emojis),
|
||||||
bot = timelineAccount.bot,
|
bot = timelineAccount.bot,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -214,11 +216,6 @@ data class StatusViewDataEntity(
|
||||||
val translationState: TranslationState,
|
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(
|
data class TimelineStatusWithAccount(
|
||||||
@Embedded
|
@Embedded
|
||||||
val status: TimelineStatusEntity,
|
val status: TimelineStatusEntity,
|
||||||
|
@ -232,32 +229,28 @@ data class TimelineStatusWithAccount(
|
||||||
@Embedded(prefix = "t_")
|
@Embedded(prefix = "t_")
|
||||||
val translatedStatus: TranslatedStatusEntity? = null,
|
val translatedStatus: TranslatedStatusEntity? = null,
|
||||||
) {
|
) {
|
||||||
fun toStatus(gson: Gson): Status {
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
val attachments: ArrayList<Attachment> = gson.fromJson(
|
fun toStatus(moshi: Moshi): Status {
|
||||||
status.attachments,
|
val attachments: List<Attachment> = status.attachments?.let {
|
||||||
attachmentArrayListType,
|
moshi.adapter<List<Attachment>?>().fromJson(it)
|
||||||
) ?: arrayListOf()
|
} ?: emptyList()
|
||||||
val mentions: List<Status.Mention> = gson.fromJson(
|
val mentions: List<Status.Mention> = status.mentions?.let {
|
||||||
status.mentions,
|
moshi.adapter<List<Status.Mention>?>().fromJson(it)
|
||||||
mentionListType,
|
} ?: emptyList()
|
||||||
) ?: emptyList()
|
val tags: List<HashTag>? = status.tags?.let {
|
||||||
val tags: List<HashTag>? = gson.fromJson(
|
moshi.adapter<List<HashTag>?>().fromJson(it)
|
||||||
status.tags,
|
}
|
||||||
tagListType,
|
val application = status.application?.let { moshi.adapter<Status.Application>().fromJson(it) }
|
||||||
)
|
val emojis: List<Emoji> = status.emojis?.let { moshi.adapter<List<Emoji>?>().fromJson(it) }
|
||||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
?: emptyList()
|
||||||
val emojis: List<Emoji> = gson.fromJson(
|
val poll: Poll? = status.poll?.let { moshi.adapter<Poll?>().fromJson(it) }
|
||||||
status.emojis,
|
val card: Card? = status.card?.let { moshi.adapter<Card?>().fromJson(it) }
|
||||||
emojisListType,
|
|
||||||
) ?: emptyList()
|
|
||||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
|
||||||
val card: Card? = gson.fromJson(status.card, Card::class.java)
|
|
||||||
|
|
||||||
val reblog = status.reblogServerId?.let { id ->
|
val reblog = status.reblogServerId?.let { id ->
|
||||||
Status(
|
Status(
|
||||||
id = id,
|
id = id,
|
||||||
url = status.url,
|
url = status.url,
|
||||||
account = account.toTimelineAccount(gson),
|
account = account.toTimelineAccount(moshi),
|
||||||
inReplyToId = status.inReplyToId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
|
@ -291,7 +284,7 @@ data class TimelineStatusWithAccount(
|
||||||
id = status.serverId,
|
id = status.serverId,
|
||||||
// no url for reblogs
|
// no url for reblogs
|
||||||
url = null,
|
url = null,
|
||||||
account = reblogAccount!!.toTimelineAccount(gson),
|
account = reblogAccount!!.toTimelineAccount(moshi),
|
||||||
inReplyToId = null,
|
inReplyToId = null,
|
||||||
inReplyToAccountId = null,
|
inReplyToAccountId = null,
|
||||||
reblog = reblog,
|
reblog = reblog,
|
||||||
|
@ -308,7 +301,7 @@ data class TimelineStatusWithAccount(
|
||||||
sensitive = false,
|
sensitive = false,
|
||||||
spoilerText = "",
|
spoilerText = "",
|
||||||
visibility = status.visibility,
|
visibility = status.visibility,
|
||||||
attachments = ArrayList(),
|
attachments = listOf(),
|
||||||
mentions = listOf(),
|
mentions = listOf(),
|
||||||
tags = listOf(),
|
tags = listOf(),
|
||||||
application = null,
|
application = null,
|
||||||
|
@ -324,7 +317,7 @@ data class TimelineStatusWithAccount(
|
||||||
Status(
|
Status(
|
||||||
id = status.serverId,
|
id = status.serverId,
|
||||||
url = status.url,
|
url = status.url,
|
||||||
account = account.toTimelineAccount(gson),
|
account = account.toTimelineAccount(moshi),
|
||||||
inReplyToId = status.inReplyToId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
|
|
|
@ -21,7 +21,7 @@ import androidx.room.Room
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import app.pachli.core.database.AppDatabase
|
import app.pachli.core.database.AppDatabase
|
||||||
import app.pachli.core.database.Converters
|
import app.pachli.core.database.Converters
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
@ -36,10 +36,10 @@ import javax.inject.Singleton
|
||||||
object FakeDatabaseModule {
|
object FakeDatabaseModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesDatabase(gson: Gson): AppDatabase {
|
fun providesDatabase(moshi: Moshi): AppDatabase {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
return Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
return Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||||
.addTypeConverter(Converters(gson))
|
.addTypeConverter(Converters(moshi))
|
||||||
.allowMainThreadQueries()
|
.allowMainThreadQueries()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@ class ComposeActivityIntent(context: Context) : Intent() {
|
||||||
/** Editing a status started as an existing draft */
|
/** Editing a status started as an existing draft */
|
||||||
EDIT_DRAFT,
|
EDIT_DRAFT,
|
||||||
|
|
||||||
/** Editing an an existing scheduled status */
|
/** Editing an existing scheduled status */
|
||||||
EDIT_SCHEDULED,
|
EDIT_SCHEDULED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ class EditFilterActivityIntent(context: Context, filter: Filter? = null) : Inten
|
||||||
/**
|
/**
|
||||||
* @param context
|
* @param context
|
||||||
* @param loginMode See [LoginMode]
|
* @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() {
|
class LoginActivityIntent(context: Context, loginMode: LoginMode = LoginMode.DEFAULT) : Intent() {
|
||||||
/** How to log in */
|
/** How to log in */
|
||||||
|
@ -472,7 +472,7 @@ class ViewMediaActivityIntent private constructor(context: Context) : Intent() {
|
||||||
private const val EXTRA_SINGLE_IMAGE_URL = "singleImage"
|
private const val EXTRA_SINGLE_IMAGE_URL = "singleImage"
|
||||||
|
|
||||||
/** @return the list of [AttachmentViewData] passed in this intent, or null */
|
/** @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 */
|
/** @return the index of the attachment to show, or 0 */
|
||||||
fun getAttachmentIndex(intent: Intent) = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
|
fun getAttachmentIndex(intent: Intent) = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
|
||||||
|
|
|
@ -33,7 +33,10 @@ dependencies {
|
||||||
implementation(projects.core.common)
|
implementation(projects.core.common)
|
||||||
implementation(projects.core.preferences)
|
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.retrofit)
|
||||||
implementation(libs.bundles.okhttp)
|
implementation(libs.bundles.okhttp)
|
||||||
api(libs.networkresult.calladapter)
|
api(libs.networkresult.calladapter)
|
||||||
|
|
|
@ -22,7 +22,7 @@ import android.os.Build
|
||||||
import app.pachli.core.common.util.versionName
|
import app.pachli.core.common.util.versionName
|
||||||
import app.pachli.core.mastodon.model.MediaUploadApi
|
import app.pachli.core.mastodon.model.MediaUploadApi
|
||||||
import app.pachli.core.network.BuildConfig
|
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.InstanceSwitchAuthInterceptor
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import app.pachli.core.preferences.PrefKeys.HTTP_PROXY_ENABLED
|
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.SharedPreferencesRepository
|
||||||
import app.pachli.core.preferences.getNonNullString
|
import app.pachli.core.preferences.getNonNullString
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import com.google.gson.GsonBuilder
|
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -50,7 +50,7 @@ import okhttp3.OkHttp
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import retrofit2.create
|
import retrofit2.create
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -60,9 +60,10 @@ object NetworkModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGson(): Gson = GsonBuilder()
|
fun providesMoshi(): Moshi = Moshi.Builder()
|
||||||
.registerTypeAdapter(Date::class.java, Rfc3339DateJsonAdapter())
|
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||||
.create()
|
.add(GuardedAdapterFactory())
|
||||||
|
.build()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@ -116,11 +117,11 @@ object NetworkModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesRetrofit(
|
fun providesRetrofit(
|
||||||
httpClient: OkHttpClient,
|
httpClient: OkHttpClient,
|
||||||
gson: Gson,
|
moshi: Moshi,
|
||||||
): Retrofit {
|
): Retrofit {
|
||||||
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
|
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
|
||||||
.client(httpClient)
|
.client(httpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||||
.build()
|
.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
|
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(
|
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
|
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
|
import java.util.Date
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Account(
|
data class Account(
|
||||||
val id: String,
|
val id: String,
|
||||||
@SerializedName("username") val localUsername: String,
|
@Json(name = "username") val localUsername: String,
|
||||||
@SerializedName("acct") val username: String,
|
@Json(name = "acct") val username: String,
|
||||||
// should never be null per API definition, but some servers break the contract
|
// 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
|
// 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 note: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val header: String,
|
val header: String,
|
||||||
val locked: Boolean = false,
|
val locked: Boolean = false,
|
||||||
@SerializedName("followers_count") val followersCount: Int = 0,
|
@Json(name = "followers_count") val followersCount: Int = 0,
|
||||||
@SerializedName("following_count") val followingCount: Int = 0,
|
@Json(name = "following_count") val followingCount: Int = 0,
|
||||||
@SerializedName("statuses_count") val statusesCount: Int = 0,
|
@Json(name = "statuses_count") val statusesCount: Int = 0,
|
||||||
val source: AccountSource? = null,
|
val source: AccountSource? = null,
|
||||||
val bot: Boolean = false,
|
val bot: Boolean = false,
|
||||||
// nullable for backward compatibility
|
// nullable for backward compatibility
|
||||||
|
@ -55,6 +57,7 @@ data class Account(
|
||||||
fun isRemote(): Boolean = this.username != this.localUsername
|
fun isRemote(): Boolean = this.username != this.localUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class AccountSource(
|
data class AccountSource(
|
||||||
val privacy: Status.Visibility?,
|
val privacy: Status.Visibility?,
|
||||||
val sensitive: Boolean?,
|
val sensitive: Boolean?,
|
||||||
|
@ -63,18 +66,21 @@ data class AccountSource(
|
||||||
val language: String?,
|
val language: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Field(
|
data class Field(
|
||||||
val name: String,
|
val name: String,
|
||||||
val value: String,
|
val value: String,
|
||||||
@SerializedName("verified_at") val verifiedAt: Date?,
|
@Json(name = "verified_at") val verifiedAt: Date?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class StringField(
|
data class StringField(
|
||||||
val name: String,
|
val name: String,
|
||||||
val value: String,
|
val value: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** [Mastodon Entities: Role](https://docs.joinmastodon.org/entities/Role) */
|
/** [Mastodon Entities: Role](https://docs.joinmastodon.org/entities/Role) */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Role(
|
data class Role(
|
||||||
/** Displayable name of the role */
|
/** Displayable name of the role */
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
|
@ -16,17 +16,19 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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
|
import java.util.Date
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Announcement(
|
data class Announcement(
|
||||||
val id: String,
|
val id: String,
|
||||||
val content: String,
|
val content: String,
|
||||||
@SerializedName("starts_at") val startsAt: Date?,
|
@Json(name = "starts_at") val startsAt: Date?,
|
||||||
@SerializedName("ends_at") val endsAt: Date?,
|
@Json(name = "ends_at") val endsAt: Date?,
|
||||||
@SerializedName("all_day") val allDay: Boolean,
|
@Json(name = "all_day") val allDay: Boolean,
|
||||||
@SerializedName("published_at") val publishedAt: Date,
|
@Json(name = "published_at") val publishedAt: Date,
|
||||||
@SerializedName("updated_at") val updatedAt: Date,
|
@Json(name = "updated_at") val updatedAt: Date,
|
||||||
val read: Boolean,
|
val read: Boolean,
|
||||||
val mentions: List<Status.Mention>,
|
val mentions: List<Status.Mention>,
|
||||||
val statuses: List<Status>,
|
val statuses: List<Status>,
|
||||||
|
@ -47,11 +49,12 @@ data class Announcement(
|
||||||
return id.hashCode()
|
return id.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Reaction(
|
data class Reaction(
|
||||||
val name: String,
|
val name: String,
|
||||||
val count: Int,
|
val count: Int,
|
||||||
val me: Boolean,
|
val me: Boolean,
|
||||||
val url: String?,
|
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
|
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(
|
data class AppCredentials(
|
||||||
@SerializedName("client_id") val clientId: String,
|
@Json(name = "client_id") val clientId: String,
|
||||||
@SerializedName("client_secret") val clientSecret: String,
|
@Json(name = "client_secret") val clientSecret: String,
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,61 +18,45 @@
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.JsonDeserializationContext
|
import com.squareup.moshi.Json
|
||||||
import com.google.gson.JsonDeserializer
|
import com.squareup.moshi.JsonClass
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import com.google.gson.annotations.JsonAdapter
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Attachment(
|
data class Attachment(
|
||||||
val id: String,
|
val id: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
// can be null for e.g. audio attachments
|
// 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 meta: MetaData?,
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val blurhash: String?,
|
val blurhash: String?,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@JsonAdapter(MediaTypeDeserializer::class)
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
@SerializedName("image")
|
@Json(name = "image")
|
||||||
IMAGE,
|
IMAGE,
|
||||||
|
|
||||||
@SerializedName("gifv")
|
@Json(name = "gifv")
|
||||||
GIFV,
|
GIFV,
|
||||||
|
|
||||||
@SerializedName("video")
|
@Json(name = "video")
|
||||||
VIDEO,
|
VIDEO,
|
||||||
|
|
||||||
@SerializedName("audio")
|
@Json(name = "audio")
|
||||||
AUDIO,
|
AUDIO,
|
||||||
|
|
||||||
@SerializedName("unknown")
|
@Json(name = "unknown")
|
||||||
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].
|
* The meta data of an [Attachment].
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class MetaData(
|
data class MetaData(
|
||||||
val focus: Focus?,
|
val focus: Focus?,
|
||||||
val duration: Float?,
|
val duration: Float?,
|
||||||
|
@ -87,6 +71,7 @@ data class Attachment(
|
||||||
* https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
* https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Focus(
|
data class Focus(
|
||||||
val x: Float,
|
val x: Float,
|
||||||
val y: Float,
|
val y: Float,
|
||||||
|
@ -98,9 +83,21 @@ data class Attachment(
|
||||||
* The size of an image, used to specify the width/height.
|
* The size of an image, used to specify the width/height.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Size(
|
data class Size(
|
||||||
val width: Int,
|
val width: Int?,
|
||||||
val height: Int,
|
val height: Int?,
|
||||||
val aspect: Double,
|
// Not always present, see https://github.com/mastodon/mastodon/issues/29125
|
||||||
) : Parcelable
|
@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
|
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(
|
data class Card(
|
||||||
override val url: String,
|
override val url: String,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val description: String,
|
override val description: String,
|
||||||
@SerializedName("type") override val kind: PreviewCardKind,
|
@Json(name = "type") override val kind: PreviewCardKind,
|
||||||
@SerializedName("author_name") override val authorName: String,
|
@Json(name = "author_name") override val authorName: String,
|
||||||
@SerializedName("author_url") override val authorUrl: String,
|
@Json(name = "author_url") override val authorUrl: String,
|
||||||
@SerializedName("provider_name") override val providerName: String,
|
@Json(name = "provider_name") override val providerName: String,
|
||||||
@SerializedName("provider_url") override val providerUrl: String,
|
@Json(name = "provider_url") override val providerUrl: String,
|
||||||
override val html: String,
|
override val html: String,
|
||||||
override val width: Int,
|
override val width: Int,
|
||||||
override val height: Int,
|
override val height: Int,
|
||||||
override val image: String? = null,
|
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,
|
override val blurhash: String? = null,
|
||||||
) : PreviewCard {
|
) : PreviewCard {
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,14 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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(
|
data class Conversation(
|
||||||
val id: String,
|
val id: String,
|
||||||
val accounts: List<TimelineAccount>,
|
val accounts: List<TimelineAccount>,
|
||||||
// should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
|
// 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,
|
val unread: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,18 +16,20 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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
|
import java.util.Date
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class DeletedStatus(
|
data class DeletedStatus(
|
||||||
val text: String?,
|
val text: String?,
|
||||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
@Json(name = "in_reply_to_id") val inReplyToId: String?,
|
||||||
@SerializedName("spoiler_text") val spoilerText: String,
|
@Json(name = "spoiler_text") val spoilerText: String,
|
||||||
val visibility: Status.Visibility,
|
val visibility: Status.Visibility,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
@SerializedName("media_attachments") val attachments: List<Attachment>?,
|
@Json(name = "media_attachments") val attachments: List<Attachment>?,
|
||||||
val poll: Poll?,
|
val poll: Poll?,
|
||||||
@SerializedName("created_at") val createdAt: Date,
|
@Json(name = "created_at") val createdAt: Date,
|
||||||
val language: String?,
|
val language: String?,
|
||||||
) {
|
) {
|
||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
|
|
|
@ -17,13 +17,15 @@
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Emoji(
|
data class Emoji(
|
||||||
val shortcode: String,
|
val shortcode: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
@SerializedName("static_url") val staticUrl: String,
|
@Json(name = "static_url") val staticUrl: String,
|
||||||
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?,
|
@Json(name = "visible_in_picker") val visibleInPicker: Boolean?,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
@ -17,8 +17,13 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
/** @see [Error](https://docs.joinmastodon.org/entities/Error/) */
|
/** @see [Error](https://docs.joinmastodon.org/entities/Error/) */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Error(
|
data class Error(
|
||||||
val error: String,
|
val error: String,
|
||||||
val error_description: String?,
|
@Json(name = "error_description")
|
||||||
|
val errorDescription: String?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
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 java.util.Date
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Filter(
|
data class Filter(
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val context: List<String>,
|
val context: List<String>,
|
||||||
@SerializedName("expires_at") val expiresAt: Date?,
|
@Json(name = "expires_at") val expiresAt: Date?,
|
||||||
@SerializedName("filter_action") private val filterAction: String,
|
@Json(name = "filter_action") val filterAction: String,
|
||||||
val keywords: List<FilterKeyword>,
|
// 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>,
|
// val statuses: List<FilterStatus>,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
enum class Action(val action: String) {
|
enum class Action(val action: String) {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class FilterKeyword(
|
data class FilterKeyword(
|
||||||
val id: String,
|
val id: String,
|
||||||
val keyword: String,
|
val keyword: String,
|
||||||
@SerializedName("whole_word") val wholeWord: Boolean,
|
@Json(name = "whole_word") val wholeWord: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package app.pachli.core.network.model
|
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(
|
data class FilterResult(
|
||||||
val filter: Filter,
|
val filter: Filter,
|
||||||
@SerializedName("keyword_matches") val keywordMatches: List<String>?,
|
@Json(name = "keyword_matches") val keywordMatches: List<String>?,
|
||||||
@SerializedName("status_matches") val statusMatches: List<String>?,
|
@Json(name = "status_matches") val statusMatches: List<String>?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,16 +16,18 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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
|
import java.util.Date
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class FilterV1(
|
data class FilterV1(
|
||||||
val id: String,
|
val id: String,
|
||||||
val phrase: String,
|
val phrase: String,
|
||||||
val context: List<String>,
|
val context: List<String>,
|
||||||
@SerializedName("expires_at") val expiresAt: Date?,
|
@Json(name = "expires_at") val expiresAt: Date?,
|
||||||
val irreversible: Boolean,
|
val irreversible: Boolean,
|
||||||
@SerializedName("whole_word") val wholeWord: Boolean,
|
@Json(name = "whole_word") val wholeWord: Boolean,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val HOME = "home"
|
const val HOME = "home"
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
package app.pachli.core.network.model
|
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)
|
data class HashTag(val name: String, val url: String, val following: Boolean? = null)
|
||||||
|
|
|
@ -16,9 +16,11 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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/ */
|
/** https://docs.joinmastodon.org/entities/V1_Instance/ */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class InstanceV1(
|
data class InstanceV1(
|
||||||
val uri: String,
|
val uri: String,
|
||||||
// val title: String,
|
// val title: String,
|
||||||
|
@ -29,13 +31,13 @@ data class InstanceV1(
|
||||||
// val stats: Map<String, Int>?,
|
// val stats: Map<String, Int>?,
|
||||||
// val thumbnail: String?,
|
// val thumbnail: String?,
|
||||||
// val languages: List<String>,
|
// val languages: List<String>,
|
||||||
// @SerializedName("contact_account") val contactAccount: Account,
|
// @Json(name = "contact_account") val contactAccount: Account,
|
||||||
@SerializedName("max_toot_chars") val maxTootChars: Int?,
|
@Json(name = "max_toot_chars") val maxTootChars: Int?,
|
||||||
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?,
|
@Json(name = "poll_limits") val pollConfiguration: PollConfiguration?,
|
||||||
val configuration: InstanceConfiguration?,
|
val configuration: InstanceConfiguration?,
|
||||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
@Json(name = "max_media_attachments") val maxMediaAttachments: Int?,
|
||||||
val pleroma: PleromaConfiguration?,
|
val pleroma: PleromaConfiguration?,
|
||||||
@SerializedName("upload_limit") val uploadLimit: Int?,
|
@Json(name = "upload_limit") val uploadLimit: Int?,
|
||||||
val rules: List<InstanceRules>?,
|
val rules: List<InstanceRules>?,
|
||||||
) {
|
) {
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
|
@ -51,49 +53,57 @@ data class InstanceV1(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class PollConfiguration(
|
data class PollConfiguration(
|
||||||
@SerializedName("max_options") val maxOptions: Int?,
|
@Json(name = "max_options") val maxOptions: Int?,
|
||||||
@SerializedName("max_option_chars") val maxOptionChars: Int?,
|
@Json(name = "max_option_chars") val maxOptionChars: Int?,
|
||||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
|
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int?,
|
||||||
@SerializedName("min_expiration") val minExpiration: Int?,
|
@Json(name = "min_expiration") val minExpiration: Int?,
|
||||||
@SerializedName("max_expiration") val maxExpiration: Int?,
|
@Json(name = "max_expiration") val maxExpiration: Int?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class InstanceConfiguration(
|
data class InstanceConfiguration(
|
||||||
val statuses: StatusConfiguration?,
|
val statuses: StatusConfiguration?,
|
||||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
@Json(name = "media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
||||||
val polls: PollConfiguration?,
|
val polls: PollConfiguration?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class StatusConfiguration(
|
data class StatusConfiguration(
|
||||||
@SerializedName("max_characters") val maxCharacters: Int?,
|
@Json(name = "max_characters") val maxCharacters: Int?,
|
||||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
@Json(name = "max_media_attachments") val maxMediaAttachments: Int?,
|
||||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?,
|
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class MediaAttachmentConfiguration(
|
data class MediaAttachmentConfiguration(
|
||||||
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>?,
|
@Json(name = "supported_mime_types") val supportedMimeTypes: List<String>?,
|
||||||
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
|
@Json(name = "image_size_limit") val imageSizeLimit: Int?,
|
||||||
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
|
@Json(name = "image_matrix_limit") val imageMatrixLimit: Int?,
|
||||||
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
|
@Json(name = "video_size_limit") val videoSizeLimit: Int?,
|
||||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
||||||
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?,
|
@Json(name = "video_matrix_limit") val videoMatrixLimit: Int?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class PleromaConfiguration(
|
data class PleromaConfiguration(
|
||||||
val metadata: PleromaMetadata?,
|
val metadata: PleromaMetadata?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class PleromaMetadata(
|
data class PleromaMetadata(
|
||||||
@SerializedName("fields_limits") val fieldLimits: PleromaFieldLimits,
|
@Json(name = "fields_limits") val fieldLimits: PleromaFieldLimits,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class PleromaFieldLimits(
|
data class PleromaFieldLimits(
|
||||||
@SerializedName("max_fields") val maxFields: Int?,
|
@Json(name = "max_fields") val maxFields: Int?,
|
||||||
@SerializedName("name_length") val nameLength: Int?,
|
@Json(name = "name_length") val nameLength: Int?,
|
||||||
@SerializedName("value_length") val valueLength: Int?,
|
@Json(name = "value_length") val valueLength: Int?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class InstanceRules(
|
data class InstanceRules(
|
||||||
val id: String,
|
val id: String,
|
||||||
val text: String,
|
val text: String,
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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/ */
|
/** https://docs.joinmastodon.org/entities/Instance/ */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class InstanceV2(
|
data class InstanceV2(
|
||||||
/** The domain name of the instance */
|
/** The domain name of the instance */
|
||||||
val domain: String,
|
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
|
* The URL for the source code of the software running on this instance, in keeping with AGPL
|
||||||
* license requirements.
|
* license requirements.
|
||||||
*/
|
*/
|
||||||
@SerializedName("source_url") val sourceUrl: String,
|
@Json(name = "source_url") val sourceUrl: String,
|
||||||
|
|
||||||
/** A short, plain-text description defined by the admin. */
|
/** A short, plain-text description defined by the admin. */
|
||||||
val description: String,
|
val description: String,
|
||||||
|
@ -61,16 +63,19 @@ data class InstanceV2(
|
||||||
val rules: List<Rule>,
|
val rules: List<Rule>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Usage(
|
data class Usage(
|
||||||
/** Usage data related to users on this instance. */
|
/** Usage data related to users on this instance. */
|
||||||
val users: Users,
|
val users: Users,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Users(
|
data class Users(
|
||||||
/** The number of active users in the past 4 weeks. */
|
/** The number of active users in the past 4 weeks. */
|
||||||
val activeMonth: Int = 0,
|
val activeMonth: Int = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Thumbnail(
|
data class Thumbnail(
|
||||||
/** The URL for the thumbnail image. */
|
/** The URL for the thumbnail image. */
|
||||||
val url: String,
|
val url: String,
|
||||||
|
@ -85,14 +90,16 @@ data class Thumbnail(
|
||||||
val versions: ThumbnailVersions?,
|
val versions: ThumbnailVersions?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class ThumbnailVersions(
|
data class ThumbnailVersions(
|
||||||
/** The URL for the thumbnail image at 1x resolution. */
|
/** 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. */
|
/** 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(
|
data class Configuration(
|
||||||
/** URLs of interest for clients apps. */
|
/** URLs of interest for clients apps. */
|
||||||
val urls: InstanceV2Urls,
|
val urls: InstanceV2Urls,
|
||||||
|
@ -104,7 +111,7 @@ data class Configuration(
|
||||||
val statuses: InstanceV2Statuses,
|
val statuses: InstanceV2Statuses,
|
||||||
|
|
||||||
/** Hints for which attachments will be accepted. */
|
/** Hints for which attachments will be accepted. */
|
||||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachments,
|
@Json(name = "media_attachments") val mediaAttachments: MediaAttachments,
|
||||||
|
|
||||||
/** Limits related to polls. */
|
/** Limits related to polls. */
|
||||||
val polls: InstanceV2Polls,
|
val polls: InstanceV2Polls,
|
||||||
|
@ -113,77 +120,94 @@ data class Configuration(
|
||||||
val translation: InstanceV2Translation,
|
val translation: InstanceV2Translation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class InstanceV2Urls(
|
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(
|
data class InstanceV2Accounts(
|
||||||
/** The maximum number of featured tags allowed for each account. */
|
/** 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(
|
data class InstanceV2Statuses(
|
||||||
/** The maximum number of allowed characters per status. */
|
/** 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. */
|
/** 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. */
|
/** 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(
|
data class MediaAttachments(
|
||||||
/** Contains MIME types that can be uploaded. */
|
/** 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. */
|
/** 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. */
|
/** 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. */
|
/** 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. */
|
/** 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. */
|
/** 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(
|
data class InstanceV2Polls(
|
||||||
/** Each poll is allowed to have up to this many options. */
|
/** 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. */
|
/** 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. */
|
/** 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. */
|
/** 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(
|
data class InstanceV2Translation(
|
||||||
/** Whether the Translations API is available on this instance. */
|
/** Whether the Translations API is available on this instance. */
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Registrations(
|
data class Registrations(
|
||||||
/** Whether registrations are enabled. */
|
/** Whether registrations are enabled. */
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
|
|
||||||
/** Whether registrations require moderator approval. */
|
/** 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. */
|
/** A custom message to be shown when registrations are closed. */
|
||||||
val message: String?,
|
val message: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Contact(
|
data class Contact(
|
||||||
/** An email address that can be messaged regarding inquiries or issues. */
|
/** An email address that can be messaged regarding inquiries or issues. */
|
||||||
val email: String,
|
val email: String,
|
||||||
|
@ -193,6 +217,7 @@ data class Contact(
|
||||||
)
|
)
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/entities/Rule/ */
|
/** https://docs.joinmastodon.org/entities/Rule/ */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Rule(
|
data class Rule(
|
||||||
/** An identifier for the rule. */
|
/** An identifier for the rule. */
|
||||||
val id: String,
|
val id: String,
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
package app.pachli.core.network.model
|
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
|
import java.util.Date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API type for saving the scroll position of a timeline.
|
* API type for saving the scroll position of a timeline.
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Marker(
|
data class Marker(
|
||||||
@SerializedName("last_read_id")
|
@Json(name = "last_read_id")
|
||||||
val lastReadId: String,
|
val lastReadId: String,
|
||||||
val version: Int,
|
val version: Int,
|
||||||
@SerializedName("updated_at")
|
@Json(name = "updated_at")
|
||||||
val updatedAt: Date,
|
val updatedAt: Date,
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class MastoList(
|
data class MastoList(
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package app.pachli.core.network.model
|
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/
|
* 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
|
* We are only interested in the id, so other attributes are omitted
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class MediaUploadResult(
|
data class MediaUploadResult(
|
||||||
val id: String,
|
val id: String,
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,32 +17,36 @@
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class NewStatus(
|
data class NewStatus(
|
||||||
val status: String,
|
val status: String,
|
||||||
@SerializedName("spoiler_text") val warningText: String,
|
@Json(name = "spoiler_text") val warningText: String,
|
||||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
@Json(name = "in_reply_to_id") val inReplyToId: String?,
|
||||||
val visibility: String,
|
val visibility: String,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
@SerializedName("media_ids") val mediaIds: List<String>?,
|
@Json(name = "media_ids") val mediaIds: List<String>?,
|
||||||
@SerializedName("media_attributes") val mediaAttributes: List<MediaAttribute>?,
|
@Json(name = "media_attributes") val mediaAttributes: List<MediaAttribute>?,
|
||||||
@SerializedName("scheduled_at") val scheduledAt: String?,
|
@Json(name = "scheduled_at") val scheduledAt: String?,
|
||||||
val poll: NewPoll?,
|
val poll: NewPoll?,
|
||||||
val language: String?,
|
val language: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class NewPoll(
|
data class NewPoll(
|
||||||
val options: List<String>,
|
val options: List<String>,
|
||||||
@SerializedName("expires_in") val expiresIn: Int,
|
@Json(name = "expires_in") val expiresIn: Int,
|
||||||
val multiple: Boolean,
|
val multiple: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
// It would be nice if we could reuse MediaToSend,
|
// It would be nice if we could reuse MediaToSend,
|
||||||
// but the server requires a different format for focus
|
// but the server requires a different format for focus
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class MediaAttribute(
|
data class MediaAttribute(
|
||||||
val id: String,
|
val id: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
|
|
|
@ -17,14 +17,12 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
import com.squareup.moshi.Json
|
||||||
import com.google.gson.JsonDeserializer
|
import com.squareup.moshi.JsonClass
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import com.google.gson.annotations.JsonAdapter
|
|
||||||
|
|
||||||
// TODO: These should be different subclasses per type, so that each subclass can
|
// TODO: These should be different subclasses per type, so that each subclass can
|
||||||
// carry the non-null data that it needs.
|
// carry the non-null data that it needs.
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Notification(
|
data class Notification(
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val id: String,
|
val id: String,
|
||||||
|
@ -34,38 +32,48 @@ data class Notification(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** From https://docs.joinmastodon.org/entities/Notification/#type */
|
/** From https://docs.joinmastodon.org/entities/Notification/#type */
|
||||||
@JsonAdapter(NotificationTypeAdapter::class)
|
|
||||||
enum class Type(val presentation: String) {
|
enum class Type(val presentation: String) {
|
||||||
|
@Json(name = "unknown")
|
||||||
UNKNOWN("unknown"),
|
UNKNOWN("unknown"),
|
||||||
|
|
||||||
/** Someone mentioned you */
|
/** Someone mentioned you */
|
||||||
|
@Json(name = "mention")
|
||||||
MENTION("mention"),
|
MENTION("mention"),
|
||||||
|
|
||||||
/** Someone boosted one of your statuses */
|
/** Someone boosted one of your statuses */
|
||||||
|
@Json(name = "reblog")
|
||||||
REBLOG("reblog"),
|
REBLOG("reblog"),
|
||||||
|
|
||||||
/** Someone favourited one of your statuses */
|
/** Someone favourited one of your statuses */
|
||||||
|
@Json(name = "favourite")
|
||||||
FAVOURITE("favourite"),
|
FAVOURITE("favourite"),
|
||||||
|
|
||||||
/** Someone followed you */
|
/** Someone followed you */
|
||||||
|
@Json(name = "follow")
|
||||||
FOLLOW("follow"),
|
FOLLOW("follow"),
|
||||||
|
|
||||||
/** Someone requested to follow you */
|
/** Someone requested to follow you */
|
||||||
|
@Json(name = "follow_request")
|
||||||
FOLLOW_REQUEST("follow_request"),
|
FOLLOW_REQUEST("follow_request"),
|
||||||
|
|
||||||
/** A poll you have voted in or created has ended */
|
/** A poll you have voted in or created has ended */
|
||||||
|
@Json(name = "poll")
|
||||||
POLL("poll"),
|
POLL("poll"),
|
||||||
|
|
||||||
/** Someone you enabled notifications for has posted a status */
|
/** Someone you enabled notifications for has posted a status */
|
||||||
|
@Json(name = "status")
|
||||||
STATUS("status"),
|
STATUS("status"),
|
||||||
|
|
||||||
/** Someone signed up (optionally sent to admins) */
|
/** Someone signed up (optionally sent to admins) */
|
||||||
|
@Json(name = "admin.sign_up")
|
||||||
SIGN_UP("admin.sign_up"),
|
SIGN_UP("admin.sign_up"),
|
||||||
|
|
||||||
/** A status you interacted with has been updated */
|
/** A status you interacted with has been updated */
|
||||||
|
@Json(name = "update")
|
||||||
UPDATE("update"),
|
UPDATE("update"),
|
||||||
|
|
||||||
/** A new report has been filed */
|
/** A new report has been filed */
|
||||||
|
@Json(name = "admin.report")
|
||||||
REPORT("admin.report"),
|
REPORT("admin.report"),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -101,18 +109,6 @@ data class Notification(
|
||||||
return notification?.id == this.id
|
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
|
// for Pleroma compatibility that uses Mention type
|
||||||
fun rewriteToStatusTypeIfNeeded(accountId: String): Notification {
|
fun rewriteToStatusTypeIfNeeded(accountId: String): Notification {
|
||||||
if (type == Type.MENTION && status != null) {
|
if (type == Type.MENTION && status != null) {
|
||||||
|
|
|
@ -16,10 +16,12 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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(
|
data class NotificationSubscribeResult(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val endpoint: String,
|
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
|
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
|
import java.util.Date
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Poll(
|
data class Poll(
|
||||||
val id: String,
|
val id: String,
|
||||||
@SerializedName("expires_at") val expiresAt: Date?,
|
@Json(name = "expires_at") val expiresAt: Date?,
|
||||||
val expired: Boolean,
|
val expired: Boolean,
|
||||||
val multiple: Boolean,
|
val multiple: Boolean,
|
||||||
@SerializedName("votes_count") val votesCount: Int,
|
@Json(name = "votes_count") val votesCount: Int,
|
||||||
// nullable for compatibility with Pleroma
|
// nullable for compatibility with Pleroma
|
||||||
@SerializedName("voters_count") val votersCount: Int?,
|
@Json(name = "voters_count") val votersCount: Int?,
|
||||||
val options: List<PollOption>,
|
val options: List<PollOption>,
|
||||||
val voted: Boolean,
|
val voted: Boolean,
|
||||||
@SerializedName("own_votes") val ownVotes: List<Int>?,
|
@Json(name = "own_votes") val ownVotes: List<Int>?,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun votedCopy(choices: List<Int>): Poll {
|
fun votedCopy(choices: List<Int>): Poll {
|
||||||
|
@ -42,7 +44,8 @@ data class Poll(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class PollOption(
|
data class PollOption(
|
||||||
val title: String,
|
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
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
import app.pachli.core.network.json.GuardedBooleanAdapter
|
import app.pachli.core.network.json.Guarded
|
||||||
import com.google.gson.annotations.JsonAdapter
|
import com.squareup.moshi.Json
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Relationship(
|
data class Relationship(
|
||||||
val id: String,
|
val id: String,
|
||||||
val following: Boolean,
|
val following: Boolean,
|
||||||
@SerializedName("followed_by") val followedBy: Boolean,
|
@Json(name = "followed_by") val followedBy: Boolean,
|
||||||
val blocking: Boolean,
|
val blocking: Boolean,
|
||||||
val muting: Boolean,
|
val muting: Boolean,
|
||||||
@SerializedName("muting_notifications") val mutingNotifications: Boolean,
|
@Json(name = "muting_notifications") val mutingNotifications: Boolean,
|
||||||
val requested: Boolean,
|
val requested: Boolean,
|
||||||
@SerializedName("showing_reblogs") val showingReblogs: 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,
|
* Pleroma extension, same as 'notifying' on Mastodon.
|
||||||
* so we use the custom GuardedBooleanAdapter to ignore the field if it is not a boolean.
|
*
|
||||||
|
* 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,
|
@Guarded
|
||||||
@SerializedName("domain_blocking") val blockingDomain: Boolean,
|
val subscribing: Boolean? = null,
|
||||||
|
@Json(name = "domain_blocking") val blockingDomain: Boolean,
|
||||||
// nullable for backward compatibility / feature detection
|
// nullable for backward compatibility / feature detection
|
||||||
val note: String?,
|
val note: String?,
|
||||||
// since 3.3.0rc
|
// since 3.3.0rc
|
||||||
|
|
|
@ -17,13 +17,15 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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
|
import java.util.Date
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Report(
|
data class Report(
|
||||||
val id: String,
|
val id: String,
|
||||||
val category: String,
|
val category: String,
|
||||||
val status_ids: List<String>?,
|
@Json(name = "status_ids") val statusIds: List<String>?,
|
||||||
@SerializedName("created_at") val createdAt: Date,
|
@Json(name = "created_at") val createdAt: Date,
|
||||||
@SerializedName("target_account") val targetAccount: TimelineAccount,
|
@Json(name = "target_account") val targetAccount: TimelineAccount,
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,11 +16,13 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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(
|
data class ScheduledStatus(
|
||||||
val id: String,
|
val id: String,
|
||||||
@SerializedName("scheduled_at") val scheduledAt: String,
|
@Json(name = "scheduled_at") val scheduledAt: String,
|
||||||
val params: StatusParams,
|
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
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
val accounts: List<TimelineAccount>,
|
val accounts: List<TimelineAccount>,
|
||||||
val statuses: List<Status>,
|
val statuses: List<Status>,
|
||||||
|
|
|
@ -20,31 +20,33 @@ package app.pachli.core.network.model
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import app.pachli.core.network.parseAsMastodonHtml
|
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
|
import java.util.Date
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Status(
|
data class Status(
|
||||||
val id: String,
|
val id: String,
|
||||||
// not present if it's reblog
|
// not present if it's reblog
|
||||||
val url: String?,
|
val url: String?,
|
||||||
val account: TimelineAccount,
|
val account: TimelineAccount,
|
||||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
@Json(name = "in_reply_to_id") val inReplyToId: String?,
|
||||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
@Json(name = "in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||||
val reblog: Status?,
|
val reblog: Status?,
|
||||||
val content: String,
|
val content: String,
|
||||||
@SerializedName("created_at") val createdAt: Date,
|
@Json(name = "created_at") val createdAt: Date,
|
||||||
@SerializedName("edited_at") val editedAt: Date?,
|
@Json(name = "edited_at") val editedAt: Date?,
|
||||||
val emojis: List<Emoji>,
|
val emojis: List<Emoji>,
|
||||||
@SerializedName("reblogs_count") val reblogsCount: Int,
|
@Json(name = "reblogs_count") val reblogsCount: Int,
|
||||||
@SerializedName("favourites_count") val favouritesCount: Int,
|
@Json(name = "favourites_count") val favouritesCount: Int,
|
||||||
@SerializedName("replies_count") val repliesCount: Int,
|
@Json(name = "replies_count") val repliesCount: Int,
|
||||||
val reblogged: Boolean,
|
val reblogged: Boolean,
|
||||||
val favourited: Boolean,
|
val favourited: Boolean,
|
||||||
val bookmarked: Boolean,
|
val bookmarked: Boolean,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
@SerializedName("spoiler_text") val spoilerText: String,
|
@Json(name = "spoiler_text") val spoilerText: String,
|
||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
@SerializedName("media_attachments") val attachments: List<Attachment>,
|
@Json(name = "media_attachments") val attachments: List<Attachment>,
|
||||||
val mentions: List<Mention>,
|
val mentions: List<Mention>,
|
||||||
val tags: List<HashTag>?,
|
val tags: List<HashTag>?,
|
||||||
val application: Application?,
|
val application: Application?,
|
||||||
|
@ -65,16 +67,16 @@ data class Status(
|
||||||
enum class Visibility(val num: Int) {
|
enum class Visibility(val num: Int) {
|
||||||
UNKNOWN(0),
|
UNKNOWN(0),
|
||||||
|
|
||||||
@SerializedName("public")
|
@Json(name = "public")
|
||||||
PUBLIC(1),
|
PUBLIC(1),
|
||||||
|
|
||||||
@SerializedName("unlisted")
|
@Json(name = "unlisted")
|
||||||
UNLISTED(2),
|
UNLISTED(2),
|
||||||
|
|
||||||
@SerializedName("private")
|
@Json(name = "private")
|
||||||
PRIVATE(3),
|
PRIVATE(3),
|
||||||
|
|
||||||
@SerializedName("direct")
|
@Json(name = "direct")
|
||||||
DIRECT(4),
|
DIRECT(4),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -156,13 +158,15 @@ data class Status(
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Mention(
|
data class Mention(
|
||||||
val id: String,
|
val id: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
@SerializedName("acct") val username: String,
|
@Json(name = "acct") val username: String,
|
||||||
@SerializedName("username") val localUsername: String,
|
@Json(name = "username") val localUsername: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Application(
|
data class Application(
|
||||||
val name: String,
|
val name: String,
|
||||||
val website: String?,
|
val website: String?,
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class StatusContext(
|
data class StatusContext(
|
||||||
val ancestors: List<Status>,
|
val ancestors: List<Status>,
|
||||||
val descendants: List<Status>,
|
val descendants: List<Status>,
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
package app.pachli.core.network.model
|
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
|
import java.util.Date
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class StatusEdit(
|
data class StatusEdit(
|
||||||
val content: String,
|
val content: String,
|
||||||
@SerializedName("spoiler_text") val spoilerText: String,
|
@Json(name = "spoiler_text") val spoilerText: String,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
@SerializedName("created_at") val createdAt: Date,
|
@Json(name = "created_at") val createdAt: Date,
|
||||||
val account: TimelineAccount,
|
val account: TimelineAccount,
|
||||||
val poll: Poll?,
|
val poll: Poll?,
|
||||||
@SerializedName("media_attachments") val mediaAttachments: List<Attachment>,
|
@Json(name = "media_attachments") val mediaAttachments: List<Attachment>,
|
||||||
val emojis: List<Emoji>,
|
val emojis: List<Emoji>,
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,12 +17,14 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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(
|
data class StatusParams(
|
||||||
val text: String,
|
val text: String,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
val visibility: Status.Visibility,
|
val visibility: Status.Visibility,
|
||||||
@SerializedName("spoiler_text") val spoilerText: String,
|
@Json(name = "spoiler_text") val spoilerText: String,
|
||||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
@Json(name = "in_reply_to_id") val inReplyToId: String?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,10 +17,12 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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(
|
data class StatusSource(
|
||||||
val id: String,
|
val id: String,
|
||||||
val text: 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
|
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.
|
* 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.
|
* Prefer this class over [Account] because it uses way less memory & deserializes faster from json.
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class TimelineAccount(
|
data class TimelineAccount(
|
||||||
val id: String,
|
val id: String,
|
||||||
@SerializedName("username") val localUsername: String,
|
@Json(name = "username") val localUsername: String,
|
||||||
@SerializedName("acct") val username: String,
|
@Json(name = "acct") val username: String,
|
||||||
// should never be null per Api definition, but some servers break the contract
|
// 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")
|
@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 url: String,
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val note: String,
|
val note: String,
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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/ */
|
/** https://docs.joinmastodon.org/entities/Translation/ */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Translation(
|
data class Translation(
|
||||||
/** The translated text of the status (HTML), equivalent to [Status.content] */
|
/** The translated text of the status (HTML), equivalent to [Status.content] */
|
||||||
val content: String,
|
val content: String,
|
||||||
|
@ -28,14 +30,14 @@ data class Translation(
|
||||||
* The language of the source text, as auto-detected by the machine translation
|
* The language of the source text, as auto-detected by the machine translation
|
||||||
* (ISO 639 language code)
|
* (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
|
// Not documented, see https://github.com/mastodon/documentation/issues/1248
|
||||||
/**
|
/**
|
||||||
* The translated spoiler text of the status (text), if it exists, equivalent to
|
* The translated spoiler text of the status (text), if it exists, equivalent to
|
||||||
* [Status.spoilerText]
|
* [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
|
// Not documented, see https://github.com/mastodon/documentation/issues/1248
|
||||||
/** The translated poll (if it exists) */
|
/** 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
|
* Translated descriptions for media attachments, if any were attached. Other metadata has
|
||||||
* to be determined from the original attachment.
|
* 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 */
|
/** The service that provided the machine translation */
|
||||||
val provider: String,
|
val provider: String,
|
||||||
|
@ -56,17 +58,20 @@ data class Translation(
|
||||||
* A translated poll. Does not contain all the poll data, only the translated text.
|
* 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.
|
* Vote counts and other metadata has to be determined from the original poll object.
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class TranslatedPoll(
|
data class TranslatedPoll(
|
||||||
val id: String,
|
val id: String,
|
||||||
val options: List<TranslatedPollOption>,
|
val options: List<TranslatedPollOption>,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** A translated poll option. */
|
/** A translated poll option. */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class TranslatedPollOption(
|
data class TranslatedPollOption(
|
||||||
val title: String,
|
val title: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** A translated attachment. Only the description is translated */
|
/** A translated attachment. Only the description is translated */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class TranslatedAttachment(
|
data class TranslatedAttachment(
|
||||||
val id: String,
|
val id: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import java.util.Date
|
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 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.)
|
* (@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(
|
data class TrendingTag(
|
||||||
val name: String,
|
val name: String,
|
||||||
val history: List<TrendingTagHistory>,
|
val history: List<TrendingTagHistory>,
|
||||||
|
@ -39,6 +41,7 @@ data class TrendingTag(
|
||||||
* @param accounts The number of accounts that have posted with this hashtag.
|
* @param accounts The number of accounts that have posted with this hashtag.
|
||||||
* @param uses The number of posts with this hashtag.
|
* @param uses The number of posts with this hashtag.
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class TrendingTagHistory(
|
data class TrendingTagHistory(
|
||||||
val day: String,
|
val day: String,
|
||||||
val accounts: String,
|
val accounts: String,
|
||||||
|
|
|
@ -17,19 +17,20 @@
|
||||||
|
|
||||||
package app.pachli.core.network.model
|
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 {
|
enum class PreviewCardKind {
|
||||||
@SerializedName("link")
|
@Json(name = "link")
|
||||||
LINK,
|
LINK,
|
||||||
|
|
||||||
@SerializedName("photo")
|
@Json(name = "photo")
|
||||||
PHOTO,
|
PHOTO,
|
||||||
|
|
||||||
@SerializedName("video")
|
@Json(name = "video")
|
||||||
VIDEO,
|
VIDEO,
|
||||||
|
|
||||||
@SerializedName("rich")
|
@Json(name = "rich")
|
||||||
RICH,
|
RICH,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ interface PreviewCard {
|
||||||
val blurhash: String?
|
val blurhash: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class LinkHistory(
|
data class LinkHistory(
|
||||||
val day: String,
|
val day: String,
|
||||||
val accounts: Int,
|
val accounts: Int,
|
||||||
|
@ -60,20 +62,21 @@ data class LinkHistory(
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Represents a https://docs.joinmastodon.org/entities/PreviewCard/#trends-link */
|
/** Represents a https://docs.joinmastodon.org/entities/PreviewCard/#trends-link */
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class TrendsLink(
|
data class TrendsLink(
|
||||||
override val url: String,
|
override val url: String,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val description: String,
|
override val description: String,
|
||||||
@SerializedName("type") override val kind: PreviewCardKind,
|
@Json(name = "type") override val kind: PreviewCardKind,
|
||||||
@SerializedName("author_name") override val authorName: String,
|
@Json(name = "author_name") override val authorName: String,
|
||||||
@SerializedName("author_url") override val authorUrl: String,
|
@Json(name = "author_url") override val authorUrl: String,
|
||||||
@SerializedName("provider_name") override val providerName: String,
|
@Json(name = "provider_name") override val providerName: String,
|
||||||
@SerializedName("provider_url") override val providerUrl: String,
|
@Json(name = "provider_url") override val providerUrl: String,
|
||||||
override val html: String,
|
override val html: String,
|
||||||
override val width: Int,
|
override val width: Int,
|
||||||
override val height: Int,
|
override val height: Int,
|
||||||
override val image: String? = null,
|
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,
|
override val blurhash: String? = null,
|
||||||
val history: List<LinkHistory>,
|
val history: List<LinkHistory>,
|
||||||
) : PreviewCard
|
) : 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.Err
|
||||||
import com.github.michaelbull.result.Ok
|
import com.github.michaelbull.result.Ok
|
||||||
import com.github.michaelbull.result.Result
|
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.
|
* The JRD document that links to one or more URLs that contain the schema.
|
||||||
*
|
*
|
||||||
* See https://nodeinfo.diaspora.software/protocol.html.
|
* See https://nodeinfo.diaspora.software/protocol.html.
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class UnvalidatedJrd(val links: List<Link>) {
|
data class UnvalidatedJrd(val links: List<Link>) {
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Link(val rel: String, val href: String)
|
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
|
* See https://nodeinfo.diaspora.software/protocol.html and
|
||||||
* https://nodeinfo.diaspora.software/schema.html.
|
* https://nodeinfo.diaspora.software/schema.html.
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class UnvalidatedNodeInfo(val software: Software?) {
|
data class UnvalidatedNodeInfo(val software: Software?) {
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Software(val name: String?, val version: String?)
|
data class Software(val name: String?, val version: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -202,6 +202,14 @@ interface MastodonApi {
|
||||||
@Body status: NewStatus,
|
@Body status: NewStatus,
|
||||||
): NetworkResult<Status>
|
): 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}")
|
@GET("api/v1/statuses/{id}")
|
||||||
suspend fun status(
|
suspend fun status(
|
||||||
@Path("id") statusId: String,
|
@Path("id") statusId: String,
|
||||||
|
|
|
@ -44,9 +44,10 @@ import app.pachli.core.network.model.Users
|
||||||
import app.pachli.core.network.model.nodeinfo.NodeInfo
|
import app.pachli.core.network.model.nodeinfo.NodeInfo
|
||||||
import com.github.michaelbull.result.Ok
|
import com.github.michaelbull.result.Ok
|
||||||
import com.github.michaelbull.result.Result
|
import com.github.michaelbull.result.Result
|
||||||
|
import com.google.common.reflect.TypeToken
|
||||||
import com.google.common.truth.Truth.assertWithMessage
|
import com.google.common.truth.Truth.assertWithMessage
|
||||||
import com.google.gson.Gson
|
import com.squareup.moshi.Moshi
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.squareup.moshi.adapter
|
||||||
import io.github.z4kn4fein.semver.toVersion
|
import io.github.z4kn4fein.semver.toVersion
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -274,8 +275,8 @@ class ServerTest(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerVersionTest() {
|
class ServerVersionTest {
|
||||||
private val gson = Gson()
|
private val moshi = Moshi.Builder().build()
|
||||||
|
|
||||||
private fun loadJsonAsString(fileName: String): String {
|
private fun loadJsonAsString(fileName: String): String {
|
||||||
return javaClass.getResourceAsStream("/$fileName")!!
|
return javaClass.getResourceAsStream("/$fileName")!!
|
||||||
|
@ -290,15 +291,15 @@ class ServerVersionTest() {
|
||||||
* that have been seen by Fediverse Observer. These version strings
|
* that have been seen by Fediverse Observer. These version strings
|
||||||
* are then parsed and are all expected to parse correctly.
|
* are then parsed and are all expected to parse correctly.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun parseVersionString() {
|
fun parseVersionString() {
|
||||||
val mapType: TypeToken<Map<String, Set<String>>> =
|
val mapType: TypeToken<Map<String, Set<String>>> =
|
||||||
object : TypeToken<Map<String, Set<String>>>() {}
|
object : TypeToken<Map<String, Set<String>>>() {}
|
||||||
|
|
||||||
val serverVersions = gson.fromJson(
|
val serverVersions = moshi.adapter<Map<String, Set<String>>>()
|
||||||
loadJsonAsString("server-versions.json5"),
|
.lenient()
|
||||||
mapType,
|
.fromJson(loadJsonAsString("server-versions.json5"))!!
|
||||||
)
|
|
||||||
|
|
||||||
// Sanity check that data was loaded correctly. Expect at least 5 distinct keys
|
// 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")
|
assertWithMessage("number of server types in server-versions.json5 is too low")
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
package app.pachli.core.network.json
|
package app.pachli.core.network.json
|
||||||
|
|
||||||
|
import app.pachli.core.network.json.GuardedAdapter.Companion.GuardedAdapterFactory
|
||||||
import app.pachli.core.network.model.Relationship
|
import app.pachli.core.network.model.Relationship
|
||||||
import com.google.gson.Gson
|
import com.google.common.truth.Truth.assertThat
|
||||||
import org.junit.Assert.assertEquals
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapter
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class GuardedBooleanAdapterTest {
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
class GuardedAdapterTest {
|
||||||
|
|
||||||
private val gson = Gson()
|
private val moshi = Moshi.Builder()
|
||||||
|
.add(GuardedAdapterFactory())
|
||||||
|
.build()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should deserialize Relationship when attribute 'subscribing' is a boolean`() {
|
fun `should deserialize Relationship when attribute 'subscribing' is a boolean`() {
|
||||||
|
@ -30,7 +35,7 @@ class GuardedBooleanAdapterTest {
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
assertEquals(
|
assertThat(moshi.adapter<Relationship>().fromJson(jsonInput)).isEqualTo(
|
||||||
Relationship(
|
Relationship(
|
||||||
id = "1",
|
id = "1",
|
||||||
following = true,
|
following = true,
|
||||||
|
@ -45,7 +50,6 @@ class GuardedBooleanAdapterTest {
|
||||||
note = "Hi",
|
note = "Hi",
|
||||||
notifying = false,
|
notifying = false,
|
||||||
),
|
),
|
||||||
gson.fromJson(jsonInput, Relationship::class.java),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +74,7 @@ class GuardedBooleanAdapterTest {
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
assertEquals(
|
assertThat(moshi.adapter<Relationship>().fromJson(jsonInput)).isEqualTo(
|
||||||
Relationship(
|
Relationship(
|
||||||
id = "2",
|
id = "2",
|
||||||
following = true,
|
following = true,
|
||||||
|
@ -85,7 +89,6 @@ class GuardedBooleanAdapterTest {
|
||||||
note = "Hi",
|
note = "Hi",
|
||||||
notifying = false,
|
notifying = false,
|
||||||
),
|
),
|
||||||
gson.fromJson(jsonInput, Relationship::class.java),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +112,7 @@ class GuardedBooleanAdapterTest {
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
assertEquals(
|
assertThat(moshi.adapter<Relationship>().fromJson(jsonInput)).isEqualTo(
|
||||||
Relationship(
|
Relationship(
|
||||||
id = "3",
|
id = "3",
|
||||||
following = true,
|
following = true,
|
||||||
|
@ -124,7 +127,6 @@ class GuardedBooleanAdapterTest {
|
||||||
note = "Hi",
|
note = "Hi",
|
||||||
notifying = false,
|
notifying = false,
|
||||||
),
|
),
|
||||||
gson.fromJson(jsonInput, Relationship::class.java),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -39,7 +39,6 @@ filemoji-compat = "3.2.7"
|
||||||
glide = "4.16.0"
|
glide = "4.16.0"
|
||||||
# Deliberate downgrade, https://github.com/tuskyapp/Tusky/issues/3631
|
# Deliberate downgrade, https://github.com/tuskyapp/Tusky/issues/3631
|
||||||
glide-animation-plugin = "2.23.0"
|
glide-animation-plugin = "2.23.0"
|
||||||
gson = "2.10.1"
|
|
||||||
hilt = "2.50"
|
hilt = "2.50"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "1.9.22"
|
kotlin = "1.9.22"
|
||||||
|
@ -53,6 +52,7 @@ material-drawer = "9.0.2"
|
||||||
material-typeface = "4.0.0.2-kotlin"
|
material-typeface = "4.0.0.2-kotlin"
|
||||||
mockito-inline = "5.2.0"
|
mockito-inline = "5.2.0"
|
||||||
mockito-kotlin = "5.2.1"
|
mockito-kotlin = "5.2.1"
|
||||||
|
moshi = "1.15.1"
|
||||||
networkresult-calladapter = "1.0.0"
|
networkresult-calladapter = "1.0.0"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
quadrant = "1.9.1"
|
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-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
|
||||||
glide-core = { module = "com.github.bumptech.glide:glide", 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" }
|
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-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||||
hilt-compiler = { module = "com.google.dagger:hilt-compiler", 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" }
|
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-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" }
|
||||||
mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito-inline" }
|
mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito-inline" }
|
||||||
mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
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" }
|
networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" }
|
||||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", 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" }
|
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||||
rxjava3-android = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid3" }
|
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"]
|
material-drawer = ["material-drawer-core", "material-drawer-iconics"]
|
||||||
mockito = ["mockito-kotlin", "mockito-inline"]
|
mockito = ["mockito-kotlin", "mockito-inline"]
|
||||||
okhttp = ["okhttp-core", "okhttp-logging-interceptor"]
|
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"]
|
room = ["androidx-room-ktx", "androidx-room-paging"]
|
||||||
rxjava3 = ["rxjava3-core", "rxjava3-android", "rxjava3-kotlin"]
|
rxjava3 = ["rxjava3-core", "rxjava3-android", "rxjava3-kotlin"]
|
||||||
xmldiff = ["diffx", "xmlwriter"]
|
xmldiff = ["diffx", "xmlwriter"]
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
id("com.apollographql.apollo3") version "3.8.2"
|
id("com.apollographql.apollo3") version "3.8.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +32,9 @@ dependencies {
|
||||||
implementation("io.github.oshai:kotlin-logging-jvm:5.1.0")
|
implementation("io.github.oshai:kotlin-logging-jvm:5.1.0")
|
||||||
implementation("ch.qos.logback:logback-classic:1.4.11")
|
implementation("ch.qos.logback:logback-classic:1.4.11")
|
||||||
|
|
||||||
implementation(libs.gson)
|
// Moshi
|
||||||
|
implementation(libs.moshi)
|
||||||
|
ksp(libs.moshi.codegen)
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation(kotlin("test"))
|
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.core.UsageError
|
||||||
import com.github.ajalt.clikt.parameters.options.flag
|
import com.github.ajalt.clikt.parameters.options.flag
|
||||||
import com.github.ajalt.clikt.parameters.options.option
|
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.DelegatingKLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
@ -65,6 +66,7 @@ class App : CliktCommand(help = """Update server-versions.json5""") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
override fun run() = runBlocking {
|
override fun run() = runBlocking {
|
||||||
System.setProperty("file.encoding", "UTF8")
|
System.setProperty("file.encoding", "UTF8")
|
||||||
((log as? DelegatingKLogger<*>)?.underlyingLogger as Logger).level = if (verbose) Level.INFO else Level.WARN
|
((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()
|
val moshi = Moshi.Builder().build()
|
||||||
w.print(gson.toJson(m))
|
w.print(moshi.adapter<Map<String, Set<String>>>().indent(" ").toJson(m))
|
||||||
w.close()
|
w.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue