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
|
@ -142,7 +142,8 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.android.material
|
implementation libs.android.material
|
||||||
|
|
||||||
implementation libs.gson
|
implementation libs.bundles.moshi
|
||||||
|
ksp libs.moshi.kotlin.codegen
|
||||||
|
|
||||||
implementation libs.bundles.retrofit
|
implementation libs.bundles.retrofit
|
||||||
implementation libs.networkresult.calladapter
|
implementation libs.networkresult.calladapter
|
||||||
|
|
|
@ -39,34 +39,6 @@
|
||||||
|
|
||||||
# TUSKY SPECIFIC OPTIONS
|
# TUSKY SPECIFIC OPTIONS
|
||||||
|
|
||||||
# keep members of our model classes, they are used in json de/serialization
|
|
||||||
-keepclassmembers class com.keylesspalace.tusky.entity.* { *; }
|
|
||||||
|
|
||||||
-keep public enum com.keylesspalace.tusky.entity.*$** {
|
|
||||||
**[] $VALUES;
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class com.keylesspalace.tusky.components.conversation.ConversationAccountEntity { *; }
|
|
||||||
-keepclassmembers class com.keylesspalace.tusky.db.DraftAttachment { *; }
|
|
||||||
|
|
||||||
-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type {
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# preserve line numbers for crash reporting
|
# preserve line numbers for crash reporting
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
-renamesourcefileattribute SourceFile
|
-renamesourcefileattribute SourceFile
|
||||||
|
|
|
@ -19,7 +19,7 @@ class MigrationsTest {
|
||||||
@Rule
|
@Rule
|
||||||
var helper: MigrationTestHelper = MigrationTestHelper(
|
var helper: MigrationTestHelper = MigrationTestHelper(
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
AppDatabase::class.java.canonicalName,
|
AppDatabase::class.java.canonicalName!!,
|
||||||
FrameworkSQLiteOpenHelperFactory()
|
FrameworkSQLiteOpenHelperFactory()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ class EmojiAdapter(
|
||||||
private val animate: Boolean
|
private val animate: Boolean
|
||||||
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
|
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
|
||||||
|
|
||||||
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
|
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker }
|
||||||
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
|
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
|
||||||
|
|
||||||
override fun getItemCount() = emojiList.size
|
override fun getItemCount() = emojiList.size
|
||||||
|
|
|
@ -28,7 +28,6 @@ import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
|
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
import com.keylesspalace.tusky.util.unicodeWrap
|
import com.keylesspalace.tusky.util.unicodeWrap
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class ReportNotificationViewHolder(
|
class ReportNotificationViewHolder(
|
||||||
private val binding: ItemReportNotificationBinding
|
private val binding: ItemReportNotificationBinding
|
||||||
|
@ -54,7 +53,7 @@ class ReportNotificationViewHolder(
|
||||||
|
|
||||||
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
|
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
|
||||||
binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName)
|
binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName)
|
||||||
binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time), report.status_ids?.size ?: 0)
|
binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, getRelativeTimeSpanString(itemView.context, report.createdAt.time, System.currentTimeMillis()), report.statusIds?.size ?: 0)
|
||||||
binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
|
binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
|
||||||
|
|
||||||
// Fancy avatar inset
|
// Fancy avatar inset
|
||||||
|
|
|
@ -815,7 +815,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
setTranslationStatus(status, listener);
|
setTranslationStatus(status, listener);
|
||||||
|
|
||||||
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
setRebloggingEnabled(actionable.isRebloggingAllowed(), actionable.getVisibility());
|
||||||
|
|
||||||
setSpoilerAndContent(status, statusDisplayOptions, listener);
|
setSpoilerAndContent(status, statusDisplayOptions, listener);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.keylesspalace.tusky.appstore
|
package com.keylesspalace.tusky.appstore
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -13,8 +12,7 @@ import kotlinx.coroutines.launch
|
||||||
class CacheUpdater @Inject constructor(
|
class CacheUpdater @Inject constructor(
|
||||||
eventHub: EventHub,
|
eventHub: EventHub,
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
appDatabase: AppDatabase,
|
appDatabase: AppDatabase
|
||||||
gson: Gson
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
@ -30,8 +28,7 @@ class CacheUpdater @Inject constructor(
|
||||||
val status = event.status
|
val status = event.status
|
||||||
timelineDao.update(
|
timelineDao.update(
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
status = status,
|
status = status
|
||||||
gson = gson
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is UnfollowEvent ->
|
is UnfollowEvent ->
|
||||||
|
@ -39,8 +36,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)
|
timelineDao.setVoted(accountId, event.statusId, event.poll)
|
||||||
timelineDao.setVoted(accountId, event.statusId, pollString)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -527,8 +527,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
)
|
)
|
||||||
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||||
|
|
||||||
accountFieldAdapter.fields = account.fields.orEmpty()
|
accountFieldAdapter.fields = account.fields
|
||||||
accountFieldAdapter.emojis = account.emojis.orEmpty()
|
accountFieldAdapter.emojis = account.emojis
|
||||||
accountFieldAdapter.notifyDataSetChanged()
|
accountFieldAdapter.notifyDataSetChanged()
|
||||||
|
|
||||||
binding.accountLockedImageView.visible(account.locked)
|
binding.accountLockedImageView.visible(account.locked)
|
||||||
|
@ -669,7 +669,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
*/
|
*/
|
||||||
private fun updateRemoteAccount() {
|
private fun updateRemoteAccount() {
|
||||||
loadedAccount?.let { account ->
|
loadedAccount?.let { account ->
|
||||||
if (account.isRemote()) {
|
if (account.isRemote) {
|
||||||
binding.accountRemoveView.show()
|
binding.accountRemoveView.show()
|
||||||
binding.accountRemoveView.setOnClickListener {
|
binding.accountRemoveView.setOnClickListener {
|
||||||
openLink(account.url)
|
openLink(account.url)
|
||||||
|
@ -1097,7 +1097,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFullUsername(account: Account): String {
|
private fun getFullUsername(account: Account): String {
|
||||||
return if (account.isRemote()) {
|
return if (account.isRemote) {
|
||||||
"@" + account.username
|
"@" + account.username
|
||||||
} else {
|
} else {
|
||||||
val localUsername = account.localUsername
|
val localUsername = account.localUsername
|
||||||
|
|
|
@ -386,7 +386,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
val tootToSend = StatusToSend(
|
val tootToSend = StatusToSend(
|
||||||
text = content,
|
text = content,
|
||||||
warningText = spoilerText,
|
warningText = spoilerText,
|
||||||
visibility = _statusVisibility.value.serverString(),
|
visibility = _statusVisibility.value.serverString,
|
||||||
sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value),
|
sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value),
|
||||||
media = attachedMedia,
|
media = attachedMedia,
|
||||||
scheduledAt = _scheduledAt.value,
|
scheduledAt = _scheduledAt.value,
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Entity(primaryKeys = ["id", "accountId"])
|
@Entity(primaryKeys = ["id", "accountId"])
|
||||||
|
@ -50,6 +51,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,
|
||||||
|
@ -131,7 +133,7 @@ data class ConversationStatusEntity(
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = null,
|
card = null,
|
||||||
language = language,
|
language = language,
|
||||||
filtered = null
|
filtered = emptyList()
|
||||||
),
|
),
|
||||||
isExpanded = expanded,
|
isExpanded = expanded,
|
||||||
isShowingContent = showingHiddenContent,
|
isShowingContent = showingHiddenContent,
|
||||||
|
@ -172,7 +174,7 @@ fun Status.toEntity(expanded: Boolean, contentShowing: Boolean, contentCollapsed
|
||||||
showingHiddenContent = contentShowing,
|
showingHiddenContent = contentShowing,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
collapsed = contentCollapsed,
|
collapsed = contentCollapsed,
|
||||||
muted = muted ?: false,
|
muted = muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
language = language
|
language = language
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,7 +29,7 @@ data class ConversationViewData(
|
||||||
accountId: Long,
|
accountId: Long,
|
||||||
favourited: Boolean = lastStatus.status.favourited,
|
favourited: Boolean = lastStatus.status.favourited,
|
||||||
bookmarked: Boolean = lastStatus.status.bookmarked,
|
bookmarked: Boolean = lastStatus.status.bookmarked,
|
||||||
muted: Boolean = lastStatus.status.muted ?: false,
|
muted: Boolean = lastStatus.status.muted,
|
||||||
poll: Poll? = lastStatus.status.poll,
|
poll: Poll? = lastStatus.status.poll,
|
||||||
expanded: Boolean = lastStatus.isExpanded,
|
expanded: Boolean = lastStatus.isExpanded,
|
||||||
collapsed: Boolean = lastStatus.isCollapsed,
|
collapsed: Boolean = lastStatus.isCollapsed,
|
||||||
|
@ -57,7 +57,7 @@ data class ConversationViewData(
|
||||||
fun StatusViewData.Concrete.toConversationStatusEntity(
|
fun StatusViewData.Concrete.toConversationStatusEntity(
|
||||||
favourited: Boolean = status.favourited,
|
favourited: Boolean = status.favourited,
|
||||||
bookmarked: Boolean = status.bookmarked,
|
bookmarked: Boolean = status.bookmarked,
|
||||||
muted: Boolean = status.muted ?: false,
|
muted: Boolean = status.muted,
|
||||||
poll: Poll? = status.poll,
|
poll: Poll? = status.poll,
|
||||||
expanded: Boolean = isExpanded,
|
expanded: Boolean = isExpanded,
|
||||||
collapsed: Boolean = isCollapsed,
|
collapsed: Boolean = isCollapsed,
|
||||||
|
|
|
@ -300,7 +300,7 @@ class ConversationsFragment :
|
||||||
val popup = PopupMenu(requireContext(), view)
|
val popup = PopupMenu(requireContext(), view)
|
||||||
popup.inflate(R.menu.conversation_more)
|
popup.inflate(R.menu.conversation_more)
|
||||||
|
|
||||||
if (conversation.lastStatus.status.muted == true) {
|
if (conversation.lastStatus.status.muted) {
|
||||||
popup.menu.removeItem(R.id.status_mute_conversation)
|
popup.menu.removeItem(R.id.status_mute_conversation)
|
||||||
} else {
|
} else {
|
||||||
popup.menu.removeItem(R.id.status_unmute_conversation)
|
popup.menu.removeItem(R.id.status_unmute_conversation)
|
||||||
|
|
|
@ -159,12 +159,12 @@ class ConversationsViewModel @Inject constructor(
|
||||||
try {
|
try {
|
||||||
timelineCases.muteConversation(
|
timelineCases.muteConversation(
|
||||||
conversation.lastStatus.id,
|
conversation.lastStatus.id,
|
||||||
!(conversation.lastStatus.status.muted ?: false)
|
!conversation.lastStatus.status.muted
|
||||||
)
|
)
|
||||||
|
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
muted = !(conversation.lastStatus.status.muted ?: false)
|
muted = !conversation.lastStatus.status.muted
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
database.conversationDao().insert(newConversation)
|
||||||
|
|
|
@ -101,16 +101,17 @@ class DraftHelper @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachments: MutableList<DraftAttachment> = mutableListOf()
|
val attachments: List<DraftAttachment> = buildList(mediaUris.size) {
|
||||||
for (i in mediaUris.indices) {
|
for (i in mediaUris.indices) {
|
||||||
attachments.add(
|
add(
|
||||||
DraftAttachment(
|
DraftAttachment(
|
||||||
uriString = uris[i].toString(),
|
uriString = uris[i].toString(),
|
||||||
description = mediaDescriptions[i],
|
description = mediaDescriptions[i],
|
||||||
focus = mediaFocus[i],
|
focus = mediaFocus[i],
|
||||||
type = types[i]
|
type = types[i]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val draft = DraftEntity(
|
val draft = DraftEntity(
|
||||||
|
|
|
@ -41,13 +41,13 @@ class LoginWebViewViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
api.getInstance().fold(
|
api.getInstance().fold(
|
||||||
{ instance ->
|
{ instance ->
|
||||||
_instanceRules.value = instance.rules.orEmpty().map { rule -> rule.text }
|
_instanceRules.value = instance.rules.map { rule -> rule.text }
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable ->
|
||||||
if (throwable.isHttpNotFound()) {
|
if (throwable.isHttpNotFound()) {
|
||||||
api.getInstanceV1(domain).fold(
|
api.getInstanceV1(domain).fold(
|
||||||
{ instance ->
|
{ instance ->
|
||||||
_instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty()
|
_instanceRules.value = instance.rules.map { rule -> rule.text }
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable ->
|
||||||
Log.w(
|
Log.w(
|
||||||
|
|
|
@ -840,7 +840,7 @@ public class NotificationHelper {
|
||||||
PollOption option = options.get(i);
|
PollOption option = options.get(i);
|
||||||
builder.append(buildDescription(option.getTitle(),
|
builder.append(buildDescription(option.getTitle(),
|
||||||
PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()),
|
PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()),
|
||||||
poll.getOwnVotes() != null && poll.getOwnVotes().contains(i),
|
poll.getOwnVotes().contains(i),
|
||||||
context));
|
context));
|
||||||
builder.append('\n');
|
builder.append('\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
key = PrefKeys.DEFAULT_POST_PRIVACY
|
key = PrefKeys.DEFAULT_POST_PRIVACY
|
||||||
setSummaryProvider { entry }
|
setSummaryProvider { entry }
|
||||||
val visibility = accountManager.activeAccount?.defaultPostPrivacy ?: Status.Visibility.PUBLIC
|
val visibility = accountManager.activeAccount?.defaultPostPrivacy ?: Status.Visibility.PUBLIC
|
||||||
value = visibility.serverString()
|
value = visibility.serverString
|
||||||
setIcon(getIconForVisibility(visibility))
|
setIcon(getIconForVisibility(visibility))
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
setIcon(
|
setIcon(
|
||||||
|
|
|
@ -284,7 +284,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> {
|
Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> {
|
||||||
val textId =
|
val textId =
|
||||||
getString(
|
getString(
|
||||||
if (status.isPinned()) R.string.unpin_action else R.string.pin_action
|
if (status.pinned) R.string.unpin_action else R.string.pin_action
|
||||||
)
|
)
|
||||||
menu.add(0, R.id.pin, 1, textId)
|
menu.add(0, R.id.pin, 1, textId)
|
||||||
}
|
}
|
||||||
|
@ -320,7 +320,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
}
|
}
|
||||||
if (mutable) {
|
if (mutable) {
|
||||||
muteConversationItem.setTitle(
|
muteConversationItem.setTitle(
|
||||||
if (status.muted == true) {
|
if (status.muted) {
|
||||||
R.string.action_unmute_conversation
|
R.string.action_unmute_conversation
|
||||||
} else {
|
} else {
|
||||||
R.string.action_mute_conversation
|
R.string.action_mute_conversation
|
||||||
|
@ -392,7 +392,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
|
|
||||||
R.id.status_mute_conversation -> {
|
R.id.status_mute_conversation -> {
|
||||||
searchAdapter.peek(position)?.let { foundStatus ->
|
searchAdapter.peek(position)?.let { foundStatus ->
|
||||||
viewModel.muteConversation(foundStatus, status.muted != true)
|
viewModel.muteConversation(foundStatus, !status.muted)
|
||||||
}
|
}
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
|
@ -438,7 +438,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.pin -> {
|
R.id.pin -> {
|
||||||
viewModel.pinAccount(status, !status.isPinned())
|
viewModel.pinAccount(status, !status.pinned)
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,7 +562,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
{ deletedStatus ->
|
{ deletedStatus ->
|
||||||
removeItem(position)
|
removeItem(position)
|
||||||
|
|
||||||
val redraftStatus = if (deletedStatus.isEmpty()) {
|
val redraftStatus = if (deletedStatus.isEmpty) {
|
||||||
status.toDeletedStatus()
|
status.toDeletedStatus()
|
||||||
} else {
|
} else {
|
||||||
deletedStatus
|
deletedStatus
|
||||||
|
|
|
@ -13,11 +13,11 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalStdlibApi::class)
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.timeline
|
package com.keylesspalace.tusky.components.timeline
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
|
@ -30,6 +30,8 @@ import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapter
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
private const val TAG = "TimelineTypeMappers"
|
private const val TAG = "TimelineTypeMappers"
|
||||||
|
@ -39,12 +41,7 @@ data class Placeholder(
|
||||||
val loading: Boolean
|
val loading: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
private val attachmentArrayListType = object : TypeToken<ArrayList<Attachment>>() {}.type
|
fun TimelineAccount.toEntity(accountId: Long, moshi: Moshi): TimelineAccountEntity {
|
||||||
private val emojisListType = object : TypeToken<List<Emoji>>() {}.type
|
|
||||||
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
|
||||||
private val tagListType = object : TypeToken<List<HashTag>>() {}.type
|
|
||||||
|
|
||||||
fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
|
||||||
return TimelineAccountEntity(
|
return TimelineAccountEntity(
|
||||||
serverId = id,
|
serverId = id,
|
||||||
timelineUserId = accountId,
|
timelineUserId = accountId,
|
||||||
|
@ -53,12 +50,12 @@ fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity
|
||||||
displayName = name,
|
displayName = name,
|
||||||
url = url,
|
url = url,
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
emojis = gson.toJson(emojis),
|
emojis = moshi.adapter<List<Emoji>>().toJson(emojis),
|
||||||
bot = bot
|
bot = bot
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
fun TimelineAccountEntity.toAccount(moshi: Moshi): TimelineAccount {
|
||||||
return TimelineAccount(
|
return TimelineAccount(
|
||||||
id = serverId,
|
id = serverId,
|
||||||
localUsername = localUsername,
|
localUsername = localUsername,
|
||||||
|
@ -68,7 +65,7 @@ fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
||||||
url = url,
|
url = url,
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
bot = bot,
|
bot = bot,
|
||||||
emojis = gson.fromJson(emojis, emojisListType)
|
emojis = moshi.adapter<List<Emoji>?>().fromJson(emojis).orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,13 +104,13 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
||||||
card = null,
|
card = null,
|
||||||
repliesCount = 0,
|
repliesCount = 0,
|
||||||
language = null,
|
language = null,
|
||||||
filtered = null
|
filtered = emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Status.toEntity(
|
fun Status.toEntity(
|
||||||
timelineUserId: Long,
|
timelineUserId: Long,
|
||||||
gson: Gson,
|
moshi: Moshi,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
contentShowing: Boolean,
|
contentShowing: Boolean,
|
||||||
contentCollapsed: Boolean
|
contentCollapsed: Boolean
|
||||||
|
@ -128,7 +125,7 @@ fun Status.toEntity(
|
||||||
content = actionableStatus.content,
|
content = actionableStatus.content,
|
||||||
createdAt = actionableStatus.createdAt.time,
|
createdAt = actionableStatus.createdAt.time,
|
||||||
editedAt = actionableStatus.editedAt?.time,
|
editedAt = actionableStatus.editedAt?.time,
|
||||||
emojis = actionableStatus.emojis.let(gson::toJson),
|
emojis = actionableStatus.emojis.let { moshi.adapter<List<Emoji>>().toJson(it) },
|
||||||
reblogsCount = actionableStatus.reblogsCount,
|
reblogsCount = actionableStatus.reblogsCount,
|
||||||
favouritesCount = actionableStatus.favouritesCount,
|
favouritesCount = actionableStatus.favouritesCount,
|
||||||
reblogged = actionableStatus.reblogged,
|
reblogged = actionableStatus.reblogged,
|
||||||
|
@ -137,44 +134,44 @@ fun Status.toEntity(
|
||||||
sensitive = actionableStatus.sensitive,
|
sensitive = actionableStatus.sensitive,
|
||||||
spoilerText = actionableStatus.spoilerText,
|
spoilerText = actionableStatus.spoilerText,
|
||||||
visibility = actionableStatus.visibility,
|
visibility = actionableStatus.visibility,
|
||||||
attachments = actionableStatus.attachments.let(gson::toJson),
|
attachments = actionableStatus.attachments.let { moshi.adapter<List<Attachment>>().toJson(it) },
|
||||||
mentions = actionableStatus.mentions.let(gson::toJson),
|
mentions = actionableStatus.mentions.let { moshi.adapter<List<Status.Mention>>().toJson(it) },
|
||||||
tags = actionableStatus.tags.let(gson::toJson),
|
tags = actionableStatus.tags.let { moshi.adapter<List<HashTag>?>().toJson(it) },
|
||||||
application = actionableStatus.application.let(gson::toJson),
|
application = actionableStatus.application.let { moshi.adapter<Status.Application?>().toJson(it) },
|
||||||
reblogServerId = reblog?.id,
|
reblogServerId = reblog?.id,
|
||||||
reblogAccountId = reblog?.let { this.account.id },
|
reblogAccountId = reblog?.let { this.account.id },
|
||||||
poll = actionableStatus.poll.let(gson::toJson),
|
poll = actionableStatus.poll.let { moshi.adapter<Poll?>().toJson(it) },
|
||||||
muted = actionableStatus.muted,
|
muted = actionableStatus.muted,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
contentShowing = contentShowing,
|
contentShowing = contentShowing,
|
||||||
contentCollapsed = contentCollapsed,
|
contentCollapsed = contentCollapsed,
|
||||||
pinned = actionableStatus.pinned == true,
|
pinned = actionableStatus.pinned,
|
||||||
card = actionableStatus.card?.let(gson::toJson),
|
card = actionableStatus.card?.let { moshi.adapter<Card>().toJson(it) },
|
||||||
repliesCount = actionableStatus.repliesCount,
|
repliesCount = actionableStatus.repliesCount,
|
||||||
language = actionableStatus.language,
|
language = actionableStatus.language,
|
||||||
filtered = actionableStatus.filtered
|
filtered = actionableStatus.filtered
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false, translation: TranslationViewData? = null): StatusViewData {
|
fun TimelineStatusWithAccount.toViewData(moshi: Moshi, isDetailed: Boolean = false, translation: TranslationViewData? = null): StatusViewData {
|
||||||
if (this.account == null) {
|
if (this.account == null) {
|
||||||
Log.d(TAG, "Constructing Placeholder(${this.status.serverId}, ${this.status.expanded})")
|
Log.d(TAG, "Constructing Placeholder(${this.status.serverId}, ${this.status.expanded})")
|
||||||
return StatusViewData.Placeholder(this.status.serverId, this.status.expanded)
|
return StatusViewData.Placeholder(this.status.serverId, this.status.expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachments: ArrayList<Attachment> = gson.fromJson(status.attachments, attachmentArrayListType) ?: arrayListOf()
|
val attachments: List<Attachment> = status.attachments?.let { moshi.adapter<List<Attachment>?>().fromJson(it) }.orEmpty()
|
||||||
val mentions: List<Status.Mention> = gson.fromJson(status.mentions, mentionListType) ?: emptyList()
|
val mentions: List<Status.Mention> = status.mentions?.let { moshi.adapter<List<Status.Mention>?>().fromJson(it) }.orEmpty()
|
||||||
val tags: List<HashTag>? = gson.fromJson(status.tags, tagListType)
|
val tags: List<HashTag>? = status.tags?.let { moshi.adapter<List<HashTag>?>().fromJson(it) }
|
||||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
val application = status.application?.let { moshi.adapter<Status.Application?>().fromJson(it) }
|
||||||
val emojis: List<Emoji> = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
|
val emojis: List<Emoji> = status.emojis?.let { moshi.adapter<List<Emoji>?>().fromJson(it) }.orEmpty()
|
||||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
val poll: Poll? = status.poll?.let { moshi.adapter<Poll?>().fromJson(it) }
|
||||||
val card: Card? = gson.fromJson(status.card, Card::class.java)
|
val card: Card? = status.card?.let { moshi.adapter<Card?>().fromJson(it) }
|
||||||
|
|
||||||
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.toAccount(gson),
|
account = account.toAccount(moshi),
|
||||||
inReplyToId = status.inReplyToId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
|
@ -195,12 +192,12 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
||||||
tags = tags,
|
tags = tags,
|
||||||
application = application,
|
application = application,
|
||||||
pinned = false,
|
pinned = false,
|
||||||
muted = status.muted,
|
muted = status.muted ?: false,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = card,
|
card = card,
|
||||||
repliesCount = status.repliesCount,
|
repliesCount = status.repliesCount,
|
||||||
language = status.language,
|
language = status.language,
|
||||||
filtered = status.filtered,
|
filtered = status.filtered.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val status = if (reblog != null) {
|
val status = if (reblog != null) {
|
||||||
|
@ -208,7 +205,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
||||||
id = status.serverId,
|
id = status.serverId,
|
||||||
// no url for reblogs
|
// no url for reblogs
|
||||||
url = null,
|
url = null,
|
||||||
account = this.reblogAccount!!.toAccount(gson),
|
account = this.reblogAccount!!.toAccount(moshi),
|
||||||
inReplyToId = null,
|
inReplyToId = null,
|
||||||
inReplyToAccountId = null,
|
inReplyToAccountId = null,
|
||||||
reblog = reblog,
|
reblog = reblog,
|
||||||
|
@ -216,7 +213,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
||||||
// lie but whatever?
|
// lie but whatever?
|
||||||
createdAt = Date(status.createdAt),
|
createdAt = Date(status.createdAt),
|
||||||
editedAt = null,
|
editedAt = null,
|
||||||
emojis = listOf(),
|
emojis = emptyList(),
|
||||||
reblogsCount = 0,
|
reblogsCount = 0,
|
||||||
favouritesCount = 0,
|
favouritesCount = 0,
|
||||||
reblogged = false,
|
reblogged = false,
|
||||||
|
@ -225,23 +222,23 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
||||||
sensitive = false,
|
sensitive = false,
|
||||||
spoilerText = "",
|
spoilerText = "",
|
||||||
visibility = status.visibility,
|
visibility = status.visibility,
|
||||||
attachments = ArrayList(),
|
attachments = emptyList(),
|
||||||
mentions = listOf(),
|
mentions = emptyList(),
|
||||||
tags = listOf(),
|
tags = emptyList(),
|
||||||
application = null,
|
application = null,
|
||||||
pinned = status.pinned,
|
pinned = status.pinned,
|
||||||
muted = status.muted,
|
muted = status.muted ?: false,
|
||||||
poll = null,
|
poll = null,
|
||||||
card = null,
|
card = null,
|
||||||
repliesCount = status.repliesCount,
|
repliesCount = status.repliesCount,
|
||||||
language = status.language,
|
language = status.language,
|
||||||
filtered = status.filtered
|
filtered = status.filtered.orEmpty()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Status(
|
Status(
|
||||||
id = status.serverId,
|
id = status.serverId,
|
||||||
url = status.url,
|
url = status.url,
|
||||||
account = account.toAccount(gson),
|
account = account.toAccount(moshi),
|
||||||
inReplyToId = status.inReplyToId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
|
@ -262,12 +259,12 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
||||||
tags = tags,
|
tags = tags,
|
||||||
application = application,
|
application = application,
|
||||||
pinned = status.pinned,
|
pinned = status.pinned,
|
||||||
muted = status.muted,
|
muted = status.muted ?: false,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = card,
|
card = card,
|
||||||
repliesCount = status.repliesCount,
|
repliesCount = status.repliesCount,
|
||||||
language = status.language,
|
language = status.language,
|
||||||
filtered = status.filtered
|
filtered = status.filtered.orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return StatusViewData.Concrete(
|
return StatusViewData.Concrete(
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package com.keylesspalace.tusky.components.timeline.util
|
package com.keylesspalace.tusky.components.timeline.util
|
||||||
|
|
||||||
import com.google.gson.JsonParseException
|
import com.squareup.moshi.JsonDataException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
fun Throwable.isExpected() =
|
fun Throwable.isExpected() =
|
||||||
this is IOException || this is HttpException || this is JsonParseException
|
this is IOException || this is HttpException || this is JsonDataException
|
||||||
|
|
||||||
inline fun <T> ifExpected(t: Throwable, cb: () -> T): T {
|
inline fun <T> ifExpected(t: Throwable, cb: () -> T): T {
|
||||||
if (t.isExpected()) {
|
if (t.isExpected()) {
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.paging.LoadType
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.RemoteMediator
|
import androidx.paging.RemoteMediator
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
|
@ -31,6 +30,7 @@ import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
@ -38,7 +38,7 @@ class CachedTimelineRemoteMediator(
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
private val api: MastodonApi,
|
private val api: MastodonApi,
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val gson: Gson
|
private val moshi: Moshi
|
||||||
) : RemoteMediator<Int, TimelineStatusWithAccount>() {
|
) : RemoteMediator<Int, TimelineStatusWithAccount>() {
|
||||||
|
|
||||||
private var initialRefresh = false
|
private var initialRefresh = false
|
||||||
|
@ -143,8 +143,8 @@ class CachedTimelineRemoteMediator(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (status in statuses) {
|
for (status in statuses) {
|
||||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, moshi))
|
||||||
status.reblog?.account?.toEntity(activeAccount.id, gson)?.let { rebloggedAccount ->
|
status.reblog?.account?.toEntity(activeAccount.id, moshi)?.let { rebloggedAccount ->
|
||||||
timelineDao.insertAccount(rebloggedAccount)
|
timelineDao.insertAccount(rebloggedAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ class CachedTimelineRemoteMediator(
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
status.toEntity(
|
status.toEntity(
|
||||||
timelineUserId = activeAccount.id,
|
timelineUserId = activeAccount.id,
|
||||||
gson = gson,
|
moshi = moshi,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
contentShowing = contentShowing,
|
contentShowing = contentShowing,
|
||||||
contentCollapsed = contentCollapsed
|
contentCollapsed = contentCollapsed
|
||||||
|
|
|
@ -29,7 +29,6 @@ import androidx.room.withTransaction
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
import at.connyduck.calladapter.networkresult.map
|
import at.connyduck.calladapter.networkresult.map
|
||||||
import at.connyduck.calladapter.networkresult.onFailure
|
import at.connyduck.calladapter.networkresult.onFailure
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.NEWEST_FIRST
|
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.NEWEST_FIRST
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.OLDEST_FIRST
|
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.OLDEST_FIRST
|
||||||
|
@ -49,6 +48,7 @@ import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.EmptyPagingSource
|
import com.keylesspalace.tusky.util.EmptyPagingSource
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
|
@ -69,7 +69,7 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
sharedPreferences: SharedPreferences,
|
sharedPreferences: SharedPreferences,
|
||||||
filterModel: FilterModel,
|
filterModel: FilterModel,
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val gson: Gson
|
private val moshi: Moshi
|
||||||
) : TimelineViewModel(
|
) : TimelineViewModel(
|
||||||
timelineCases,
|
timelineCases,
|
||||||
api,
|
api,
|
||||||
|
@ -87,7 +87,7 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
override val statuses = Pager(
|
override val statuses = Pager(
|
||||||
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
||||||
remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, gson),
|
remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, moshi),
|
||||||
pagingSourceFactory = {
|
pagingSourceFactory = {
|
||||||
val activeAccount = accountManager.activeAccount
|
val activeAccount = accountManager.activeAccount
|
||||||
if (activeAccount == null) {
|
if (activeAccount == null) {
|
||||||
|
@ -108,7 +108,7 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus ->
|
pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus ->
|
||||||
val translation = translations[timelineStatus.status.serverId]
|
val translation = translations[timelineStatus.status.serverId]
|
||||||
timelineStatus.toViewData(
|
timelineStatus.toViewData(
|
||||||
gson,
|
moshi,
|
||||||
isDetailed = false,
|
isDetailed = false,
|
||||||
translation = translation
|
translation = translation
|
||||||
)
|
)
|
||||||
|
@ -218,15 +218,15 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (status in statuses) {
|
for (status in statuses) {
|
||||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, moshi))
|
||||||
status.reblog?.account?.toEntity(activeAccount.id, gson)
|
status.reblog?.account?.toEntity(activeAccount.id, moshi)
|
||||||
?.let { rebloggedAccount ->
|
?.let { rebloggedAccount ->
|
||||||
timelineDao.insertAccount(rebloggedAccount)
|
timelineDao.insertAccount(rebloggedAccount)
|
||||||
}
|
}
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
status.toEntity(
|
status.toEntity(
|
||||||
timelineUserId = activeAccount.id,
|
timelineUserId = activeAccount.id,
|
||||||
gson = gson,
|
moshi = moshi,
|
||||||
expanded = activeAccount.alwaysOpenSpoiler,
|
expanded = activeAccount.alwaysOpenSpoiler,
|
||||||
contentShowing = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
contentShowing = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||||
contentCollapsed = true
|
contentCollapsed = true
|
||||||
|
|
|
@ -259,7 +259,7 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
override fun clearWarning(status: StatusViewData.Concrete) {
|
override fun clearWarning(status: StatusViewData.Concrete) {
|
||||||
updateActionableStatusById(status.id) {
|
updateActionableStatusById(status.id) {
|
||||||
it.copy(filtered = null)
|
it.copy(filtered = emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ class TrendingTagsViewModel @Inject constructor(
|
||||||
.sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } }
|
.sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } }
|
||||||
.toViewData()
|
.toViewData()
|
||||||
|
|
||||||
val header = TrendingViewData.Header(firstTag.start(), firstTag.end())
|
val header = TrendingViewData.Header(firstTag.start, firstTag.end)
|
||||||
TrendingTagsUiState(listOf(header) + tags, LoadingState.LOADED)
|
TrendingTagsUiState(listOf(header) + tags, LoadingState.LOADED)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,7 +24,6 @@ import at.connyduck.calladapter.networkresult.getOrElse
|
||||||
import at.connyduck.calladapter.networkresult.getOrThrow
|
import at.connyduck.calladapter.networkresult.getOrThrow
|
||||||
import at.connyduck.calladapter.networkresult.map
|
import at.connyduck.calladapter.networkresult.map
|
||||||
import at.connyduck.calladapter.networkresult.onFailure
|
import at.connyduck.calladapter.networkresult.onFailure
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusChangedEvent
|
import com.keylesspalace.tusky.appstore.StatusChangedEvent
|
||||||
|
@ -44,6 +43,7 @@ import com.keylesspalace.tusky.util.isHttpNotFound
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
@ -64,7 +64,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
eventHub: EventHub,
|
eventHub: EventHub,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val gson: Gson
|
private val moshi: Moshi
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ThreadUiState.Loading as ThreadUiState)
|
private val _uiState = MutableStateFlow(ThreadUiState.Loading as ThreadUiState)
|
||||||
|
@ -113,7 +113,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
var detailedStatus = if (timelineStatus != null) {
|
var detailedStatus = if (timelineStatus != null) {
|
||||||
Log.d(TAG, "Loaded status from local timeline")
|
Log.d(TAG, "Loaded status from local timeline")
|
||||||
val viewData = timelineStatus.toViewData(
|
val viewData = timelineStatus.toViewData(
|
||||||
gson,
|
moshi,
|
||||||
isDetailed = true,
|
isDetailed = true,
|
||||||
) as StatusViewData.Concrete
|
) as StatusViewData.Concrete
|
||||||
|
|
||||||
|
@ -148,8 +148,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
api.status(id).getOrNull()?.let { result ->
|
api.status(id).getOrNull()?.let { result ->
|
||||||
db.timelineDao().update(
|
db.timelineDao().update(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
status = result,
|
status = result
|
||||||
gson = gson
|
|
||||||
)
|
)
|
||||||
detailedStatus = result.toViewData(isDetailed = true)
|
detailedStatus = result.toViewData(isDetailed = true)
|
||||||
}
|
}
|
||||||
|
@ -520,7 +519,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
|
|
||||||
fun clearWarning(viewData: StatusViewData.Concrete) {
|
fun clearWarning(viewData: StatusViewData.Concrete) {
|
||||||
updateStatus(viewData.id) { status ->
|
updateStatus(viewData.id) { status ->
|
||||||
status.copy(filtered = null)
|
status.copy(filtered = emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,38 +17,40 @@ package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
import androidx.room.ProvidedTypeConverter
|
import androidx.room.ProvidedTypeConverter
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.keylesspalace.tusky.TabData
|
import com.keylesspalace.tusky.TabData
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
||||||
import com.keylesspalace.tusky.createTabDataFromId
|
import com.keylesspalace.tusky.createTabDataFromId
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.entity.Card
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.FilterResult
|
import com.keylesspalace.tusky.entity.FilterResult
|
||||||
import com.keylesspalace.tusky.entity.HashTag
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
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(emojiListJson: String?): List<Emoji> {
|
||||||
return gson.fromJson(emojiListJson, object : TypeToken<List<Emoji>>() {}.type)
|
return emojiListJson?.let { moshi.adapter<List<Emoji>?>().fromJson(it) }.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
|
@ -83,55 +85,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(
|
return accountListJson?.let { moshi.adapter<List<ConversationAccountEntity>?>().fromJson(it) }.orEmpty()
|
||||||
accountListJson,
|
|
||||||
object : TypeToken<List<ConversationAccountEntity>>() {}.type
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) }.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) }.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
|
@ -145,45 +144,47 @@ 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(
|
return draftAttachmentListJson?.let { moshi.adapter<List<DraftAttachment>?>().fromJson(it) }.orEmpty()
|
||||||
draftAttachmentListJson,
|
|
||||||
object : TypeToken<List<DraftAttachment>>() {}.type
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
|
fun cardToJson(card: Card?): String {
|
||||||
|
return moshi.adapter<Card?>().toJson(card)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,10 @@ import androidx.core.net.toUri
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -46,21 +46,18 @@ data class DraftEntity(
|
||||||
val statusId: String?
|
val statusId: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
@JsonClass(generateAdapter = true)
|
||||||
* 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
|
||||||
data class DraftAttachment(
|
data class DraftAttachment(
|
||||||
@SerializedName(value = "uriString", alternate = ["e", "i"]) val uriString: String,
|
val uriString: String,
|
||||||
@SerializedName(value = "description", alternate = ["f", "j"]) val description: String?,
|
val description: String?,
|
||||||
@SerializedName(value = "focus") val focus: Attachment.Focus?,
|
val focus: Attachment.Focus?,
|
||||||
@SerializedName(value = "type", alternate = ["g", "k"]) val type: Type
|
val type: Type
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
val uri: Uri
|
val uri: Uri
|
||||||
get() = uriString.toUri()
|
get() = uriString.toUri()
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = false)
|
||||||
enum class Type {
|
enum class Type {
|
||||||
IMAGE,
|
IMAGE,
|
||||||
VIDEO,
|
VIDEO,
|
||||||
|
|
|
@ -21,7 +21,11 @@ import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.google.gson.Gson
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.entity.Card
|
||||||
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
@ -89,13 +93,13 @@ AND
|
||||||
)
|
)
|
||||||
abstract suspend fun deleteRange(accountId: Long, minId: String, maxId: String): Int
|
abstract suspend fun deleteRange(accountId: Long, minId: String, maxId: String): Int
|
||||||
|
|
||||||
suspend fun update(accountId: Long, status: Status, gson: Gson) {
|
suspend fun update(accountId: Long, status: Status) {
|
||||||
update(
|
update(
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
statusId = status.id,
|
statusId = status.id,
|
||||||
content = status.content,
|
content = status.content,
|
||||||
editedAt = status.editedAt?.time,
|
editedAt = status.editedAt?.time,
|
||||||
emojis = gson.toJson(status.emojis),
|
emojis = status.emojis,
|
||||||
reblogsCount = status.reblogsCount,
|
reblogsCount = status.reblogsCount,
|
||||||
favouritesCount = status.favouritesCount,
|
favouritesCount = status.favouritesCount,
|
||||||
repliesCount = status.repliesCount,
|
repliesCount = status.repliesCount,
|
||||||
|
@ -105,13 +109,13 @@ AND
|
||||||
sensitive = status.sensitive,
|
sensitive = status.sensitive,
|
||||||
spoilerText = status.spoilerText,
|
spoilerText = status.spoilerText,
|
||||||
visibility = status.visibility,
|
visibility = status.visibility,
|
||||||
attachments = gson.toJson(status.attachments),
|
attachments = status.attachments,
|
||||||
mentions = gson.toJson(status.mentions),
|
mentions = status.mentions,
|
||||||
tags = gson.toJson(status.tags),
|
tags = status.tags,
|
||||||
poll = gson.toJson(status.poll),
|
poll = status.poll,
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
pinned = status.pinned ?: false,
|
pinned = status.pinned,
|
||||||
card = gson.toJson(status.card),
|
card = status.card,
|
||||||
language = status.language
|
language = status.language
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -141,12 +145,12 @@ AND
|
||||||
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
|
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract suspend fun update(
|
protected abstract suspend fun update(
|
||||||
accountId: Long,
|
accountId: Long,
|
||||||
statusId: String,
|
statusId: String,
|
||||||
content: String?,
|
content: String?,
|
||||||
editedAt: Long?,
|
editedAt: Long?,
|
||||||
emojis: String?,
|
emojis: List<Emoji>,
|
||||||
reblogsCount: Int,
|
reblogsCount: Int,
|
||||||
favouritesCount: Int,
|
favouritesCount: Int,
|
||||||
repliesCount: Int,
|
repliesCount: Int,
|
||||||
|
@ -156,13 +160,13 @@ AND
|
||||||
sensitive: Boolean,
|
sensitive: Boolean,
|
||||||
spoilerText: String,
|
spoilerText: String,
|
||||||
visibility: Status.Visibility,
|
visibility: Status.Visibility,
|
||||||
attachments: String?,
|
attachments: List<Attachment>,
|
||||||
mentions: String?,
|
mentions: List<Status.Mention>,
|
||||||
tags: String?,
|
tags: List<HashTag>?,
|
||||||
poll: String?,
|
poll: Poll?,
|
||||||
muted: Boolean?,
|
muted: Boolean?,
|
||||||
pinned: Boolean,
|
pinned: Boolean,
|
||||||
card: String?,
|
card: Card?,
|
||||||
language: String?
|
language: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -243,7 +247,8 @@ AND serverId = :statusId"""
|
||||||
"""UPDATE TimelineStatusEntity SET poll = :poll
|
"""UPDATE TimelineStatusEntity SET poll = :poll
|
||||||
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
|
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
|
||||||
)
|
)
|
||||||
abstract suspend fun setVoted(accountId: Long, statusId: String, poll: String)
|
@TypeConverters(Converters::class)
|
||||||
|
abstract suspend fun setVoted(accountId: Long, statusId: String, poll: Poll)
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""UPDATE TimelineStatusEntity SET expanded = :expanded
|
"""UPDATE TimelineStatusEntity SET expanded = :expanded
|
||||||
|
|
|
@ -20,11 +20,12 @@ import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.json.Rfc3339DateJsonAdapter
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.entity.Notification
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.json.GuardedAdapter
|
||||||
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.MediaUploadApi
|
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||||
|
@ -33,6 +34,9 @@ import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_PORT
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_SERVER
|
import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_SERVER
|
||||||
import com.keylesspalace.tusky.settings.ProxyConfiguration
|
import com.keylesspalace.tusky.settings.ProxyConfiguration
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapters.EnumJsonAdapter
|
||||||
|
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import java.net.IDN
|
import java.net.IDN
|
||||||
|
@ -46,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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,13 +58,32 @@ import retrofit2.create
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class NetworkModule {
|
object NetworkModule {
|
||||||
|
|
||||||
|
private const val TAG = "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(GuardedAdapter.ANNOTATION_FACTORY)
|
||||||
|
// Enum types with fallback value
|
||||||
|
.add(
|
||||||
|
Attachment.Type::class.java,
|
||||||
|
EnumJsonAdapter.create(Attachment.Type::class.java)
|
||||||
|
.withUnknownFallback(Attachment.Type.UNKNOWN)
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
Notification.Type::class.java,
|
||||||
|
EnumJsonAdapter.create(Notification.Type::class.java)
|
||||||
|
.withUnknownFallback(Notification.Type.UNKNOWN)
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
Status.Visibility::class.java,
|
||||||
|
EnumJsonAdapter.create(Status.Visibility::class.java)
|
||||||
|
.withUnknownFallback(Status.Visibility.UNKNOWN)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@ -113,10 +136,10 @@ class NetworkModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesRetrofit(httpClient: OkHttpClient, gson: Gson): Retrofit {
|
fun providesRetrofit(httpClient: OkHttpClient, moshi: Moshi): 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()
|
||||||
}
|
}
|
||||||
|
@ -138,8 +161,4 @@ class NetworkModule {
|
||||||
.build()
|
.build()
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "NetworkModule"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,10 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
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
|
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
|
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? = null,
|
||||||
@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
|
// default value for backward compatibility
|
||||||
val emojis: List<Emoji>? = emptyList(),
|
val emojis: List<Emoji> = emptyList(),
|
||||||
// nullable for backward compatibility
|
// default value for backward compatibility
|
||||||
val fields: List<Field>? = emptyList(),
|
val fields: List<Field> = emptyList(),
|
||||||
val moved: Account? = null,
|
val moved: Account? = null,
|
||||||
val roles: List<Role>? = emptyList()
|
val roles: List<Role> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val name: String
|
val name: String
|
||||||
|
@ -50,28 +52,33 @@ data class Account(
|
||||||
displayName
|
displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isRemote(): Boolean = this.username != this.localUsername
|
val isRemote: Boolean
|
||||||
|
get() = this.username != this.localUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class AccountSource(
|
data class AccountSource(
|
||||||
val privacy: Status.Visibility?,
|
val privacy: Status.Visibility = Status.Visibility.PUBLIC,
|
||||||
val sensitive: Boolean?,
|
val sensitive: Boolean? = null,
|
||||||
val note: String?,
|
val note: String? = null,
|
||||||
val fields: List<StringField>?,
|
val fields: List<StringField> = emptyList(),
|
||||||
val language: String?
|
val language: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@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? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class StringField(
|
data class StringField(
|
||||||
val name: String,
|
val name: String,
|
||||||
val value: String
|
val value: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Role(
|
data class Role(
|
||||||
val name: String,
|
val name: String,
|
||||||
val color: String
|
val color: String
|
||||||
|
|
|
@ -15,18 +15,20 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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
|
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? = null,
|
||||||
@SerializedName("ends_at") val endsAt: Date?,
|
@Json(name = "ends_at") val endsAt: Date? = null,
|
||||||
@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 = false,
|
||||||
val mentions: List<Status.Mention>,
|
val mentions: List<Status.Mention>,
|
||||||
val statuses: List<Status>,
|
val statuses: List<Status>,
|
||||||
val tags: List<HashTag>,
|
val tags: List<HashTag>,
|
||||||
|
@ -36,21 +38,21 @@ data class Announcement(
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
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 == other.id
|
||||||
return id == announcement?.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
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 = false,
|
||||||
val url: String?,
|
val url: String? = null,
|
||||||
@SerializedName("static_url") val staticUrl: String?
|
@Json(name = "static_url") val staticUrl: String? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,11 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
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
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,70 +16,50 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
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? = null,
|
||||||
val meta: MetaData?,
|
val meta: MetaData? = null,
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val description: String?,
|
val description: String? = null,
|
||||||
val blurhash: String?
|
val blurhash: String? = null
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@JsonAdapter(MediaTypeDeserializer::class)
|
@JsonClass(generateAdapter = false)
|
||||||
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")
|
|
||||||
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].
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class MetaData(
|
data class MetaData(
|
||||||
val focus: Focus?,
|
val focus: Focus? = null,
|
||||||
val duration: Float?,
|
val duration: Float? = null,
|
||||||
val original: Size?,
|
val original: Size? = null,
|
||||||
val small: Size?
|
val small: Size? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +68,7 @@ data class Attachment(
|
||||||
* See here for more details what the x and y mean:
|
* See here for more details what the x and y mean:
|
||||||
* https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
* https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Focus(
|
data class Focus(
|
||||||
val x: Float,
|
val x: Float,
|
||||||
|
@ -99,10 +80,11 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Size(
|
data class Size(
|
||||||
val width: Int,
|
val width: Int,
|
||||||
val height: Int,
|
val height: Int,
|
||||||
val aspect: Double
|
val aspect: Double = 0.0
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,19 +15,21 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
data class Card(
|
||||||
val url: String,
|
val url: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
@SerializedName("author_name") val authorName: String,
|
@Json(name = "author_name") val authorName: String,
|
||||||
val image: String,
|
val image: String? = null,
|
||||||
val type: String,
|
val type: String,
|
||||||
val width: Int,
|
val width: Int,
|
||||||
val height: Int,
|
val height: Int,
|
||||||
val blurhash: String?,
|
val blurhash: String? = null,
|
||||||
@SerializedName("embed_url") val embedUrl: String?
|
@Json(name = "embed_url") val embedUrl: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun hashCode() = url.hashCode()
|
override fun hashCode() = url.hashCode()
|
||||||
|
@ -36,8 +38,7 @@ data class Card(
|
||||||
if (other !is Card) {
|
if (other !is Card) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val account = other as Card?
|
return other.url == this.url
|
||||||
return account?.url == this.url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -15,12 +15,14 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
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 it's possible https://github.com/tuskyapp/Tusky/issues/1038
|
||||||
@SerializedName("last_status") val lastStatus: Status?,
|
@Json(name = "last_status") val lastStatus: Status? = null,
|
||||||
val unread: Boolean
|
val unread: Boolean
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,21 +15,22 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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
|
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? = null,
|
||||||
@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? = null,
|
||||||
@SerializedName("created_at") val createdAt: Date,
|
@Json(name = "created_at") val createdAt: Date,
|
||||||
val language: String?
|
val language: String? = null
|
||||||
) {
|
) {
|
||||||
fun isEmpty(): Boolean {
|
val isEmpty: Boolean
|
||||||
return text == null && attachments == null
|
get() = text == null && attachments.isEmpty()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,15 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
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)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
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 = true
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
@ -17,8 +17,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
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? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
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? = null,
|
||||||
@SerializedName("filter_action") private val filterAction: String,
|
@Json(name = "filter_action") val filterAction: String,
|
||||||
val keywords: List<FilterKeyword>
|
// 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>,
|
// val statuses: List<FilterStatus>,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
enum class Action(val action: String) {
|
enum class Action(val action: String) {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
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)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
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,10 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
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>? = null,
|
||||||
@SerializedName("status_matches") val statusMatches: List<String>?
|
// @Json(name = "status_matches") val statusMatches: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,16 +15,18 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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
|
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? = null,
|
||||||
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"
|
||||||
|
@ -42,8 +44,7 @@ data class FilterV1(
|
||||||
if (other !is FilterV1) {
|
if (other !is FilterV1) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val filter = other as FilterV1?
|
return other.id == id
|
||||||
return filter?.id.equals(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toFilter(): Filter {
|
fun toFilter(): Filter {
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
package com.keylesspalace.tusky.entity
|
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
|
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(
|
data class Instance(
|
||||||
val domain: String,
|
val domain: String,
|
||||||
// val title: String,
|
// val title: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
// @SerializedName("source_url") val sourceUrl: String,
|
// @Json(name = "source_url") val sourceUrl: String,
|
||||||
// val description: String,
|
// val description: String,
|
||||||
// val usage: Usage,
|
// val usage: Usage,
|
||||||
// val thumbnail: Thumbnail,
|
// val thumbnail: Thumbnail,
|
||||||
// val languages: List<String>,
|
// val languages: List<String>,
|
||||||
val configuration: Configuration?,
|
val configuration: Configuration? = null,
|
||||||
// val registrations: Registrations,
|
// val registrations: Registrations,
|
||||||
// val contact: Contact,
|
// val contact: Contact,
|
||||||
val rules: List<Rule>?,
|
val rules: List<Rule> = emptyList(),
|
||||||
val pleroma: PleromaConfiguration?
|
val pleroma: PleromaConfiguration? = null
|
||||||
) {
|
) {
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Usage(val users: Users) {
|
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(
|
data class Thumbnail(
|
||||||
val url: String,
|
val url: String,
|
||||||
val blurhash: String?,
|
val blurhash: String? = null,
|
||||||
val versions: Versions?
|
val versions: Versions? = null
|
||||||
) {
|
) {
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Versions(
|
data class Versions(
|
||||||
@SerializedName("@1x") val at1x: String?,
|
@Json(name = "@1x") val at1x: String? = null,
|
||||||
@SerializedName("@2x") val at2x: String?
|
@Json(name = "@2x") val at2x: String? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Configuration(
|
data class Configuration(
|
||||||
val urls: Urls?,
|
val urls: Urls? = null,
|
||||||
val accounts: Accounts?,
|
val accounts: Accounts? = null,
|
||||||
val statuses: Statuses?,
|
val statuses: Statuses? = null,
|
||||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachments?,
|
@Json(name = "media_attachments") val mediaAttachments: MediaAttachments? = null,
|
||||||
val polls: Polls?,
|
val polls: Polls? = null,
|
||||||
val translation: Translation?
|
val translation: Translation? = null
|
||||||
) {
|
) {
|
||||||
data class Urls(@SerializedName("streaming_api") val streamingApi: String)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Accounts(@SerializedName("max_featured_tags") val maxFeaturedTags: Int)
|
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(
|
data class Statuses(
|
||||||
@SerializedName("max_characters") val maxCharacters: Int,
|
@Json(name = "max_characters") val maxCharacters: Int? = null,
|
||||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int,
|
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int
|
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class MediaAttachments(
|
data class MediaAttachments(
|
||||||
// Warning: This is an array in mastodon and a dictionary in friendica
|
// Warning: This is an array in mastodon and a dictionary in friendica
|
||||||
// @SerializedName("supported_mime_types") val supportedMimeTypes: List<String>,
|
// @Json(name = "supported_mime_types") val supportedMimeTypes: List<String> = emptyList(),
|
||||||
@SerializedName("image_size_limit") val imageSizeLimitBytes: Long,
|
@Json(name = "image_size_limit") val imageSizeLimitBytes: Long? = null,
|
||||||
@SerializedName("image_matrix_limit") val imagePixelCountLimit: Long,
|
@Json(name = "image_matrix_limit") val imagePixelCountLimit: Long? = null,
|
||||||
@SerializedName("video_size_limit") val videoSizeLimitBytes: Long,
|
@Json(name = "video_size_limit") val videoSizeLimitBytes: Long? = null,
|
||||||
@SerializedName("video_matrix_limit") val videoPixelCountLimit: Long,
|
@Json(name = "video_matrix_limit") val videoPixelCountLimit: Long? = null,
|
||||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int
|
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Polls(
|
data class Polls(
|
||||||
@SerializedName("max_options") val maxOptions: Int,
|
@Json(name = "max_options") val maxOptions: Int? = null,
|
||||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int,
|
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int? = null,
|
||||||
@SerializedName("min_expiration") val minExpirationSeconds: Int,
|
@Json(name = "min_expiration") val minExpirationSeconds: Int? = null,
|
||||||
@SerializedName("max_expiration") val maxExpirationSeconds: Int
|
@Json(name = "max_expiration") val maxExpirationSeconds: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Translation(val enabled: Boolean)
|
data class Translation(val enabled: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Registrations(
|
data class Registrations(
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
@SerializedName("approval_required") val approvalRequired: Boolean,
|
@Json(name = "approval_required") val approvalRequired: Boolean,
|
||||||
val message: String?
|
val message: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Contact(val email: String, val account: Account)
|
data class Contact(val email: String, val account: Account)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Rule(val id: String, val text: String)
|
data class Rule(val id: String, val text: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,10 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
data class InstanceV1(
|
||||||
val uri: String,
|
val uri: String,
|
||||||
// val title: String,
|
// val title: String,
|
||||||
|
@ -27,14 +29,14 @@ 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? = null,
|
||||||
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?,
|
@Json(name = "poll_limits") val pollConfiguration: PollConfiguration? = null,
|
||||||
val configuration: InstanceConfiguration?,
|
val configuration: InstanceConfiguration? = null,
|
||||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||||
val pleroma: PleromaConfiguration?,
|
val pleroma: PleromaConfiguration? = null,
|
||||||
@SerializedName("upload_limit") val uploadLimit: Int?,
|
@Json(name = "upload_limit") val uploadLimit: Int? = null,
|
||||||
val rules: List<InstanceRules>?
|
val rules: List<InstanceRules> = emptyList()
|
||||||
) {
|
) {
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return uri.hashCode()
|
return uri.hashCode()
|
||||||
|
@ -44,54 +46,61 @@ data class InstanceV1(
|
||||||
if (other !is InstanceV1) {
|
if (other !is InstanceV1) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val instance = other as InstanceV1?
|
return other.uri == uri
|
||||||
return instance?.uri.equals(uri)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class PollConfiguration(
|
data class PollConfiguration(
|
||||||
@SerializedName("max_options") val maxOptions: Int?,
|
@Json(name = "max_options") val maxOptions: Int? = null,
|
||||||
@SerializedName("max_option_chars") val maxOptionChars: Int?,
|
@Json(name = "max_option_chars") val maxOptionChars: Int? = null,
|
||||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
|
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int? = null,
|
||||||
@SerializedName("min_expiration") val minExpiration: Int?,
|
@Json(name = "min_expiration") val minExpiration: Int? = null,
|
||||||
@SerializedName("max_expiration") val maxExpiration: Int?
|
@Json(name = "max_expiration") val maxExpiration: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class InstanceConfiguration(
|
data class InstanceConfiguration(
|
||||||
val statuses: StatusConfiguration?,
|
val statuses: StatusConfiguration? = null,
|
||||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
@Json(name = "media_attachments") val mediaAttachments: MediaAttachmentConfiguration? = null,
|
||||||
val polls: PollConfiguration?
|
val polls: PollConfiguration? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class StatusConfiguration(
|
data class StatusConfiguration(
|
||||||
@SerializedName("max_characters") val maxCharacters: Int?,
|
@Json(name = "max_characters") val maxCharacters: Int? = null,
|
||||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?
|
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@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> = emptyList(),
|
||||||
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
|
@Json(name = "image_size_limit") val imageSizeLimit: Int? = null,
|
||||||
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
|
@Json(name = "image_matrix_limit") val imageMatrixLimit: Int? = null,
|
||||||
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
|
@Json(name = "video_size_limit") val videoSizeLimit: Int? = null,
|
||||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int? = null,
|
||||||
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?
|
@Json(name = "video_matrix_limit") val videoMatrixLimit: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class PleromaConfiguration(
|
data class PleromaConfiguration(
|
||||||
val metadata: PleromaMetadata?
|
val metadata: PleromaMetadata? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@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? = null,
|
||||||
@SerializedName("name_length") val nameLength: Int?,
|
@Json(name = "name_length") val nameLength: Int? = null,
|
||||||
@SerializedName("value_length") val valueLength: Int?
|
@Json(name = "value_length") val valueLength: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class InstanceRules(
|
data class InstanceRules(
|
||||||
val id: String,
|
val id: String,
|
||||||
val text: String
|
val text: String
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
package com.keylesspalace.tusky.entity
|
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
|
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,17 +16,18 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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.
|
* Created by charlag on 1/4/18.
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class MastoList(
|
data class MastoList(
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val exclusive: Boolean?,
|
val exclusive: Boolean? = null,
|
||||||
@SerializedName("replies_policy") val repliesPolicy: String?
|
@Json(name = "replies_policy") val repliesPolicy: String? = null
|
||||||
) {
|
) {
|
||||||
enum class ReplyPolicy(val policy: String) {
|
enum class ReplyPolicy(val policy: String) {
|
||||||
NONE("none"),
|
NONE("none"),
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package com.keylesspalace.tusky.entity
|
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/
|
* 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
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,35 +16,39 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
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? = null,
|
||||||
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> = emptyList(),
|
||||||
@SerializedName("media_attributes") val mediaAttributes: List<MediaAttribute>?,
|
@Json(name = "media_attributes") val mediaAttributes: List<MediaAttribute> = emptyList(),
|
||||||
@SerializedName("scheduled_at") val scheduledAt: String?,
|
@Json(name = "scheduled_at") val scheduledAt: String? = null,
|
||||||
val poll: NewPoll?,
|
val poll: NewPoll? = null,
|
||||||
val language: String?
|
val language: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
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
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class MediaAttribute(
|
data class MediaAttribute(
|
||||||
val id: String,
|
val id: String,
|
||||||
val description: String?,
|
val description: String? = null,
|
||||||
val focus: String?,
|
val focus: String? = null,
|
||||||
val thumbnail: String?
|
val thumbnail: String? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
@ -16,65 +16,68 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
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.keylesspalace.tusky.R
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Notification(
|
data class Notification(
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val id: String,
|
val id: String,
|
||||||
val account: TimelineAccount,
|
val account: TimelineAccount,
|
||||||
val status: Status?,
|
val status: Status? = null,
|
||||||
val report: Report?
|
val report: Report? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** From https://docs.joinmastodon.org/entities/Notification/#type */
|
/** From https://docs.joinmastodon.org/entities/Notification/#type */
|
||||||
@JsonAdapter(NotificationTypeAdapter::class)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class Type(val presentation: String, @StringRes val uiString: Int) {
|
enum class Type(val presentation: String, @StringRes val uiString: Int) {
|
||||||
UNKNOWN("unknown", R.string.notification_unknown_name),
|
UNKNOWN("unknown", R.string.notification_unknown_name),
|
||||||
|
|
||||||
/** Someone mentioned you */
|
/** Someone mentioned you */
|
||||||
|
@Json(name = "mention")
|
||||||
MENTION("mention", R.string.notification_mention_name),
|
MENTION("mention", R.string.notification_mention_name),
|
||||||
|
|
||||||
/** Someone boosted one of your statuses */
|
/** Someone boosted one of your statuses */
|
||||||
|
@Json(name = "reblog")
|
||||||
REBLOG("reblog", R.string.notification_boost_name),
|
REBLOG("reblog", R.string.notification_boost_name),
|
||||||
|
|
||||||
/** Someone favourited one of your statuses */
|
/** Someone favourited one of your statuses */
|
||||||
|
@Json(name = "favourite")
|
||||||
FAVOURITE("favourite", R.string.notification_favourite_name),
|
FAVOURITE("favourite", R.string.notification_favourite_name),
|
||||||
|
|
||||||
/** Someone followed you */
|
/** Someone followed you */
|
||||||
|
@Json(name = "follow")
|
||||||
FOLLOW("follow", R.string.notification_follow_name),
|
FOLLOW("follow", R.string.notification_follow_name),
|
||||||
|
|
||||||
/** Someone requested to follow you */
|
/** Someone requested to follow you */
|
||||||
|
@Json(name = "follow_request")
|
||||||
FOLLOW_REQUEST("follow_request", R.string.notification_follow_request_name),
|
FOLLOW_REQUEST("follow_request", R.string.notification_follow_request_name),
|
||||||
|
|
||||||
/** 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", R.string.notification_poll_name),
|
POLL("poll", R.string.notification_poll_name),
|
||||||
|
|
||||||
/** Someone you enabled notifications for has posted a status */
|
/** Someone you enabled notifications for has posted a status */
|
||||||
|
@Json(name = "status")
|
||||||
STATUS("status", R.string.notification_subscription_name),
|
STATUS("status", R.string.notification_subscription_name),
|
||||||
|
|
||||||
/** Someone signed up (optionally sent to admins) */
|
/** Someone signed up (optionally sent to admins) */
|
||||||
|
@Json(name = "admin.sign_up")
|
||||||
SIGN_UP("admin.sign_up", R.string.notification_sign_up_name),
|
SIGN_UP("admin.sign_up", R.string.notification_sign_up_name),
|
||||||
|
|
||||||
/** A status you interacted with has been updated */
|
/** A status you interacted with has been updated */
|
||||||
|
@Json(name = "update")
|
||||||
UPDATE("update", R.string.notification_update_name),
|
UPDATE("update", R.string.notification_update_name),
|
||||||
|
|
||||||
/** A new report has been filed */
|
/** A new report has been filed */
|
||||||
|
@Json(name = "admin.report")
|
||||||
REPORT("admin.report", R.string.notification_report_name);
|
REPORT("admin.report", R.string.notification_report_name);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun byString(s: String): Type {
|
fun byString(s: String): Type {
|
||||||
entries.forEach {
|
return entries.firstOrNull { it.presentation == s } ?: UNKNOWN
|
||||||
if (s == it.presentation) {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return UNKNOWN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Notification types for UI display (omits UNKNOWN) */
|
/** Notification types for UI display (omits UNKNOWN) */
|
||||||
|
@ -95,20 +98,7 @@ data class Notification(
|
||||||
if (other !is Notification) {
|
if (other !is Notification) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val notification = other as Notification?
|
return other.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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper for Java */
|
/** Helper for Java */
|
||||||
|
|
|
@ -15,10 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
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,25 +1,27 @@
|
||||||
package com.keylesspalace.tusky.entity
|
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
|
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? = null,
|
||||||
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? = null,
|
||||||
val options: List<PollOption>,
|
val options: List<PollOption>,
|
||||||
val voted: Boolean,
|
val voted: Boolean = false,
|
||||||
@SerializedName("own_votes") val ownVotes: List<Int>?
|
@Json(name = "own_votes") val ownVotes: List<Int> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun votedCopy(choices: List<Int>): Poll {
|
fun votedCopy(choices: List<Int>): Poll {
|
||||||
val newOptions = options.mapIndexed { index, option ->
|
val newOptions = options.mapIndexed { index, option ->
|
||||||
if (choices.contains(index)) {
|
if (choices.contains(index)) {
|
||||||
option.copy(votesCount = option.votesCount + 1)
|
option.copy(votesCount = (option.votesCount ?: 0) + 1)
|
||||||
} else {
|
} else {
|
||||||
option
|
option
|
||||||
}
|
}
|
||||||
|
@ -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? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,27 +15,28 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter
|
import com.keylesspalace.tusky.json.Guarded
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.squareup.moshi.Json
|
||||||
import com.keylesspalace.tusky.json.GuardedBooleanAdapter
|
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.
|
/* Pleroma extension, same as 'notifying' on Mastodon.
|
||||||
* Some instances like qoto.org have a custom subscription feature where 'subscribing' is a json object,
|
* 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,
|
@Guarded val subscribing: Boolean? = null,
|
||||||
@SerializedName("domain_blocking") val blockingDomain: Boolean,
|
@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? = null,
|
||||||
// since 3.3.0rc
|
// since 3.3.0rc
|
||||||
val notifying: Boolean?
|
val notifying: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package com.keylesspalace.tusky.entity
|
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
|
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>? = null,
|
||||||
@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
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,11 +15,13 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
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>,
|
||||||
|
|
|
@ -17,44 +17,47 @@ package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
|
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? = null,
|
||||||
val account: TimelineAccount,
|
val account: TimelineAccount,
|
||||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
@Json(name = "in_reply_to_id") val inReplyToId: String? = null,
|
||||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
@Json(name = "in_reply_to_account_id") val inReplyToAccountId: String? = null,
|
||||||
val reblog: Status?,
|
val reblog: Status? = null,
|
||||||
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? = null,
|
||||||
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 = false,
|
||||||
val favourited: Boolean,
|
val favourited: Boolean = false,
|
||||||
val bookmarked: Boolean,
|
val bookmarked: Boolean = false,
|
||||||
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>?,
|
// Use null to mark the absence of tags because of semantic differences in LinkHelper
|
||||||
val application: Application?,
|
val tags: List<HashTag>? = null,
|
||||||
val pinned: Boolean?,
|
val application: Application? = null,
|
||||||
val muted: Boolean?,
|
val pinned: Boolean = false,
|
||||||
val poll: Poll?,
|
val muted: Boolean = false,
|
||||||
|
val poll: Poll? = null,
|
||||||
/** Preview card for links included within status content. */
|
/** Preview card for links included within status content. */
|
||||||
val card: Card?,
|
val card: Card? = null,
|
||||||
/** ISO 639 language code for this status. */
|
/** 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. */
|
/** 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
|
val actionableId: String
|
||||||
|
@ -70,30 +73,30 @@ data class Status(
|
||||||
fun copyWithPoll(poll: Poll?): Status = copy(poll = poll)
|
fun copyWithPoll(poll: Poll?): Status = copy(poll = poll)
|
||||||
fun copyWithPinned(pinned: Boolean): Status = copy(pinned = pinned)
|
fun copyWithPinned(pinned: Boolean): Status = copy(pinned = pinned)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = false)
|
||||||
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);
|
||||||
|
|
||||||
fun serverString(): String {
|
val serverString: String
|
||||||
return when (this) {
|
get() = when (this) {
|
||||||
PUBLIC -> "public"
|
PUBLIC -> "public"
|
||||||
UNLISTED -> "unlisted"
|
UNLISTED -> "unlisted"
|
||||||
PRIVATE -> "private"
|
PRIVATE -> "private"
|
||||||
DIRECT -> "direct"
|
DIRECT -> "direct"
|
||||||
UNKNOWN -> "unknown"
|
UNKNOWN -> "unknown"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@ -123,13 +126,10 @@ data class Status(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rebloggingAllowed(): Boolean {
|
val isRebloggingAllowed: Boolean
|
||||||
return (visibility != Visibility.DIRECT && visibility != Visibility.UNKNOWN)
|
get() {
|
||||||
}
|
return (visibility != Visibility.DIRECT && visibility != Visibility.UNKNOWN)
|
||||||
|
}
|
||||||
fun isPinned(): Boolean {
|
|
||||||
return pinned ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toDeletedStatus(): DeletedStatus {
|
fun toDeletedStatus(): DeletedStatus {
|
||||||
return DeletedStatus(
|
return DeletedStatus(
|
||||||
|
@ -164,16 +164,18 @@ 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? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
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 com.keylesspalace.tusky.entity
|
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
|
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? = null,
|
||||||
@SerializedName("media_attachments") val mediaAttachments: List<Attachment>,
|
@Json(name = "media_attachments") val mediaAttachments: List<Attachment>,
|
||||||
val emojis: List<Emoji>
|
val emojis: List<Emoji>
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,12 +15,14 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
data class StatusParams(
|
||||||
val text: String,
|
val text: String,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean? = null,
|
||||||
val visibility: Status.Visibility,
|
val visibility: Status.Visibility,
|
||||||
@SerializedName("spoiler_text") val spoilerText: String,
|
@Json(name = "spoiler_text") val spoilerText: String? = null,
|
||||||
@SerializedName("in_reply_to_id") val inReplyToId: String?
|
@Json(name = "in_reply_to_id") val inReplyToId: String? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,10 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
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
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,24 +15,26 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
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.
|
* 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
|
||||||
@SerializedName("display_name") val displayName: String?,
|
@Json(name = "display_name") val displayName: String? = null,
|
||||||
val url: String,
|
val url: String,
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val note: String,
|
val note: String,
|
||||||
val bot: Boolean = false,
|
val bot: Boolean = false,
|
||||||
// nullable for backward compatibility
|
// optional for backward compatibility
|
||||||
val emojis: List<Emoji>? = emptyList()
|
val emojis: List<Emoji> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val name: String
|
val name: String
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package com.keylesspalace.tusky.entity
|
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(
|
data class MediaTranslation(
|
||||||
val id: String,
|
val id: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
|
@ -12,22 +14,25 @@ data class MediaTranslation(
|
||||||
*
|
*
|
||||||
* See [doc](https://docs.joinmastodon.org/entities/Translation/).
|
* See [doc](https://docs.joinmastodon.org/entities/Translation/).
|
||||||
*/
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Translation(
|
data class Translation(
|
||||||
val content: String,
|
val content: String,
|
||||||
@SerializedName("spoiler_text")
|
@Json(name = "spoiler_text")
|
||||||
val spoilerText: String?,
|
val spoilerText: String? = null,
|
||||||
val poll: TranslatedPoll?,
|
val poll: TranslatedPoll? = null,
|
||||||
@SerializedName("media_attachments")
|
@Json(name = "media_attachments")
|
||||||
val mediaAttachments: List<MediaTranslation>,
|
val mediaAttachments: List<MediaTranslation>,
|
||||||
@SerializedName("detected_source_language")
|
@Json(name = "detected_source_language")
|
||||||
val detectedSourceLanguage: String,
|
val detectedSourceLanguage: String,
|
||||||
val provider: String,
|
val provider: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class TranslatedPoll(
|
data class TranslatedPoll(
|
||||||
val options: List<TranslatedPollOption>
|
val options: List<TranslatedPollOption>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class TranslatedPollOption(
|
data class TranslatedPollOption(
|
||||||
val title: String
|
val title: String
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import java.util.Date
|
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 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>
|
||||||
|
@ -37,11 +39,14 @@ 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,
|
||||||
val uses: String
|
val uses: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TrendingTag.start() = Date(history.last().day.toLong() * 1000L)
|
val TrendingTag.start
|
||||||
fun TrendingTag.end() = Date(history.first().day.toLong() * 1000L)
|
get() = Date(history.last().day.toLong() * 1000L)
|
||||||
|
val TrendingTag.end
|
||||||
|
get() = Date(history.first().day.toLong() * 1000L)
|
||||||
|
|
|
@ -180,7 +180,7 @@ abstract class SFragment : Fragment(), Injectable {
|
||||||
R.id.pin,
|
R.id.pin,
|
||||||
1,
|
1,
|
||||||
getString(
|
getString(
|
||||||
if (status.isPinned()) R.string.unpin_action else R.string.pin_action
|
if (status.pinned) R.string.unpin_action else R.string.pin_action
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ abstract class SFragment : Fragment(), Injectable {
|
||||||
muteConversationItem.isVisible = mutable
|
muteConversationItem.isVisible = mutable
|
||||||
if (mutable) {
|
if (mutable) {
|
||||||
muteConversationItem.setTitle(
|
muteConversationItem.setTitle(
|
||||||
if (status.muted != true) {
|
if (!status.muted) {
|
||||||
R.string.action_mute_conversation
|
R.string.action_mute_conversation
|
||||||
} else {
|
} else {
|
||||||
R.string.action_unmute_conversation
|
R.string.action_unmute_conversation
|
||||||
|
@ -328,10 +328,10 @@ abstract class SFragment : Fragment(), Injectable {
|
||||||
|
|
||||||
R.id.pin -> {
|
R.id.pin -> {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
timelineCases.pin(status.id, !status.isPinned())
|
timelineCases.pin(status.id, !status.pinned)
|
||||||
.onFailure { e: Throwable ->
|
.onFailure { e: Throwable ->
|
||||||
val message = e.message
|
val message = e.message
|
||||||
?: getString(if (status.isPinned()) R.string.failed_to_unpin else R.string.failed_to_pin)
|
?: getString(if (status.pinned) R.string.failed_to_unpin else R.string.failed_to_pin)
|
||||||
Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG)
|
Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
@ -341,7 +341,7 @@ abstract class SFragment : Fragment(), Injectable {
|
||||||
|
|
||||||
R.id.status_mute_conversation -> {
|
R.id.status_mute_conversation -> {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
timelineCases.muteConversation(status.id, status.muted != true)
|
timelineCases.muteConversation(status.id, !status.muted)
|
||||||
}
|
}
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
|
@ -444,7 +444,7 @@ abstract class SFragment : Fragment(), Injectable {
|
||||||
timelineCases.delete(id).fold(
|
timelineCases.delete(id).fold(
|
||||||
{ deletedStatus ->
|
{ deletedStatus ->
|
||||||
removeItem(position)
|
removeItem(position)
|
||||||
val sourceStatus = if (deletedStatus.isEmpty()) {
|
val sourceStatus = if (deletedStatus.isEmpty) {
|
||||||
status.toDeletedStatus()
|
status.toDeletedStatus()
|
||||||
} else {
|
} else {
|
||||||
deletedStatus
|
deletedStatus
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* Copyright 2022 Tusky Contributors
|
/*
|
||||||
|
* Copyright 2024 Tusky Contributors
|
||||||
*
|
*
|
||||||
* This file is a part of Tusky.
|
* This file is a part of Tusky.
|
||||||
*
|
*
|
||||||
|
@ -11,27 +12,13 @@
|
||||||
* Public License for more details.
|
* Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
package com.keylesspalace.tusky.json
|
package com.keylesspalace.tusky.json
|
||||||
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
import com.squareup.moshi.JsonQualifier
|
||||||
import com.google.gson.JsonDeserializer
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class GuardedBooleanAdapter : JsonDeserializer<Boolean?> {
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@Throws(JsonParseException::class)
|
@JsonQualifier
|
||||||
override fun deserialize(
|
internal annotation class Guarded
|
||||||
json: JsonElement,
|
|
||||||
typeOfT: Type,
|
|
||||||
context: JsonDeserializationContext
|
|
||||||
): Boolean? {
|
|
||||||
return if (json.isJsonObject) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
json.asBoolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Tusky 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 Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.json
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This adapter tries to parse the value using a delegated parser
|
||||||
|
* and returns null in case of error.
|
||||||
|
*/
|
||||||
|
class GuardedAdapter<T> private constructor(
|
||||||
|
private val delegate: JsonAdapter<T>
|
||||||
|
) : JsonAdapter<T>() {
|
||||||
|
|
||||||
|
override fun fromJson(reader: JsonReader): T? {
|
||||||
|
return try {
|
||||||
|
delegate.fromJson(reader)
|
||||||
|
} catch (e: JsonDataException) {
|
||||||
|
reader.skipValue()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(writer: JsonWriter, value: T?) {
|
||||||
|
delegate.toJson(writer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ANNOTATION_FACTORY = object : Factory {
|
||||||
|
override fun create(
|
||||||
|
type: Type,
|
||||||
|
annotations: Set<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,313 +0,0 @@
|
||||||
package com.keylesspalace.tusky.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 com.keylesspalace.tusky.json
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
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
|
|
||||||
|
|
||||||
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) {
|
|
||||||
Log.w("Rfc3339DateJsonAdapter", jpe)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -47,11 +47,11 @@ class FilterModel @Inject constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val matchingKind = status.filtered?.filter { result ->
|
val matchingKind = status.filtered.filter { result ->
|
||||||
result.filter.kinds.contains(kind)
|
result.filter.kinds.contains(kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (matchingKind.isNullOrEmpty()) {
|
return if (matchingKind.isEmpty()) {
|
||||||
Filter.Action.NONE
|
Filter.Action.NONE
|
||||||
} else {
|
} else {
|
||||||
matchingKind.maxOf { it.filter.action }
|
matchingKind.maxOf { it.filter.action }
|
||||||
|
|
|
@ -94,7 +94,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
StatusToSend(
|
StatusToSend(
|
||||||
text = text,
|
text = text,
|
||||||
warningText = spoiler,
|
warningText = spoiler,
|
||||||
visibility = visibility.serverString(),
|
visibility = visibility.serverString,
|
||||||
sensitive = false,
|
sensitive = false,
|
||||||
media = emptyList(),
|
media = emptyList(),
|
||||||
scheduledAt = null,
|
scheduledAt = null,
|
||||||
|
|
|
@ -39,8 +39,8 @@ import java.util.regex.Pattern
|
||||||
* @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable)
|
* @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable)
|
||||||
* @return the text with the shortcodes replaced by EmojiSpans
|
* @return the text with the shortcodes replaced by EmojiSpans
|
||||||
*/
|
*/
|
||||||
fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean): CharSequence {
|
fun CharSequence.emojify(emojis: List<Emoji>, view: View, animate: Boolean): CharSequence {
|
||||||
if (emojis.isNullOrEmpty()) {
|
if (emojis.isEmpty()) {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ class ListStatusAccessibilityDelegate(
|
||||||
info.addAction(replyAction)
|
info.addAction(replyAction)
|
||||||
|
|
||||||
val actionable = status.actionable
|
val actionable = status.actionable
|
||||||
if (actionable.rebloggingAllowed()) {
|
if (actionable.isRebloggingAllowed) {
|
||||||
info.addAction(if (actionable.reblogged) unreblogAction else reblogAction)
|
info.addAction(if (actionable.reblogged) unreblogAction else reblogAction)
|
||||||
}
|
}
|
||||||
info.addAction(if (actionable.favourited) unfavouriteAction else favouriteAction)
|
info.addAction(if (actionable.favourited) unfavouriteAction else favouriteAction)
|
||||||
|
|
|
@ -43,8 +43,8 @@ data class PollOptionViewData(
|
||||||
var voted: Boolean
|
var voted: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int {
|
fun calculatePercent(fraction: Int?, totalVoters: Int?, totalVotes: Int): Int {
|
||||||
return if (fraction == 0) {
|
return if (fraction == null || fraction == 0) {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
val total = totalVoters ?: totalVotes
|
val total = totalVoters ?: totalVotes
|
||||||
|
@ -76,7 +76,7 @@ fun Poll?.toViewData(): PollViewData? {
|
||||||
votersCount = votersCount,
|
votersCount = votersCount,
|
||||||
options = options.mapIndexed { index, option ->
|
options = options.mapIndexed { index, option ->
|
||||||
option.toViewData(
|
option.toViewData(
|
||||||
ownVotes?.contains(index) == true
|
ownVotes.contains(index)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
voted = voted
|
voted = voted
|
||||||
|
@ -86,7 +86,7 @@ fun Poll?.toViewData(): PollViewData? {
|
||||||
fun PollOption.toViewData(voted: Boolean): PollOptionViewData {
|
fun PollOption.toViewData(voted: Boolean): PollOptionViewData {
|
||||||
return PollOptionViewData(
|
return PollOptionViewData(
|
||||||
title = title,
|
title = title,
|
||||||
votesCount = votesCount,
|
votesCount = votesCount ?: 0,
|
||||||
selected = false,
|
selected = false,
|
||||||
voted = voted
|
voted = voted
|
||||||
)
|
)
|
||||||
|
|
|
@ -101,8 +101,8 @@
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
license:license="@string/license_apache_2"
|
license:license="@string/license_apache_2"
|
||||||
license:link="https://github.com/google/gson"
|
license:link="https://github.com/square/moshi"
|
||||||
license:name="Gson" />
|
license:name="Moshi" />
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.LicenseCard
|
<com.keylesspalace.tusky.view.LicenseCard
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -94,7 +94,7 @@ class BottomSheetActivityTest {
|
||||||
poll = null,
|
poll = null,
|
||||||
card = null,
|
card = null,
|
||||||
language = null,
|
language = null,
|
||||||
filtered = null
|
filtered = emptyList()
|
||||||
)
|
)
|
||||||
private val statusResult = NetworkResult.success(SearchResult(emptyList(), listOf(status), emptyList()))
|
private val statusResult = NetworkResult.success(SearchResult(emptyList(), listOf(status), emptyList()))
|
||||||
|
|
||||||
|
|
|
@ -334,14 +334,14 @@ class FilterV1Test {
|
||||||
PollOption(it, 0)
|
PollOption(it, 0)
|
||||||
},
|
},
|
||||||
voted = false,
|
voted = false,
|
||||||
ownVotes = null
|
ownVotes = emptyList()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
card = null,
|
card = null,
|
||||||
language = null,
|
language = null,
|
||||||
filtered = null
|
filtered = emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.google.gson.Gson
|
import com.keylesspalace.tusky.di.NetworkModule
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
import com.squareup.moshi.adapter
|
||||||
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
|
||||||
|
@ -37,7 +38,7 @@ class StatusComparisonTest {
|
||||||
assertNotEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
assertNotEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val gson = Gson()
|
private val moshi = NetworkModule.providesMoshi()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `two equal status view data - should be equal`() {
|
fun `two equal status view data - should be equal`() {
|
||||||
|
@ -90,6 +91,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 = """
|
||||||
|
@ -201,6 +203,6 @@ class StatusComparisonTest {
|
||||||
"poll": null
|
"poll": null
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
return gson.fromJson(statusJson, Status::class.java)
|
return moshi.adapter<Status>().fromJson(statusJson)!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import android.os.Looper.getMainLooper
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
|
@ -31,6 +30,7 @@ import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.EmojisEntity
|
import com.keylesspalace.tusky.db.EmojisEntity
|
||||||
import com.keylesspalace.tusky.db.InstanceDao
|
import com.keylesspalace.tusky.db.InstanceDao
|
||||||
import com.keylesspalace.tusky.db.InstanceInfoEntity
|
import com.keylesspalace.tusky.db.InstanceInfoEntity
|
||||||
|
import com.keylesspalace.tusky.di.NetworkModule
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Instance
|
import com.keylesspalace.tusky.entity.Instance
|
||||||
import com.keylesspalace.tusky.entity.InstanceConfiguration
|
import com.keylesspalace.tusky.entity.InstanceConfiguration
|
||||||
|
@ -38,6 +38,7 @@ import com.keylesspalace.tusky.entity.InstanceV1
|
||||||
import com.keylesspalace.tusky.entity.SearchResult
|
import com.keylesspalace.tusky.entity.SearchResult
|
||||||
import com.keylesspalace.tusky.entity.StatusConfiguration
|
import com.keylesspalace.tusky.entity.StatusConfiguration
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.squareup.moshi.adapter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
@ -98,7 +99,7 @@ class ComposeActivityTest {
|
||||||
private var instanceV1ResponseCallback: (() -> InstanceV1)? = null
|
private var instanceV1ResponseCallback: (() -> InstanceV1)? = null
|
||||||
private var instanceResponseCallback: (() -> Instance)? = null
|
private var instanceResponseCallback: (() -> Instance)? = null
|
||||||
private var composeOptions: ComposeActivity.ComposeOptions? = null
|
private var composeOptions: ComposeActivity.ComposeOptions? = null
|
||||||
private val gson = Gson()
|
private val moshi = NetworkModule.providesMoshi()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setupActivity() {
|
fun setupActivity() {
|
||||||
|
@ -583,7 +584,7 @@ class ComposeActivityTest {
|
||||||
|
|
||||||
private fun getConfiguration(maximumStatusCharacters: Int?, charactersReservedPerUrl: Int?): Instance.Configuration {
|
private fun getConfiguration(maximumStatusCharacters: Int?, charactersReservedPerUrl: Int?): Instance.Configuration {
|
||||||
return Instance.Configuration(
|
return Instance.Configuration(
|
||||||
Instance.Configuration.Urls(streamingApi = ""),
|
Instance.Configuration.Urls(),
|
||||||
Instance.Configuration.Accounts(1),
|
Instance.Configuration.Accounts(1),
|
||||||
Instance.Configuration.Statuses(
|
Instance.Configuration.Statuses(
|
||||||
maximumStatusCharacters ?: InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT,
|
maximumStatusCharacters ?: InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT,
|
||||||
|
@ -622,8 +623,9 @@ class ComposeActivityTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
private fun getSampleFriendicaInstance(): Instance {
|
private fun getSampleFriendicaInstance(): Instance {
|
||||||
return gson.fromJson(sampleFriendicaResponse, Instance::class.java)
|
return moshi.adapter<Instance>().fromJson(sampleFriendicaResponse)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -10,13 +10,13 @@ import androidx.paging.RemoteMediator
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineRemoteMediator
|
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineRemoteMediator
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.Converters
|
import com.keylesspalace.tusky.db.Converters
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
|
import com.keylesspalace.tusky.di.NetworkModule
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -54,6 +54,8 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
|
|
||||||
private lateinit var db: AppDatabase
|
private lateinit var db: AppDatabase
|
||||||
|
|
||||||
|
private val moshi = NetworkModule.providesMoshi()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
@ -61,7 +63,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +82,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
|
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||||
|
@ -99,7 +101,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
|
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||||
|
@ -115,7 +117,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
accountManager = accountManager,
|
accountManager = accountManager,
|
||||||
api = mock(),
|
api = mock(),
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -166,7 +168,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -229,7 +231,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -289,7 +291,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -334,7 +336,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -385,7 +387,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -441,7 +443,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
@ -493,7 +495,7 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
db = db,
|
db = db,
|
||||||
gson = Gson()
|
moshi = moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = state(
|
val state = state(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.keylesspalace.tusky.components.timeline
|
package com.keylesspalace.tusky.components.timeline
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
|
import com.keylesspalace.tusky.di.NetworkModule
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
@ -54,7 +54,7 @@ fun mockStatus(
|
||||||
poll = null,
|
poll = null,
|
||||||
card = null,
|
card = null,
|
||||||
language = null,
|
language = null,
|
||||||
filtered = null
|
filtered = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun mockStatusViewData(
|
fun mockStatusViewData(
|
||||||
|
@ -91,19 +91,19 @@ fun mockStatusEntityWithAccount(
|
||||||
expanded: Boolean = false
|
expanded: Boolean = false
|
||||||
): TimelineStatusWithAccount {
|
): TimelineStatusWithAccount {
|
||||||
val mockedStatus = mockStatus(id)
|
val mockedStatus = mockStatus(id)
|
||||||
val gson = Gson()
|
val moshi = NetworkModule.providesMoshi()
|
||||||
|
|
||||||
return TimelineStatusWithAccount(
|
return TimelineStatusWithAccount(
|
||||||
status = mockedStatus.toEntity(
|
status = mockedStatus.toEntity(
|
||||||
timelineUserId = userId,
|
timelineUserId = userId,
|
||||||
gson = gson,
|
moshi = moshi,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
contentShowing = false,
|
contentShowing = false,
|
||||||
contentCollapsed = true
|
contentCollapsed = true
|
||||||
),
|
),
|
||||||
account = mockedStatus.account.toEntity(
|
account = mockedStatus.account.toEntity(
|
||||||
accountId = userId,
|
accountId = userId,
|
||||||
gson = gson
|
moshi = moshi
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import androidx.room.Room
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusChangedEvent
|
import com.keylesspalace.tusky.appstore.StatusChangedEvent
|
||||||
import com.keylesspalace.tusky.components.timeline.mockStatus
|
import com.keylesspalace.tusky.components.timeline.mockStatus
|
||||||
|
@ -15,6 +14,7 @@ import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.Converters
|
import com.keylesspalace.tusky.db.Converters
|
||||||
|
import com.keylesspalace.tusky.di.NetworkModule
|
||||||
import com.keylesspalace.tusky.entity.StatusContext
|
import com.keylesspalace.tusky.entity.StatusContext
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
@ -44,6 +44,7 @@ class ViewThreadViewModelTest {
|
||||||
private lateinit var db: AppDatabase
|
private lateinit var db: AppDatabase
|
||||||
|
|
||||||
private val threadId = "1234"
|
private val threadId = "1234"
|
||||||
|
private val moshi = NetworkModule.providesMoshi()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute each task synchronously.
|
* Execute each task synchronously.
|
||||||
|
@ -95,12 +96,11 @@ class ViewThreadViewModelTest {
|
||||||
}
|
}
|
||||||
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))
|
||||||
.allowMainThreadQueries()
|
.allowMainThreadQueries()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val gson = Gson()
|
viewModel = ViewThreadViewModel(api, filterModel, timelineCases, eventHub, accountManager, db, moshi)
|
||||||
viewModel = ViewThreadViewModel(api, filterModel, timelineCases, eventHub, accountManager, db, gson)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|
|
@ -4,9 +4,9 @@ import androidx.paging.PagingSource
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||||
|
import com.keylesspalace.tusky.di.NetworkModule
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
@ -23,11 +23,13 @@ class TimelineDaoTest {
|
||||||
private lateinit var timelineDao: TimelineDao
|
private lateinit var timelineDao: TimelineDao
|
||||||
private lateinit var db: AppDatabase
|
private lateinit var db: AppDatabase
|
||||||
|
|
||||||
|
private val moshi = NetworkModule.providesMoshi()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun createDb() {
|
fun createDb() {
|
||||||
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))
|
||||||
.allowMainThreadQueries()
|
.allowMainThreadQueries()
|
||||||
.build()
|
.build()
|
||||||
timelineDao = db.timelineDao()
|
timelineDao = db.timelineDao()
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package com.keylesspalace.tusky.json
|
package com.keylesspalace.tusky.json
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapter
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
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(GuardedAdapter.ANNOTATION_FACTORY)
|
||||||
|
.build()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should deserialize Relationship when attribute 'subscribing' is a boolean`() {
|
fun `should deserialize Relationship when attribute 'subscribing' is a boolean`() {
|
||||||
|
@ -45,7 +49,7 @@ class GuardedBooleanAdapterTest {
|
||||||
note = "Hi",
|
note = "Hi",
|
||||||
notifying = false
|
notifying = false
|
||||||
),
|
),
|
||||||
gson.fromJson(jsonInput, Relationship::class.java)
|
moshi.adapter<Relationship>().fromJson(jsonInput)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +89,7 @@ class GuardedBooleanAdapterTest {
|
||||||
note = "Hi",
|
note = "Hi",
|
||||||
notifying = false
|
notifying = false
|
||||||
),
|
),
|
||||||
gson.fromJson(jsonInput, Relationship::class.java)
|
moshi.adapter<Relationship>().fromJson(jsonInput)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +128,7 @@ class GuardedBooleanAdapterTest {
|
||||||
note = "Hi",
|
note = "Hi",
|
||||||
notifying = false
|
notifying = false
|
||||||
),
|
),
|
||||||
gson.fromJson(jsonInput, Relationship::class.java)
|
moshi.adapter<Relationship>().fromJson(jsonInput)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -104,7 +104,7 @@ class TimelineCasesTest {
|
||||||
poll = null,
|
poll = null,
|
||||||
card = null,
|
card = null,
|
||||||
language = null,
|
language = null,
|
||||||
filtered = null
|
filtered = emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,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"
|
|
||||||
kotlin = "1.9.23"
|
kotlin = "1.9.23"
|
||||||
image-cropper = "4.3.2"
|
image-cropper = "4.3.2"
|
||||||
material = "1.11.0"
|
material = "1.11.0"
|
||||||
|
@ -40,6 +39,7 @@ material-drawer = "8.4.5"
|
||||||
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.1.0"
|
networkresult-calladapter = "1.1.0"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
retrofit = "2.11.0"
|
retrofit = "2.11.0"
|
||||||
|
@ -107,7 +107,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" }
|
|
||||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||||
image-cropper = { module = "com.github.CanHub:Android-Image-Cropper", version.ref = "image-cropper" }
|
image-cropper = { module = "com.github.CanHub:Android-Image-Cropper", version.ref = "image-cropper" }
|
||||||
|
@ -117,10 +116,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-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||||
|
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" }
|
||||||
|
moshi-kotlin-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" }
|
||||||
sparkbutton = { module = "at.connyduck.sparkbutton:sparkbutton", version.ref = "sparkbutton" }
|
sparkbutton = { module = "at.connyduck.sparkbutton:sparkbutton", version.ref = "sparkbutton" }
|
||||||
|
@ -143,7 +145,8 @@ filemojicompat = ["filemojicompat-core", "filemojicompat-ui", "filemojicompat-de
|
||||||
glide = ["glide-core", "glide-okhttp3-integration", "glide-animation-plugin"]
|
glide = ["glide-core", "glide-okhttp3-integration", "glide-animation-plugin"]
|
||||||
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"]
|
||||||
|
moshi = ["moshi-core", "moshi-adapters"]
|
||||||
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"]
|
||||||
xmldiff = ["diffx", "xmlwriter"]
|
xmldiff = ["diffx", "xmlwriter"]
|
||||||
|
|
Loading…
Reference in New Issue