Replace Gson library with Moshi (#4309)
**! ! Warning**: Do not merge before testing every API call and database read involving JSON ! **Gson** is obsolete and has been superseded by **Moshi**. But more importantly, parsing Kotlin objects using Gson is _dangerous_ because Gson uses Java serialization and is **not Kotlin-aware**. This has two main consequences: - Fields of non-null types may end up null at runtime. Parsing will succeed, but the code may crash later with a `NullPointerException` when trying to access a field member; - Default values of constructor parameters are always ignored. When absent, reference types will be null, booleans will be false and integers will be zero. On the other hand, Kotlin-aware parsers like **Moshi** or **Kotlin Serialization** will validate at parsing time that all received fields comply with the Kotlin contract and avoid errors at runtime, making apps more stable and schema mismatches easier to detect (as long as logs are accessible): - Receiving a null value for a non-null type will generate a parsing error; - Optional types are declared explicitly by adding a default value. **A missing value with no default value declaration will generate a parsing error.** Migrating the entity declarations from Gson to Moshi will make the code more robust but is not an easy task because of the semantic differences. With Gson, both nullable and optional fields are represented with a null value. After converting to Moshi, some nullable entities can become non-null with a default value (if they are optional and not nullable), others can stay nullable with no default value (if they are mandatory and nullable), and others can become **nullable with a default value of null** (if they are optional _or_ nullable _or_ both). That third option is the safest bet when it's not clear if a field is optional or not, except for lists which can usually be declared as non-null with a default value of an empty list (I have yet to see a nullable array type in the Mastodon API). Fields that are currently declared as non-null present another challenge. In theory, they should remain as-is and everything will work fine. In practice, **because Gson is not aware of nullable types at all**, it's possible that some non-null fields currently hold a null value in some cases but the app does not report any error because the field is not accessed by Kotlin code in that scenario. After migrating to Moshi however, parsing such a field will now fail early if a null value or no value is received. These fields will have to be identified by heavily testing the app and looking for parsing errors (`JsonDataException`) and/or by going through the Mastodon documentation. A default value needs to be added for missing optional fields, and their type could optionally be changed to nullable, depending on the case. Gson is also currently used to serialize and deserialize objects to and from the local database, which is also challenging because backwards compatibility needs to be preserved. Fortunately, by default Gson omits writing null fields, so a field of type `List<T>?` could be replaced with a field of type `List<T>` with a default value of `emptyList()` and reading back the old data should still work. However, nullable lists that are written directly (not as a field of another object) will still be serialized to JSON as `"null"` so the deserializing code must still be handling null properly. Finally, changing the database schema is out of scope for this pull request, so database entities that also happen to be serialized with Gson will keep their original types even if they could be made non-null as an improvement. In the end this is all for the best, because the app will be more reliable and errors will be easier to detect by showing up earlier with a clear error message. Not to mention the performance benefits of using Moshi compared to Gson. - Replace Gson reflection with Moshi Kotlin codegen to generate all parsers at compile time. - Replace custom `Rfc3339DateJsonAdapter` with the one provided by moshi-adapters. - Replace custom `JsonDeserializer` classes for Enum types with `EnumJsonAdapter.create(T).withUnknownFallback()` from moshi-adapters to support fallback values. - Replace `GuardedBooleanAdapter` with the more generic `GuardedAdapter` which works with any type. Any nullable field may now be annotated with `@Guarded`. - Remove Proguard rules related to Json entities. Each Json entity needs to be annotated with `@JsonClass` with no exception, and adding this annotation will ensure that R8/Proguard will handle the entities properly. - Replace some nullable Boolean fields with non-null Boolean fields with a default value where possible. - Replace some nullable list fields with non-null list fields with a default value of `emptyList()` where possible. - Update `TimelineDao` to perform all Json conversions internally using `Converters` so no Gson or Moshi instance has to be passed to its methods. - ~~Create a custom `DraftAttachmentJsonAdapter` to serialize and deserialize `DraftAttachment` which is a special entity that supports more than one json name per field. A custom adapter is necessary because there is not direct equivalent of `@SerializedName(alternate = [...])` in Moshi.~~ Remove alternate names for some `DraftAttachment` fields which were used as a workaround to deserialize local data in 2-years old builds of Tusky. - Update tests to make them work with Moshi. - Simplify a few `equals()` implementations. - Change a few functions to `val`s - Turn `NetworkModule` into an `object` (since it contains no abstract methods). Please test the app thoroughly before merging. There may be some fields currently declared as mandatory that are actually optional.
This commit is contained in:
parent
5343766886
commit
df7b11afc3
87 changed files with 767 additions and 992 deletions
|
|
@ -15,8 +15,10 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccessToken(
|
||||
@SerializedName("access_token") val accessToken: String
|
||||
@Json(name = "access_token") val accessToken: String
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,32 +15,34 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Account(
|
||||
val id: String,
|
||||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String,
|
||||
@Json(name = "acct") val username: String,
|
||||
// should never be null per Api definition, but some servers break the contract
|
||||
@SerializedName("display_name") val displayName: String?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@Json(name = "display_name") val displayName: String? = null,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
val note: String,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
val header: String,
|
||||
val locked: Boolean = false,
|
||||
@SerializedName("followers_count") val followersCount: Int = 0,
|
||||
@SerializedName("following_count") val followingCount: Int = 0,
|
||||
@SerializedName("statuses_count") val statusesCount: Int = 0,
|
||||
@Json(name = "followers_count") val followersCount: Int = 0,
|
||||
@Json(name = "following_count") val followingCount: Int = 0,
|
||||
@Json(name = "statuses_count") val statusesCount: Int = 0,
|
||||
val source: AccountSource? = null,
|
||||
val bot: Boolean = false,
|
||||
// nullable for backward compatibility
|
||||
val emojis: List<Emoji>? = emptyList(),
|
||||
// nullable for backward compatibility
|
||||
val fields: List<Field>? = emptyList(),
|
||||
// default value for backward compatibility
|
||||
val emojis: List<Emoji> = emptyList(),
|
||||
// default value for backward compatibility
|
||||
val fields: List<Field> = emptyList(),
|
||||
val moved: Account? = null,
|
||||
val roles: List<Role>? = emptyList()
|
||||
val roles: List<Role> = emptyList()
|
||||
) {
|
||||
|
||||
val name: String
|
||||
|
|
@ -50,28 +52,33 @@ data class Account(
|
|||
displayName
|
||||
}
|
||||
|
||||
fun isRemote(): Boolean = this.username != this.localUsername
|
||||
val isRemote: Boolean
|
||||
get() = this.username != this.localUsername
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccountSource(
|
||||
val privacy: Status.Visibility?,
|
||||
val sensitive: Boolean?,
|
||||
val note: String?,
|
||||
val fields: List<StringField>?,
|
||||
val language: String?
|
||||
val privacy: Status.Visibility = Status.Visibility.PUBLIC,
|
||||
val sensitive: Boolean? = null,
|
||||
val note: String? = null,
|
||||
val fields: List<StringField> = emptyList(),
|
||||
val language: String? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Field(
|
||||
val name: String,
|
||||
val value: String,
|
||||
@SerializedName("verified_at") val verifiedAt: Date?
|
||||
@Json(name = "verified_at") val verifiedAt: Date? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StringField(
|
||||
val name: String,
|
||||
val value: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Role(
|
||||
val name: String,
|
||||
val color: String
|
||||
|
|
|
|||
|
|
@ -15,18 +15,20 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Announcement(
|
||||
val id: String,
|
||||
val content: String,
|
||||
@SerializedName("starts_at") val startsAt: Date?,
|
||||
@SerializedName("ends_at") val endsAt: Date?,
|
||||
@SerializedName("all_day") val allDay: Boolean,
|
||||
@SerializedName("published_at") val publishedAt: Date,
|
||||
@SerializedName("updated_at") val updatedAt: Date,
|
||||
val read: Boolean,
|
||||
@Json(name = "starts_at") val startsAt: Date? = null,
|
||||
@Json(name = "ends_at") val endsAt: Date? = null,
|
||||
@Json(name = "all_day") val allDay: Boolean,
|
||||
@Json(name = "published_at") val publishedAt: Date,
|
||||
@Json(name = "updated_at") val updatedAt: Date,
|
||||
val read: Boolean = false,
|
||||
val mentions: List<Status.Mention>,
|
||||
val statuses: List<Status>,
|
||||
val tags: List<HashTag>,
|
||||
|
|
@ -36,21 +38,21 @@ data class Announcement(
|
|||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
if (other !is Announcement) return false
|
||||
|
||||
val announcement = other as Announcement?
|
||||
return id == announcement?.id
|
||||
return id == other.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Reaction(
|
||||
val name: String,
|
||||
val count: Int,
|
||||
val me: Boolean,
|
||||
val url: String?,
|
||||
@SerializedName("static_url") val staticUrl: String?
|
||||
val me: Boolean = false,
|
||||
val url: String? = null,
|
||||
@Json(name = "static_url") val staticUrl: String? = null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AppCredentials(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("client_secret") val clientSecret: String
|
||||
@Json(name = "client_id") val clientId: String,
|
||||
@Json(name = "client_secret") val clientSecret: String
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,70 +16,50 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Attachment(
|
||||
val id: String,
|
||||
val url: String,
|
||||
// can be null for e.g. audio attachments
|
||||
@SerializedName("preview_url") val previewUrl: String?,
|
||||
val meta: MetaData?,
|
||||
@Json(name = "preview_url") val previewUrl: String? = null,
|
||||
val meta: MetaData? = null,
|
||||
val type: Type,
|
||||
val description: String?,
|
||||
val blurhash: String?
|
||||
val description: String? = null,
|
||||
val blurhash: String? = null
|
||||
) : Parcelable {
|
||||
|
||||
@JsonAdapter(MediaTypeDeserializer::class)
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Type {
|
||||
@SerializedName("image")
|
||||
@Json(name = "image")
|
||||
IMAGE,
|
||||
|
||||
@SerializedName("gifv")
|
||||
@Json(name = "gifv")
|
||||
GIFV,
|
||||
|
||||
@SerializedName("video")
|
||||
@Json(name = "video")
|
||||
VIDEO,
|
||||
|
||||
@SerializedName("audio")
|
||||
@Json(name = "audio")
|
||||
AUDIO,
|
||||
|
||||
@SerializedName("unknown")
|
||||
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].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class MetaData(
|
||||
val focus: Focus?,
|
||||
val duration: Float?,
|
||||
val original: Size?,
|
||||
val small: Size?
|
||||
val focus: Focus? = null,
|
||||
val duration: Float? = null,
|
||||
val original: Size? = null,
|
||||
val small: Size? = null
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
|
|
@ -88,6 +68,7 @@ data class Attachment(
|
|||
* See here for more details what the x and y mean:
|
||||
* https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Focus(
|
||||
val x: Float,
|
||||
|
|
@ -99,10 +80,11 @@ data class Attachment(
|
|||
/**
|
||||
* The size of an image, used to specify the width/height.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Size(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val aspect: Double
|
||||
val aspect: Double = 0.0
|
||||
) : Parcelable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,19 +15,21 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Card(
|
||||
val url: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
@SerializedName("author_name") val authorName: String,
|
||||
val image: String,
|
||||
@Json(name = "author_name") val authorName: String,
|
||||
val image: String? = null,
|
||||
val type: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val blurhash: String?,
|
||||
@SerializedName("embed_url") val embedUrl: String?
|
||||
val blurhash: String? = null,
|
||||
@Json(name = "embed_url") val embedUrl: String? = null
|
||||
) {
|
||||
|
||||
override fun hashCode() = url.hashCode()
|
||||
|
|
@ -36,8 +38,7 @@ data class Card(
|
|||
if (other !is Card) {
|
||||
return false
|
||||
}
|
||||
val account = other as Card?
|
||||
return account?.url == this.url
|
||||
return other.url == this.url
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Conversation(
|
||||
val id: String,
|
||||
val accounts: List<TimelineAccount>,
|
||||
// should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
|
||||
@SerializedName("last_status") val lastStatus: Status?,
|
||||
// should never be null, but apparently it's possible https://github.com/tuskyapp/Tusky/issues/1038
|
||||
@Json(name = "last_status") val lastStatus: Status? = null,
|
||||
val unread: Boolean
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,21 +15,22 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DeletedStatus(
|
||||
val text: String?,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String? = null,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val visibility: Status.Visibility,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("media_attachments") val attachments: List<Attachment>?,
|
||||
val poll: Poll?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
val language: String?
|
||||
@Json(name = "media_attachments") val attachments: List<Attachment>,
|
||||
val poll: Poll? = null,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
val language: String? = null
|
||||
) {
|
||||
fun isEmpty(): Boolean {
|
||||
return text == null && attachments == null
|
||||
}
|
||||
val isEmpty: Boolean
|
||||
get() = text == null && attachments.isEmpty()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,15 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Emoji(
|
||||
val shortcode: String,
|
||||
val url: String,
|
||||
@SerializedName("static_url") val staticUrl: String,
|
||||
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?
|
||||
@Json(name = "static_url") val staticUrl: String,
|
||||
@Json(name = "visible_in_picker") val visibleInPicker: Boolean = true
|
||||
) : Parcelable
|
||||
|
|
|
|||
|
|
@ -17,8 +17,12 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** @see [Error](https://docs.joinmastodon.org/entities/Error/) */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Error(
|
||||
val error: String,
|
||||
val error_description: String?
|
||||
@Json(name = "error_description") val errorDescription: String? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Filter(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val context: List<String>,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@SerializedName("filter_action") private val filterAction: String,
|
||||
val keywords: List<FilterKeyword>
|
||||
@Json(name = "expires_at") val expiresAt: Date? = null,
|
||||
@Json(name = "filter_action") val filterAction: String,
|
||||
// This field is mandatory according to the API documentation but is in fact optional in some instances
|
||||
val keywords: List<FilterKeyword> = emptyList(),
|
||||
// val statuses: List<FilterStatus>,
|
||||
) : Parcelable {
|
||||
enum class Action(val action: String) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class FilterKeyword(
|
||||
val id: String,
|
||||
val keyword: String,
|
||||
@SerializedName("whole_word") val wholeWord: Boolean
|
||||
@Json(name = "whole_word") val wholeWord: Boolean
|
||||
) : Parcelable
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FilterResult(
|
||||
val filter: Filter,
|
||||
@SerializedName("keyword_matches") val keywordMatches: List<String>?,
|
||||
@SerializedName("status_matches") val statusMatches: List<String>?
|
||||
// @Json(name = "keyword_matches") val keywordMatches: List<String>? = null,
|
||||
// @Json(name = "status_matches") val statusMatches: List<String>? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,16 +15,18 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FilterV1(
|
||||
val id: String,
|
||||
val phrase: String,
|
||||
val context: List<String>,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@Json(name = "expires_at") val expiresAt: Date? = null,
|
||||
val irreversible: Boolean,
|
||||
@SerializedName("whole_word") val wholeWord: Boolean
|
||||
@Json(name = "whole_word") val wholeWord: Boolean
|
||||
) {
|
||||
companion object {
|
||||
const val HOME = "home"
|
||||
|
|
@ -42,8 +44,7 @@ data class FilterV1(
|
|||
if (other !is FilterV1) {
|
||||
return false
|
||||
}
|
||||
val filter = other as FilterV1?
|
||||
return filter?.id.equals(id)
|
||||
return other.id == id
|
||||
}
|
||||
|
||||
fun toFilter(): Filter {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
data class HashTag(val name: String, val url: String, val following: Boolean? = null)
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class HashTag(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val following: Boolean? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,72 +1,98 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Instance(
|
||||
val domain: String,
|
||||
// val title: String,
|
||||
val version: String,
|
||||
// @SerializedName("source_url") val sourceUrl: String,
|
||||
// @Json(name = "source_url") val sourceUrl: String,
|
||||
// val description: String,
|
||||
// val usage: Usage,
|
||||
// val thumbnail: Thumbnail,
|
||||
// val languages: List<String>,
|
||||
val configuration: Configuration?,
|
||||
val configuration: Configuration? = null,
|
||||
// val registrations: Registrations,
|
||||
// val contact: Contact,
|
||||
val rules: List<Rule>?,
|
||||
val pleroma: PleromaConfiguration?
|
||||
val rules: List<Rule> = emptyList(),
|
||||
val pleroma: PleromaConfiguration? = null
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Usage(val users: Users) {
|
||||
data class Users(@SerializedName("active_month") val activeMonth: Int)
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Users(@Json(name = "active_month") val activeMonth: Int)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Thumbnail(
|
||||
val url: String,
|
||||
val blurhash: String?,
|
||||
val versions: Versions?
|
||||
val blurhash: String? = null,
|
||||
val versions: Versions? = null
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Versions(
|
||||
@SerializedName("@1x") val at1x: String?,
|
||||
@SerializedName("@2x") val at2x: String?
|
||||
@Json(name = "@1x") val at1x: String? = null,
|
||||
@Json(name = "@2x") val at2x: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Configuration(
|
||||
val urls: Urls?,
|
||||
val accounts: Accounts?,
|
||||
val statuses: Statuses?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachments?,
|
||||
val polls: Polls?,
|
||||
val translation: Translation?
|
||||
val urls: Urls? = null,
|
||||
val accounts: Accounts? = null,
|
||||
val statuses: Statuses? = null,
|
||||
@Json(name = "media_attachments") val mediaAttachments: MediaAttachments? = null,
|
||||
val polls: Polls? = null,
|
||||
val translation: Translation? = null
|
||||
) {
|
||||
data class Urls(@SerializedName("streaming_api") val streamingApi: String)
|
||||
data class Accounts(@SerializedName("max_featured_tags") val maxFeaturedTags: Int)
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Urls(@Json(name = "streaming_api") val streamingApi: String? = null)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Accounts(@Json(name = "max_featured_tags") val maxFeaturedTags: Int)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Statuses(
|
||||
@SerializedName("max_characters") val maxCharacters: Int,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int,
|
||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int
|
||||
@Json(name = "max_characters") val maxCharacters: Int? = null,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaAttachments(
|
||||
// Warning: This is an array in mastodon and a dictionary in friendica
|
||||
// @SerializedName("supported_mime_types") val supportedMimeTypes: List<String>,
|
||||
@SerializedName("image_size_limit") val imageSizeLimitBytes: Long,
|
||||
@SerializedName("image_matrix_limit") val imagePixelCountLimit: Long,
|
||||
@SerializedName("video_size_limit") val videoSizeLimitBytes: Long,
|
||||
@SerializedName("video_matrix_limit") val videoPixelCountLimit: Long,
|
||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int
|
||||
// @Json(name = "supported_mime_types") val supportedMimeTypes: List<String> = emptyList(),
|
||||
@Json(name = "image_size_limit") val imageSizeLimitBytes: Long? = null,
|
||||
@Json(name = "image_matrix_limit") val imagePixelCountLimit: Long? = null,
|
||||
@Json(name = "video_size_limit") val videoSizeLimitBytes: Long? = null,
|
||||
@Json(name = "video_matrix_limit") val videoPixelCountLimit: Long? = null,
|
||||
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Polls(
|
||||
@SerializedName("max_options") val maxOptions: Int,
|
||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int,
|
||||
@SerializedName("min_expiration") val minExpirationSeconds: Int,
|
||||
@SerializedName("max_expiration") val maxExpirationSeconds: Int
|
||||
@Json(name = "max_options") val maxOptions: Int? = null,
|
||||
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int? = null,
|
||||
@Json(name = "min_expiration") val minExpirationSeconds: Int? = null,
|
||||
@Json(name = "max_expiration") val maxExpirationSeconds: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Translation(val enabled: Boolean)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Registrations(
|
||||
val enabled: Boolean,
|
||||
@SerializedName("approval_required") val approvalRequired: Boolean,
|
||||
val message: String?
|
||||
@Json(name = "approval_required") val approvalRequired: Boolean,
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Contact(val email: String, val account: Account)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Rule(val id: String, val text: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV1(
|
||||
val uri: String,
|
||||
// val title: String,
|
||||
|
|
@ -27,14 +29,14 @@ data class InstanceV1(
|
|||
// val stats: Map<String, Int>?,
|
||||
// val thumbnail: String?,
|
||||
// val languages: List<String>,
|
||||
// @SerializedName("contact_account") val contactAccount: Account,
|
||||
@SerializedName("max_toot_chars") val maxTootChars: Int?,
|
||||
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?,
|
||||
val configuration: InstanceConfiguration?,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
||||
val pleroma: PleromaConfiguration?,
|
||||
@SerializedName("upload_limit") val uploadLimit: Int?,
|
||||
val rules: List<InstanceRules>?
|
||||
// @Json(name = "contact_account") val contactAccount: Account?,
|
||||
@Json(name = "max_toot_chars") val maxTootChars: Int? = null,
|
||||
@Json(name = "poll_limits") val pollConfiguration: PollConfiguration? = null,
|
||||
val configuration: InstanceConfiguration? = null,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||
val pleroma: PleromaConfiguration? = null,
|
||||
@Json(name = "upload_limit") val uploadLimit: Int? = null,
|
||||
val rules: List<InstanceRules> = emptyList()
|
||||
) {
|
||||
override fun hashCode(): Int {
|
||||
return uri.hashCode()
|
||||
|
|
@ -44,54 +46,61 @@ data class InstanceV1(
|
|||
if (other !is InstanceV1) {
|
||||
return false
|
||||
}
|
||||
val instance = other as InstanceV1?
|
||||
return instance?.uri.equals(uri)
|
||||
return other.uri == uri
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollConfiguration(
|
||||
@SerializedName("max_options") val maxOptions: Int?,
|
||||
@SerializedName("max_option_chars") val maxOptionChars: Int?,
|
||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
|
||||
@SerializedName("min_expiration") val minExpiration: Int?,
|
||||
@SerializedName("max_expiration") val maxExpiration: Int?
|
||||
@Json(name = "max_options") val maxOptions: Int? = null,
|
||||
@Json(name = "max_option_chars") val maxOptionChars: Int? = null,
|
||||
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int? = null,
|
||||
@Json(name = "min_expiration") val minExpiration: Int? = null,
|
||||
@Json(name = "max_expiration") val maxExpiration: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceConfiguration(
|
||||
val statuses: StatusConfiguration?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
||||
val polls: PollConfiguration?
|
||||
val statuses: StatusConfiguration? = null,
|
||||
@Json(name = "media_attachments") val mediaAttachments: MediaAttachmentConfiguration? = null,
|
||||
val polls: PollConfiguration? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusConfiguration(
|
||||
@SerializedName("max_characters") val maxCharacters: Int?,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?
|
||||
@Json(name = "max_characters") val maxCharacters: Int? = null,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaAttachmentConfiguration(
|
||||
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>?,
|
||||
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
|
||||
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
|
||||
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
|
||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
||||
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?
|
||||
@Json(name = "supported_mime_types") val supportedMimeTypes: List<String> = emptyList(),
|
||||
@Json(name = "image_size_limit") val imageSizeLimit: Int? = null,
|
||||
@Json(name = "image_matrix_limit") val imageMatrixLimit: Int? = null,
|
||||
@Json(name = "video_size_limit") val videoSizeLimit: Int? = null,
|
||||
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int? = null,
|
||||
@Json(name = "video_matrix_limit") val videoMatrixLimit: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaConfiguration(
|
||||
val metadata: PleromaMetadata?
|
||||
val metadata: PleromaMetadata? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaMetadata(
|
||||
@SerializedName("fields_limits") val fieldLimits: PleromaFieldLimits
|
||||
@Json(name = "fields_limits") val fieldLimits: PleromaFieldLimits
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaFieldLimits(
|
||||
@SerializedName("max_fields") val maxFields: Int?,
|
||||
@SerializedName("name_length") val nameLength: Int?,
|
||||
@SerializedName("value_length") val valueLength: Int?
|
||||
@Json(name = "max_fields") val maxFields: Int? = null,
|
||||
@Json(name = "name_length") val nameLength: Int? = null,
|
||||
@Json(name = "value_length") val valueLength: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceRules(
|
||||
val id: String,
|
||||
val text: String
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* API type for saving the scroll position of a timeline.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Marker(
|
||||
@SerializedName("last_read_id")
|
||||
@Json(name = "last_read_id")
|
||||
val lastReadId: String,
|
||||
val version: Int,
|
||||
@SerializedName("updated_at")
|
||||
@Json(name = "updated_at")
|
||||
val updatedAt: Date
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,17 +16,18 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Created by charlag on 1/4/18.
|
||||
*/
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MastoList(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val exclusive: Boolean?,
|
||||
@SerializedName("replies_policy") val repliesPolicy: String?
|
||||
val exclusive: Boolean? = null,
|
||||
@Json(name = "replies_policy") val repliesPolicy: String? = null
|
||||
) {
|
||||
enum class ReplyPolicy(val policy: String) {
|
||||
NONE("none"),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* The same as Attachment, except the url is null - see https://docs.joinmastodon.org/methods/statuses/media/
|
||||
* We are only interested in the id, so other attributes are omitted
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaUploadResult(
|
||||
val id: String
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,35 +16,39 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewStatus(
|
||||
val status: String,
|
||||
@SerializedName("spoiler_text") val warningText: String,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@Json(name = "spoiler_text") val warningText: String,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String? = null,
|
||||
val visibility: String,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("media_ids") val mediaIds: List<String>?,
|
||||
@SerializedName("media_attributes") val mediaAttributes: List<MediaAttribute>?,
|
||||
@SerializedName("scheduled_at") val scheduledAt: String?,
|
||||
val poll: NewPoll?,
|
||||
val language: String?
|
||||
@Json(name = "media_ids") val mediaIds: List<String> = emptyList(),
|
||||
@Json(name = "media_attributes") val mediaAttributes: List<MediaAttribute> = emptyList(),
|
||||
@Json(name = "scheduled_at") val scheduledAt: String? = null,
|
||||
val poll: NewPoll? = null,
|
||||
val language: String? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class NewPoll(
|
||||
val options: List<String>,
|
||||
@SerializedName("expires_in") val expiresIn: Int,
|
||||
@Json(name = "expires_in") val expiresIn: Int,
|
||||
val multiple: Boolean
|
||||
) : Parcelable
|
||||
|
||||
// It would be nice if we could reuse MediaToSend,
|
||||
// but the server requires a different format for focus
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class MediaAttribute(
|
||||
val id: String,
|
||||
val description: String?,
|
||||
val focus: String?,
|
||||
val thumbnail: String?
|
||||
val description: String? = null,
|
||||
val focus: String? = null,
|
||||
val thumbnail: String? = null
|
||||
) : Parcelable
|
||||
|
|
|
|||
|
|
@ -16,65 +16,68 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Notification(
|
||||
val type: Type,
|
||||
val id: String,
|
||||
val account: TimelineAccount,
|
||||
val status: Status?,
|
||||
val report: Report?
|
||||
val status: Status? = null,
|
||||
val report: Report? = null
|
||||
) {
|
||||
|
||||
/** From https://docs.joinmastodon.org/entities/Notification/#type */
|
||||
@JsonAdapter(NotificationTypeAdapter::class)
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Type(val presentation: String, @StringRes val uiString: Int) {
|
||||
UNKNOWN("unknown", R.string.notification_unknown_name),
|
||||
|
||||
/** Someone mentioned you */
|
||||
@Json(name = "mention")
|
||||
MENTION("mention", R.string.notification_mention_name),
|
||||
|
||||
/** Someone boosted one of your statuses */
|
||||
@Json(name = "reblog")
|
||||
REBLOG("reblog", R.string.notification_boost_name),
|
||||
|
||||
/** Someone favourited one of your statuses */
|
||||
@Json(name = "favourite")
|
||||
FAVOURITE("favourite", R.string.notification_favourite_name),
|
||||
|
||||
/** Someone followed you */
|
||||
@Json(name = "follow")
|
||||
FOLLOW("follow", R.string.notification_follow_name),
|
||||
|
||||
/** Someone requested to follow you */
|
||||
@Json(name = "follow_request")
|
||||
FOLLOW_REQUEST("follow_request", R.string.notification_follow_request_name),
|
||||
|
||||
/** A poll you have voted in or created has ended */
|
||||
@Json(name = "poll")
|
||||
POLL("poll", R.string.notification_poll_name),
|
||||
|
||||
/** Someone you enabled notifications for has posted a status */
|
||||
@Json(name = "status")
|
||||
STATUS("status", R.string.notification_subscription_name),
|
||||
|
||||
/** Someone signed up (optionally sent to admins) */
|
||||
@Json(name = "admin.sign_up")
|
||||
SIGN_UP("admin.sign_up", R.string.notification_sign_up_name),
|
||||
|
||||
/** A status you interacted with has been updated */
|
||||
@Json(name = "update")
|
||||
UPDATE("update", R.string.notification_update_name),
|
||||
|
||||
/** A new report has been filed */
|
||||
@Json(name = "admin.report")
|
||||
REPORT("admin.report", R.string.notification_report_name);
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun byString(s: String): Type {
|
||||
entries.forEach {
|
||||
if (s == it.presentation) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return UNKNOWN
|
||||
return entries.firstOrNull { it.presentation == s } ?: UNKNOWN
|
||||
}
|
||||
|
||||
/** Notification types for UI display (omits UNKNOWN) */
|
||||
|
|
@ -95,20 +98,7 @@ data class Notification(
|
|||
if (other !is Notification) {
|
||||
return false
|
||||
}
|
||||
val notification = other as Notification?
|
||||
return notification?.id == this.id
|
||||
}
|
||||
|
||||
class NotificationTypeAdapter : JsonDeserializer<Type> {
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: java.lang.reflect.Type,
|
||||
context: JsonDeserializationContext
|
||||
): Type {
|
||||
return Type.byString(json.asString)
|
||||
}
|
||||
return other.id == this.id
|
||||
}
|
||||
|
||||
/** Helper for Java */
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NotificationSubscribeResult(
|
||||
val id: Int,
|
||||
val endpoint: String,
|
||||
@SerializedName("server_key") val serverKey: String
|
||||
@Json(name = "server_key") val serverKey: String
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Poll(
|
||||
val id: String,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@Json(name = "expires_at") val expiresAt: Date? = null,
|
||||
val expired: Boolean,
|
||||
val multiple: Boolean,
|
||||
@SerializedName("votes_count") val votesCount: Int,
|
||||
@Json(name = "votes_count") val votesCount: Int,
|
||||
// nullable for compatibility with Pleroma
|
||||
@SerializedName("voters_count") val votersCount: Int?,
|
||||
@Json(name = "voters_count") val votersCount: Int? = null,
|
||||
val options: List<PollOption>,
|
||||
val voted: Boolean,
|
||||
@SerializedName("own_votes") val ownVotes: List<Int>?
|
||||
val voted: Boolean = false,
|
||||
@Json(name = "own_votes") val ownVotes: List<Int> = emptyList()
|
||||
) {
|
||||
|
||||
fun votedCopy(choices: List<Int>): Poll {
|
||||
val newOptions = options.mapIndexed { index, option ->
|
||||
if (choices.contains(index)) {
|
||||
option.copy(votesCount = option.votesCount + 1)
|
||||
option.copy(votesCount = (option.votesCount ?: 0) + 1)
|
||||
} else {
|
||||
option
|
||||
}
|
||||
|
|
@ -42,7 +44,8 @@ data class Poll(
|
|||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollOption(
|
||||
val title: String,
|
||||
@SerializedName("votes_count") val votesCount: Int
|
||||
@Json(name = "votes_count") val votesCount: Int? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,27 +15,28 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.json.GuardedBooleanAdapter
|
||||
import com.keylesspalace.tusky.json.Guarded
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Relationship(
|
||||
val id: String,
|
||||
val following: Boolean,
|
||||
@SerializedName("followed_by") val followedBy: Boolean,
|
||||
@Json(name = "followed_by") val followedBy: Boolean,
|
||||
val blocking: Boolean,
|
||||
val muting: Boolean,
|
||||
@SerializedName("muting_notifications") val mutingNotifications: Boolean,
|
||||
@Json(name = "muting_notifications") val mutingNotifications: Boolean,
|
||||
val requested: Boolean,
|
||||
@SerializedName("showing_reblogs") val showingReblogs: Boolean,
|
||||
@Json(name = "showing_reblogs") val showingReblogs: Boolean,
|
||||
/* Pleroma extension, same as 'notifying' on Mastodon.
|
||||
* Some instances like qoto.org have a custom subscription feature where 'subscribing' is a json object,
|
||||
* so we use the custom GuardedBooleanAdapter to ignore the field if it is not a boolean.
|
||||
* so we use GuardedAdapter to ignore the field if it is not a boolean.
|
||||
*/
|
||||
@JsonAdapter(GuardedBooleanAdapter::class) val subscribing: Boolean? = null,
|
||||
@SerializedName("domain_blocking") val blockingDomain: Boolean,
|
||||
@Guarded val subscribing: Boolean? = null,
|
||||
@Json(name = "domain_blocking") val blockingDomain: Boolean,
|
||||
// nullable for backward compatibility / feature detection
|
||||
val note: String?,
|
||||
val note: String? = null,
|
||||
// since 3.3.0rc
|
||||
val notifying: Boolean?
|
||||
val notifying: Boolean? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Report(
|
||||
val id: String,
|
||||
val category: String,
|
||||
val status_ids: List<String>?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@SerializedName("target_account") val targetAccount: TimelineAccount
|
||||
@Json(name = "status_ids") val statusIds: List<String>? = null,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
@Json(name = "target_account") val targetAccount: TimelineAccount
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ScheduledStatus(
|
||||
val id: String,
|
||||
@SerializedName("scheduled_at") val scheduledAt: String,
|
||||
@Json(name = "scheduled_at") val scheduledAt: String,
|
||||
val params: StatusParams,
|
||||
@SerializedName("media_attachments") val mediaAttachments: ArrayList<Attachment>
|
||||
@Json(name = "media_attachments") val mediaAttachments: List<Attachment>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SearchResult(
|
||||
val accounts: List<TimelineAccount>,
|
||||
val statuses: List<Status>,
|
||||
|
|
|
|||
|
|
@ -17,44 +17,47 @@ package com.keylesspalace.tusky.entity
|
|||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.URLSpan
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Status(
|
||||
val id: String,
|
||||
// not present if it's reblog
|
||||
val url: String?,
|
||||
val url: String? = null,
|
||||
val account: TimelineAccount,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||
val reblog: Status?,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String? = null,
|
||||
@Json(name = "in_reply_to_account_id") val inReplyToAccountId: String? = null,
|
||||
val reblog: Status? = null,
|
||||
val content: String,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@SerializedName("edited_at") val editedAt: Date?,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
@Json(name = "edited_at") val editedAt: Date? = null,
|
||||
val emojis: List<Emoji>,
|
||||
@SerializedName("reblogs_count") val reblogsCount: Int,
|
||||
@SerializedName("favourites_count") val favouritesCount: Int,
|
||||
@SerializedName("replies_count") val repliesCount: Int,
|
||||
val reblogged: Boolean,
|
||||
val favourited: Boolean,
|
||||
val bookmarked: Boolean,
|
||||
@Json(name = "reblogs_count") val reblogsCount: Int,
|
||||
@Json(name = "favourites_count") val favouritesCount: Int,
|
||||
@Json(name = "replies_count") val repliesCount: Int,
|
||||
val reblogged: Boolean = false,
|
||||
val favourited: Boolean = false,
|
||||
val bookmarked: Boolean = false,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val visibility: Visibility,
|
||||
@SerializedName("media_attachments") val attachments: List<Attachment>,
|
||||
@Json(name = "media_attachments") val attachments: List<Attachment>,
|
||||
val mentions: List<Mention>,
|
||||
val tags: List<HashTag>?,
|
||||
val application: Application?,
|
||||
val pinned: Boolean?,
|
||||
val muted: Boolean?,
|
||||
val poll: Poll?,
|
||||
// Use null to mark the absence of tags because of semantic differences in LinkHelper
|
||||
val tags: List<HashTag>? = null,
|
||||
val application: Application? = null,
|
||||
val pinned: Boolean = false,
|
||||
val muted: Boolean = false,
|
||||
val poll: Poll? = null,
|
||||
/** Preview card for links included within status content. */
|
||||
val card: Card?,
|
||||
val card: Card? = null,
|
||||
/** ISO 639 language code for this status. */
|
||||
val language: String?,
|
||||
val language: String? = null,
|
||||
/** If the current token has an authorized user: The filter and keywords that matched this status. */
|
||||
val filtered: List<FilterResult>?
|
||||
val filtered: List<FilterResult> = emptyList()
|
||||
) {
|
||||
|
||||
val actionableId: String
|
||||
|
|
@ -70,30 +73,30 @@ data class Status(
|
|||
fun copyWithPoll(poll: Poll?): Status = copy(poll = poll)
|
||||
fun copyWithPinned(pinned: Boolean): Status = copy(pinned = pinned)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Visibility(val num: Int) {
|
||||
UNKNOWN(0),
|
||||
|
||||
@SerializedName("public")
|
||||
@Json(name = "public")
|
||||
PUBLIC(1),
|
||||
|
||||
@SerializedName("unlisted")
|
||||
@Json(name = "unlisted")
|
||||
UNLISTED(2),
|
||||
|
||||
@SerializedName("private")
|
||||
@Json(name = "private")
|
||||
PRIVATE(3),
|
||||
|
||||
@SerializedName("direct")
|
||||
@Json(name = "direct")
|
||||
DIRECT(4);
|
||||
|
||||
fun serverString(): String {
|
||||
return when (this) {
|
||||
val serverString: String
|
||||
get() = when (this) {
|
||||
PUBLIC -> "public"
|
||||
UNLISTED -> "unlisted"
|
||||
PRIVATE -> "private"
|
||||
DIRECT -> "direct"
|
||||
UNKNOWN -> "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
|
|
@ -123,13 +126,10 @@ data class Status(
|
|||
}
|
||||
}
|
||||
|
||||
fun rebloggingAllowed(): Boolean {
|
||||
return (visibility != Visibility.DIRECT && visibility != Visibility.UNKNOWN)
|
||||
}
|
||||
|
||||
fun isPinned(): Boolean {
|
||||
return pinned ?: false
|
||||
}
|
||||
val isRebloggingAllowed: Boolean
|
||||
get() {
|
||||
return (visibility != Visibility.DIRECT && visibility != Visibility.UNKNOWN)
|
||||
}
|
||||
|
||||
fun toDeletedStatus(): DeletedStatus {
|
||||
return DeletedStatus(
|
||||
|
|
@ -164,16 +164,18 @@ data class Status(
|
|||
return builder.toString()
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Mention(
|
||||
val id: String,
|
||||
val url: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@SerializedName("username") val localUsername: String
|
||||
@Json(name = "acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Application(
|
||||
val name: String,
|
||||
val website: String?
|
||||
val website: String? = null
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusContext(
|
||||
val ancestors: List<Status>,
|
||||
val descendants: List<Status>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusEdit(
|
||||
val content: String,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
val account: TimelineAccount,
|
||||
val poll: Poll?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: List<Attachment>,
|
||||
val poll: Poll? = null,
|
||||
@Json(name = "media_attachments") val mediaAttachments: List<Attachment>,
|
||||
val emojis: List<Emoji>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusParams(
|
||||
val text: String,
|
||||
val sensitive: Boolean,
|
||||
val sensitive: Boolean? = null,
|
||||
val visibility: Status.Visibility,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?
|
||||
@Json(name = "spoiler_text") val spoilerText: String? = null,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusSource(
|
||||
val id: String,
|
||||
val text: String,
|
||||
@SerializedName("spoiler_text") val spoilerText: String
|
||||
@Json(name = "spoiler_text") val spoilerText: String
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,24 +15,26 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Same as [Account], but only with the attributes required in timelines.
|
||||
* Prefer this class over [Account] because it uses way less memory & deserializes faster from json.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TimelineAccount(
|
||||
val id: String,
|
||||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String,
|
||||
@Json(name = "acct") val username: String,
|
||||
// should never be null per Api definition, but some servers break the contract
|
||||
@SerializedName("display_name") val displayName: String?,
|
||||
@Json(name = "display_name") val displayName: String? = null,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
val note: String,
|
||||
val bot: Boolean = false,
|
||||
// nullable for backward compatibility
|
||||
val emojis: List<Emoji>? = emptyList()
|
||||
// optional for backward compatibility
|
||||
val emojis: List<Emoji> = emptyList()
|
||||
) {
|
||||
|
||||
val name: String
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaTranslation(
|
||||
val id: String,
|
||||
val description: String,
|
||||
|
|
@ -12,22 +14,25 @@ data class MediaTranslation(
|
|||
*
|
||||
* See [doc](https://docs.joinmastodon.org/entities/Translation/).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Translation(
|
||||
val content: String,
|
||||
@SerializedName("spoiler_text")
|
||||
val spoilerText: String?,
|
||||
val poll: TranslatedPoll?,
|
||||
@SerializedName("media_attachments")
|
||||
@Json(name = "spoiler_text")
|
||||
val spoilerText: String? = null,
|
||||
val poll: TranslatedPoll? = null,
|
||||
@Json(name = "media_attachments")
|
||||
val mediaAttachments: List<MediaTranslation>,
|
||||
@SerializedName("detected_source_language")
|
||||
@Json(name = "detected_source_language")
|
||||
val detectedSourceLanguage: String,
|
||||
val provider: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TranslatedPoll(
|
||||
val options: List<TranslatedPollOption>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TranslatedPollOption(
|
||||
val title: String
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
|
|
@ -25,6 +26,7 @@ import java.util.Date
|
|||
* @param history A list of [TrendingTagHistory]. Each element contains metrics per day for this hashtag.
|
||||
* (@param following This is not listed in the APIs at the time of writing, but an instance is delivering it.)
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TrendingTag(
|
||||
val name: String,
|
||||
val history: List<TrendingTagHistory>
|
||||
|
|
@ -37,11 +39,14 @@ data class TrendingTag(
|
|||
* @param accounts The number of accounts that have posted with this hashtag.
|
||||
* @param uses The number of posts with this hashtag.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TrendingTagHistory(
|
||||
val day: String,
|
||||
val accounts: String,
|
||||
val uses: String
|
||||
)
|
||||
|
||||
fun TrendingTag.start() = Date(history.last().day.toLong() * 1000L)
|
||||
fun TrendingTag.end() = Date(history.first().day.toLong() * 1000L)
|
||||
val TrendingTag.start
|
||||
get() = Date(history.last().day.toLong() * 1000L)
|
||||
val TrendingTag.end
|
||||
get() = Date(history.first().day.toLong() * 1000L)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue