Merge branch 'develop' into account_header
This commit is contained in:
commit
763dc3bd4e
|
@ -72,7 +72,7 @@ android {
|
|||
|
||||
lint {
|
||||
lintConfig file("lint.xml")
|
||||
// Regenerate by running `./gradlew app:newLintBaseline`
|
||||
// Regenerate by deleting the file and running `./gradlew app:lintGreenDebug`
|
||||
baseline = file("lint-baseline.xml")
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,8 @@ dependencies {
|
|||
|
||||
implementation libs.android.material
|
||||
|
||||
implementation libs.gson
|
||||
implementation libs.bundles.moshi
|
||||
ksp libs.moshi.kotlin.codegen
|
||||
|
||||
implementation libs.bundles.retrofit
|
||||
implementation libs.networkresult.calladapter
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.2.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.2)" variant="all" version="8.2.2">
|
||||
<issues format="6" by="lint 8.3.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.1)" variant="all" version="8.3.1">
|
||||
|
||||
<issue
|
||||
id="GestureBackNavigation"
|
||||
|
@ -19,7 +19,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java"
|
||||
line="101"
|
||||
line="104"
|
||||
column="32"/>
|
||||
</issue>
|
||||
|
||||
|
@ -53,14 +53,14 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="Overriding `@layout/exo_player_control_view` which is marked as private in androidx.media3:media3-ui:1.2.1. If deliberate, use tools:override="true", otherwise pick a different name.">
|
||||
message="Overriding `@layout/exo_player_control_view` which is marked as private in androidx.media3:media3-ui:1.3.0. If deliberate, use tools:override="true", otherwise pick a different name.">
|
||||
<location
|
||||
file="src/main/res/layout/exo_player_control_view.xml"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:background="@color/exo_bottom_bar_background""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -71,7 +71,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_controls_padding` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_controls_padding` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:padding="@dimen/exo_styled_controls_padding""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -82,7 +82,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@layout/exo_player_control_rewind_button` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@layout/exo_player_control_rewind_button` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" <include layout="@layout/exo_player_control_rewind_button" />"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -93,7 +93,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@layout/exo_player_control_ffwd_button` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@layout/exo_player_control_ffwd_button` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" <include layout="@layout/exo_player_control_ffwd_button" />"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -104,7 +104,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_height` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_height` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:layout_height="@dimen/exo_styled_bottom_bar_height""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -115,7 +115,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_margin_top` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_margin_top` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -126,7 +126,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:background="@color/exo_bottom_bar_background""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -137,7 +137,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:paddingStart="@dimen/exo_styled_bottom_bar_time_padding""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -148,7 +148,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:paddingEnd="@dimen/exo_styled_bottom_bar_time_padding""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -159,7 +159,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -170,7 +170,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -181,7 +181,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_progress_layout_height` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_progress_layout_height` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:layout_height="@dimen/exo_styled_progress_layout_height""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -192,7 +192,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_progress_margin_bottom` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_progress_margin_bottom` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom"/>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -203,7 +203,7 @@
|
|||
|
||||
<issue
|
||||
id="PrivateResource"
|
||||
message="The resource `@dimen/exo_styled_minimal_controls_margin_bottom` is marked as private in androidx.media3:media3-ui:1.2.1"
|
||||
message="The resource `@dimen/exo_styled_minimal_controls_margin_bottom` is marked as private in androidx.media3:media3-ui:1.3.0"
|
||||
errorLine1=" android:layout_marginBottom="@dimen/exo_styled_minimal_controls_margin_bottom""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
|
@ -230,7 +230,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="329"
|
||||
line="331"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -241,7 +241,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="387"
|
||||
line="389"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -252,7 +252,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="565"
|
||||
line="570"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -263,7 +263,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="785"
|
||||
line="790"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -274,7 +274,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -285,7 +285,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -296,7 +296,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -307,7 +307,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -318,7 +318,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -329,7 +329,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -340,7 +340,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -351,7 +351,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -362,7 +362,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="22"
|
||||
line="21"
|
||||
column="31"/>
|
||||
</issue>
|
||||
|
||||
|
@ -373,7 +373,7 @@
|
|||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="16"
|
||||
line="15"
|
||||
column="30"/>
|
||||
</issue>
|
||||
|
||||
|
@ -384,7 +384,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt"
|
||||
line="523"
|
||||
line="532"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -498,59 +498,15 @@
|
|||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ObsoleteSdkInt"
|
||||
message="Unnecessary; SDK_INT is always >= 24"
|
||||
errorLine1=" if (Build.VERSION.SDK_INT > 23) {"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt"
|
||||
line="288"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ObsoleteSdkInt"
|
||||
message="Unnecessary; SDK_INT is never < 24"
|
||||
errorLine1=" if (Build.VERSION.SDK_INT <= 23 || player == null) {"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt"
|
||||
line="297"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ObsoleteSdkInt"
|
||||
message="Unnecessary; SDK_INT is never < 24"
|
||||
errorLine1=" if (Build.VERSION.SDK_INT <= 23) {"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt"
|
||||
line="318"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ObsoleteSdkInt"
|
||||
message="Unnecessary; SDK_INT is always >= 24"
|
||||
errorLine1=" if (Build.VERSION.SDK_INT > 23) {"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt"
|
||||
line="330"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StringFormatTrivial"
|
||||
message="This formatting string is trivial. Rather than using `String.format` to create your String, it will be more performant to concatenate your arguments with `+`. "
|
||||
errorLine1=" (error) -> Log.e(TAG, String.format("Failed to %s account id %s", accept ? "accept" : "reject", id))"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
errorLine1=" (error) -> Log.e(TAG, String.format("Failed to %s account id %s", accept ? "accept" : "reject", id))"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
||||
line="807"
|
||||
column="61"/>
|
||||
line="808"
|
||||
column="49"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
|
@ -560,7 +516,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
||||
line="828"
|
||||
line="829"
|
||||
column="61"/>
|
||||
</issue>
|
||||
|
||||
|
@ -611,12 +567,12 @@
|
|||
<issue
|
||||
id="ReportShortcutUsage"
|
||||
message="Calling this method indicates use of dynamic shortcuts, but there are no calls to methods that track shortcut usage, such as `pushDynamicShortcut` or `reportShortcutUsed`. Calling these methods is recommended, as they track shortcut usage and allow launchers to adjust which shortcuts appear based on activation history. Please see https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#track-usage"
|
||||
errorLine1=" ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
errorLine1=" ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt"
|
||||
line="91"
|
||||
column="9"/>
|
||||
line="96"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
|
@ -1165,7 +1121,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
||||
line="200"
|
||||
line="196"
|
||||
column="19"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1176,7 +1132,7 @@
|
|||
errorLine2=" ~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
||||
line="789"
|
||||
line="791"
|
||||
column="38"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1187,7 +1143,7 @@
|
|||
errorLine2=" ~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
||||
line="794"
|
||||
line="796"
|
||||
column="40"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1198,7 +1154,7 @@
|
|||
errorLine2=" ~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
||||
line="799"
|
||||
line="801"
|
||||
column="58"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1209,7 +1165,7 @@
|
|||
errorLine2=" ~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
||||
line="812"
|
||||
line="813"
|
||||
column="47"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1220,7 +1176,7 @@
|
|||
errorLine2=" ~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
||||
line="827"
|
||||
line="828"
|
||||
column="30"/>
|
||||
</issue>
|
||||
|
||||
|
|
|
@ -39,34 +39,6 @@
|
|||
|
||||
# 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
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
|
|
@ -19,7 +19,7 @@ class MigrationsTest {
|
|||
@Rule
|
||||
var helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName,
|
||||
AppDatabase::class.java.canonicalName!!,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class EmojiAdapter(
|
|||
private val animate: Boolean
|
||||
) : 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) }
|
||||
|
||||
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.loadAvatar
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import java.util.Date
|
||||
|
||||
class ReportNotificationViewHolder(
|
||||
private val binding: ItemReportNotificationBinding
|
||||
|
@ -54,7 +53,7 @@ class ReportNotificationViewHolder(
|
|||
|
||||
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
|
||||
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)
|
||||
|
||||
// Fancy avatar inset
|
||||
|
|
|
@ -815,7 +815,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
setTranslationStatus(status, listener);
|
||||
|
||||
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
||||
setRebloggingEnabled(actionable.isRebloggingAllowed(), actionable.getVisibility());
|
||||
|
||||
setSpoilerAndContent(status, statusDisplayOptions, listener);
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.keylesspalace.tusky.appstore
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import javax.inject.Inject
|
||||
|
@ -13,8 +12,7 @@ import kotlinx.coroutines.launch
|
|||
class CacheUpdater @Inject constructor(
|
||||
eventHub: EventHub,
|
||||
accountManager: AccountManager,
|
||||
appDatabase: AppDatabase,
|
||||
gson: Gson
|
||||
appDatabase: AppDatabase
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
@ -30,8 +28,7 @@ class CacheUpdater @Inject constructor(
|
|||
val status = event.status
|
||||
timelineDao.update(
|
||||
accountId = accountId,
|
||||
status = status,
|
||||
gson = gson
|
||||
status = status
|
||||
)
|
||||
}
|
||||
is UnfollowEvent ->
|
||||
|
@ -39,8 +36,7 @@ class CacheUpdater @Inject constructor(
|
|||
is StatusDeletedEvent ->
|
||||
timelineDao.delete(accountId, event.statusId)
|
||||
is PollVoteEvent -> {
|
||||
val pollString = gson.toJson(event.poll)
|
||||
timelineDao.setVoted(accountId, event.statusId, pollString)
|
||||
timelineDao.setVoted(accountId, event.statusId, event.poll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@ interface Event
|
|||
@Singleton
|
||||
class EventHub @Inject constructor() {
|
||||
|
||||
private val sharedEventFlow = MutableSharedFlow<Event>()
|
||||
val events: SharedFlow<Event> = sharedEventFlow.asSharedFlow()
|
||||
private val _events = MutableSharedFlow<Event>()
|
||||
val events: SharedFlow<Event> = _events.asSharedFlow()
|
||||
|
||||
suspend fun dispatch(event: Event) {
|
||||
sharedEventFlow.emit(event)
|
||||
_events.emit(event)
|
||||
}
|
||||
|
||||
// TODO remove as soon as NotificationsFragment is Kotlin
|
||||
|
|
|
@ -514,8 +514,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
)
|
||||
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||
|
||||
accountFieldAdapter.fields = account.fields.orEmpty()
|
||||
accountFieldAdapter.emojis = account.emojis.orEmpty()
|
||||
accountFieldAdapter.fields = account.fields
|
||||
accountFieldAdapter.emojis = account.emojis
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
|
||||
binding.accountLockedImageView.visible(account.locked)
|
||||
|
@ -656,7 +656,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
*/
|
||||
private fun updateRemoteAccount() {
|
||||
loadedAccount?.let { account ->
|
||||
if (account.isRemote()) {
|
||||
if (account.isRemote) {
|
||||
binding.accountRemoveView.show()
|
||||
binding.accountRemoveView.setOnClickListener {
|
||||
openLink(account.url)
|
||||
|
@ -1084,7 +1084,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
}
|
||||
|
||||
private fun getFullUsername(account: Account): String {
|
||||
return if (account.isRemote()) {
|
||||
return if (account.isRemote) {
|
||||
"@" + account.username
|
||||
} else {
|
||||
val localUsername = account.localUsername
|
||||
|
|
|
@ -21,9 +21,13 @@ import com.keylesspalace.tusky.util.Success
|
|||
import com.keylesspalace.tusky.util.getDomain
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -42,8 +46,8 @@ class AccountViewModel @Inject constructor(
|
|||
private val _noteSaved = MutableStateFlow(false)
|
||||
val noteSaved: StateFlow<Boolean> = _noteSaved.asStateFlow()
|
||||
|
||||
private val _isRefreshing = MutableStateFlow(false)
|
||||
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
|
||||
private val _isRefreshing = MutableSharedFlow<Boolean>(1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val isRefreshing: SharedFlow<Boolean> = _isRefreshing.asSharedFlow()
|
||||
|
||||
private var isDataLoading = false
|
||||
|
||||
|
@ -84,13 +88,13 @@ class AccountViewModel @Inject constructor(
|
|||
|
||||
_accountData.value = Success(account)
|
||||
isDataLoading = false
|
||||
_isRefreshing.value = false
|
||||
_isRefreshing.emit(false)
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining account", t)
|
||||
_accountData.value = Error(cause = t)
|
||||
isDataLoading = false
|
||||
_isRefreshing.value = false
|
||||
_isRefreshing.emit(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -53,13 +54,13 @@ class ListsForAccountViewModel @Inject constructor(
|
|||
) : ViewModel() {
|
||||
|
||||
private val _states = MutableSharedFlow<List<AccountListState>>(1)
|
||||
val states: SharedFlow<List<AccountListState>> = _states
|
||||
val states: SharedFlow<List<AccountListState>> = _states.asSharedFlow()
|
||||
|
||||
private val _loadError = MutableSharedFlow<Throwable>(1)
|
||||
val loadError: SharedFlow<Throwable> = _loadError
|
||||
val loadError: SharedFlow<Throwable> = _loadError.asSharedFlow()
|
||||
|
||||
private val _actionError = MutableSharedFlow<ActionError>(1)
|
||||
val actionError: SharedFlow<ActionError> = _actionError
|
||||
val actionError: SharedFlow<ActionError> = _actionError.asSharedFlow()
|
||||
|
||||
fun load(accountId: String?) {
|
||||
_loadError.resetReplayCache()
|
||||
|
|
|
@ -21,7 +21,7 @@ import com.keylesspalace.tusky.util.getFormattedDescription
|
|||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import java.util.Random
|
||||
import kotlin.random.Random
|
||||
|
||||
class AccountMediaGridAdapter(
|
||||
private val useBlurhash: Boolean,
|
||||
|
@ -60,7 +60,6 @@ class AccountMediaGridAdapter(
|
|||
)
|
||||
|
||||
private val itemBgBaseHSV = FloatArray(3)
|
||||
private val random = Random()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
|
@ -72,7 +71,7 @@ class AccountMediaGridAdapter(
|
|||
false
|
||||
)
|
||||
Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV)
|
||||
itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f
|
||||
itemBgBaseHSV[2] = itemBgBaseHSV[2] + Random.nextFloat() / 3f - 1f / 6f
|
||||
binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
|
|
@ -145,12 +145,12 @@ class ComposeAutoCompleteAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
sealed class AutocompleteResult {
|
||||
class AccountResult(val account: TimelineAccount) : AutocompleteResult()
|
||||
sealed interface AutocompleteResult {
|
||||
class AccountResult(val account: TimelineAccount) : AutocompleteResult
|
||||
|
||||
class HashtagResult(val hashtag: String) : AutocompleteResult()
|
||||
class HashtagResult(val hashtag: String) : AutocompleteResult
|
||||
|
||||
class EmojiResult(val emoji: Emoji) : AutocompleteResult()
|
||||
class EmojiResult(val emoji: Emoji) : AutocompleteResult
|
||||
}
|
||||
|
||||
interface AutocompletionProvider {
|
||||
|
|
|
@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
|
@ -107,12 +108,12 @@ class ComposeViewModel @Inject constructor(
|
|||
private val _media = MutableStateFlow(emptyList<QueuedMedia>())
|
||||
val media: StateFlow<List<QueuedMedia>> = _media.asStateFlow()
|
||||
|
||||
val uploadError =
|
||||
MutableSharedFlow<Throwable>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
private val _uploadError = MutableSharedFlow<Throwable>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val uploadError: SharedFlow<Throwable> = _uploadError.asSharedFlow()
|
||||
|
||||
private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE)
|
||||
val closeConfirmation: StateFlow<ConfirmationKind> = _closeConfirmation.asStateFlow()
|
||||
|
@ -202,7 +203,7 @@ class ComposeViewModel @Inject constructor(
|
|||
)
|
||||
is UploadEvent.ErrorEvent -> {
|
||||
_media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
||||
uploadError.emit(event.error)
|
||||
_uploadError.emit(event.error)
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
|
@ -385,7 +386,7 @@ class ComposeViewModel @Inject constructor(
|
|||
val tootToSend = StatusToSend(
|
||||
text = content,
|
||||
warningText = spoilerText,
|
||||
visibility = _statusVisibility.value.serverString(),
|
||||
visibility = _statusVisibility.value.serverString,
|
||||
sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value),
|
||||
media = attachedMedia,
|
||||
scheduledAt = _scheduledAt.value,
|
||||
|
|
|
@ -42,7 +42,7 @@ fun downsizeImage(
|
|||
tempFile: File
|
||||
): Boolean {
|
||||
val decodeBoundsInputStream = try {
|
||||
contentResolver.openInputStream(uri)
|
||||
contentResolver.openInputStream(uri) ?: return false
|
||||
} catch (e: FileNotFoundException) {
|
||||
return false
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ fun downsizeImage(
|
|||
return false
|
||||
}
|
||||
val decodeBitmapInputStream = try {
|
||||
contentResolver.openInputStream(uri)
|
||||
contentResolver.openInputStream(uri) ?: return false
|
||||
} catch (e: FileNotFoundException) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.media.MediaMetadataRetriever
|
||||
|
@ -31,7 +30,7 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
||||
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||
import com.keylesspalace.tusky.network.asRequestBody
|
||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||
import com.keylesspalace.tusky.util.getImageSquarePixels
|
||||
import com.keylesspalace.tusky.util.getMediaSize
|
||||
|
@ -41,7 +40,6 @@ import java.io.File
|
|||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -64,13 +62,13 @@ import retrofit2.HttpException
|
|||
|
||||
sealed interface FinalUploadEvent
|
||||
|
||||
sealed class UploadEvent {
|
||||
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
||||
sealed interface UploadEvent {
|
||||
data class ProgressEvent(val percentage: Int) : UploadEvent
|
||||
data class FinishedEvent(
|
||||
val mediaId: String,
|
||||
val processed: Boolean
|
||||
) : UploadEvent(), FinalUploadEvent
|
||||
data class ErrorEvent(val error: Throwable) : UploadEvent(), FinalUploadEvent
|
||||
) : UploadEvent, FinalUploadEvent
|
||||
data class ErrorEvent(val error: Throwable) : UploadEvent, FinalUploadEvent
|
||||
}
|
||||
|
||||
data class UploadData(
|
||||
|
@ -246,7 +244,6 @@ class MediaUploader @Inject constructor(
|
|||
|
||||
private val contentResolver = context.contentResolver
|
||||
|
||||
@SuppressLint("Recycle") // stream is closed in ProgressRequestBody
|
||||
private suspend fun upload(media: QueuedMedia): Flow<UploadEvent> {
|
||||
return callbackFlow {
|
||||
var mimeType = contentResolver.getType(media.uri)
|
||||
|
@ -265,22 +262,20 @@ class MediaUploader @Inject constructor(
|
|||
}
|
||||
val map = MimeTypeMap.getSingleton()
|
||||
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
||||
val filename = "%s_%s_%s.%s".format(
|
||||
val filename = "%s_%d_%s.%s".format(
|
||||
context.getString(R.string.app_name),
|
||||
Date().time.toString(),
|
||||
System.currentTimeMillis(),
|
||||
randomAlphanumericString(10),
|
||||
fileExtension
|
||||
)
|
||||
|
||||
val stream = contentResolver.openInputStream(media.uri)
|
||||
|
||||
if (mimeType == null) mimeType = "multipart/form-data"
|
||||
|
||||
var lastProgress = -1
|
||||
val fileBody = ProgressRequestBody(
|
||||
stream!!,
|
||||
media.mediaSize,
|
||||
mimeType.toMediaTypeOrNull()!!
|
||||
val fileBody = media.uri.asRequestBody(
|
||||
contentResolver,
|
||||
requireNotNull(mimeType.toMediaTypeOrNull()) { "Invalid Content Type" },
|
||||
media.mediaSize
|
||||
) { percentage ->
|
||||
if (percentage != lastProgress) {
|
||||
trySend(UploadEvent.ProgressEvent(percentage))
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.entity.Poll
|
|||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@Entity(primaryKeys = ["id", "accountId"])
|
||||
|
@ -50,6 +51,7 @@ data class ConversationEntity(
|
|||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ConversationAccountEntity(
|
||||
val id: String,
|
||||
val localUsername: String,
|
||||
|
@ -131,7 +133,7 @@ data class ConversationStatusEntity(
|
|||
poll = poll,
|
||||
card = null,
|
||||
language = language,
|
||||
filtered = null
|
||||
filtered = emptyList()
|
||||
),
|
||||
isExpanded = expanded,
|
||||
isShowingContent = showingHiddenContent,
|
||||
|
@ -172,7 +174,7 @@ fun Status.toEntity(expanded: Boolean, contentShowing: Boolean, contentCollapsed
|
|||
showingHiddenContent = contentShowing,
|
||||
expanded = expanded,
|
||||
collapsed = contentCollapsed,
|
||||
muted = muted ?: false,
|
||||
muted = muted,
|
||||
poll = poll,
|
||||
language = language
|
||||
)
|
||||
|
|
|
@ -29,7 +29,7 @@ data class ConversationViewData(
|
|||
accountId: Long,
|
||||
favourited: Boolean = lastStatus.status.favourited,
|
||||
bookmarked: Boolean = lastStatus.status.bookmarked,
|
||||
muted: Boolean = lastStatus.status.muted ?: false,
|
||||
muted: Boolean = lastStatus.status.muted,
|
||||
poll: Poll? = lastStatus.status.poll,
|
||||
expanded: Boolean = lastStatus.isExpanded,
|
||||
collapsed: Boolean = lastStatus.isCollapsed,
|
||||
|
@ -57,7 +57,7 @@ data class ConversationViewData(
|
|||
fun StatusViewData.Concrete.toConversationStatusEntity(
|
||||
favourited: Boolean = status.favourited,
|
||||
bookmarked: Boolean = status.bookmarked,
|
||||
muted: Boolean = status.muted ?: false,
|
||||
muted: Boolean = status.muted,
|
||||
poll: Poll? = status.poll,
|
||||
expanded: Boolean = isExpanded,
|
||||
collapsed: Boolean = isCollapsed,
|
||||
|
|
|
@ -300,7 +300,7 @@ class ConversationsFragment :
|
|||
val popup = PopupMenu(requireContext(), view)
|
||||
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)
|
||||
} else {
|
||||
popup.menu.removeItem(R.id.status_unmute_conversation)
|
||||
|
|
|
@ -159,12 +159,12 @@ class ConversationsViewModel @Inject constructor(
|
|||
try {
|
||||
timelineCases.muteConversation(
|
||||
conversation.lastStatus.id,
|
||||
!(conversation.lastStatus.status.muted ?: false)
|
||||
!conversation.lastStatus.status.muted
|
||||
)
|
||||
|
||||
val newConversation = conversation.toEntity(
|
||||
accountId = accountManager.activeAccount!!.id,
|
||||
muted = !(conversation.lastStatus.status.muted ?: false)
|
||||
muted = !conversation.lastStatus.status.muted
|
||||
)
|
||||
|
||||
database.conversationDao().insert(newConversation)
|
||||
|
|
|
@ -10,6 +10,8 @@ import at.connyduck.calladapter.networkresult.onFailure
|
|||
import com.keylesspalace.tusky.R
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DomainBlocksViewModel @Inject constructor(
|
||||
|
@ -18,12 +20,13 @@ class DomainBlocksViewModel @Inject constructor(
|
|||
|
||||
val domainPager = repo.domainPager.cachedIn(viewModelScope)
|
||||
|
||||
val uiEvents = MutableSharedFlow<SnackbarEvent>()
|
||||
private val _uiEvents = MutableSharedFlow<SnackbarEvent>()
|
||||
val uiEvents: SharedFlow<SnackbarEvent> = _uiEvents.asSharedFlow()
|
||||
|
||||
fun block(domain: String) {
|
||||
viewModelScope.launch {
|
||||
repo.block(domain).onFailure { e ->
|
||||
uiEvents.emit(
|
||||
_uiEvents.emit(
|
||||
SnackbarEvent(
|
||||
message = R.string.error_blocking_domain,
|
||||
domain = domain,
|
||||
|
@ -39,7 +42,7 @@ class DomainBlocksViewModel @Inject constructor(
|
|||
fun unblock(domain: String) {
|
||||
viewModelScope.launch {
|
||||
repo.unblock(domain).fold({
|
||||
uiEvents.emit(
|
||||
_uiEvents.emit(
|
||||
SnackbarEvent(
|
||||
message = R.string.confirmation_domain_unmuted,
|
||||
domain = domain,
|
||||
|
@ -49,7 +52,7 @@ class DomainBlocksViewModel @Inject constructor(
|
|||
)
|
||||
)
|
||||
}, { e ->
|
||||
uiEvents.emit(
|
||||
_uiEvents.emit(
|
||||
SnackbarEvent(
|
||||
message = R.string.error_unblocking_domain,
|
||||
domain = domain,
|
||||
|
|
|
@ -101,16 +101,17 @@ class DraftHelper @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val attachments: MutableList<DraftAttachment> = mutableListOf()
|
||||
for (i in mediaUris.indices) {
|
||||
attachments.add(
|
||||
DraftAttachment(
|
||||
uriString = uris[i].toString(),
|
||||
description = mediaDescriptions[i],
|
||||
focus = mediaFocus[i],
|
||||
type = types[i]
|
||||
val attachments: List<DraftAttachment> = buildList(mediaUris.size) {
|
||||
for (i in mediaUris.indices) {
|
||||
add(
|
||||
DraftAttachment(
|
||||
uriString = uris[i].toString(),
|
||||
description = mediaDescriptions[i],
|
||||
focus = mediaFocus[i],
|
||||
type = types[i]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val draft = DraftEntity(
|
||||
|
|
|
@ -11,8 +11,9 @@ import com.keylesspalace.tusky.entity.Filter
|
|||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FiltersViewModel @Inject constructor(
|
||||
|
@ -30,8 +31,8 @@ class FiltersViewModel @Inject constructor(
|
|||
|
||||
data class State(val filters: List<Filter>, val loadingState: LoadingState)
|
||||
|
||||
val state: Flow<State> get() = _state
|
||||
private val _state = MutableStateFlow(State(emptyList(), LoadingState.INITIAL))
|
||||
val state: StateFlow<State> = _state.asStateFlow()
|
||||
|
||||
fun load() {
|
||||
this@FiltersViewModel._state.value = _state.value.copy(loadingState = LoadingState.LOADING)
|
||||
|
|
|
@ -91,15 +91,15 @@ data class LoginData(
|
|||
val oauthRedirectUrl: Uri
|
||||
) : Parcelable
|
||||
|
||||
sealed class LoginResult : Parcelable {
|
||||
sealed interface LoginResult : Parcelable {
|
||||
@Parcelize
|
||||
data class Ok(val code: String) : LoginResult()
|
||||
data class Ok(val code: String) : LoginResult
|
||||
|
||||
@Parcelize
|
||||
data class Err(val errorMessage: String) : LoginResult()
|
||||
data class Err(val errorMessage: String) : LoginResult
|
||||
|
||||
@Parcelize
|
||||
data object Cancel : LoginResult()
|
||||
data object Cancel : LoginResult
|
||||
}
|
||||
|
||||
/** Activity to do Oauth process using WebView. */
|
||||
|
|
|
@ -39,15 +39,15 @@ class LoginWebViewViewModel @Inject constructor(
|
|||
if (this.domain == null) {
|
||||
this.domain = domain
|
||||
viewModelScope.launch {
|
||||
api.getInstance().fold(
|
||||
api.getInstance(domain).fold(
|
||||
{ instance ->
|
||||
_instanceRules.value = instance.rules.orEmpty().map { rule -> rule.text }
|
||||
_instanceRules.value = instance.rules.map { rule -> rule.text }
|
||||
},
|
||||
{ throwable ->
|
||||
if (throwable.isHttpNotFound()) {
|
||||
api.getInstanceV1(domain).fold(
|
||||
{ instance ->
|
||||
_instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty()
|
||||
_instanceRules.value = instance.rules.map { rule -> rule.text }
|
||||
},
|
||||
{ throwable ->
|
||||
Log.w(
|
||||
|
|
|
@ -840,7 +840,7 @@ public class NotificationHelper {
|
|||
PollOption option = options.get(i);
|
||||
builder.append(buildDescription(option.getTitle(),
|
||||
PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()),
|
||||
poll.getOwnVotes() != null && poll.getOwnVotes().contains(i),
|
||||
poll.getOwnVotes().contains(i),
|
||||
context));
|
||||
builder.append('\n');
|
||||
}
|
||||
|
|
|
@ -180,7 +180,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
key = PrefKeys.DEFAULT_POST_PRIVACY
|
||||
setSummaryProvider { entry }
|
||||
val visibility = accountManager.activeAccount?.defaultPostPrivacy ?: Status.Visibility.PUBLIC
|
||||
value = visibility.serverString()
|
||||
value = visibility.serverString
|
||||
setIcon(getIconForVisibility(visibility))
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
setIcon(
|
||||
|
|
|
@ -284,7 +284,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
|||
Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> {
|
||||
val textId =
|
||||
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)
|
||||
}
|
||||
|
@ -320,7 +320,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
|||
}
|
||||
if (mutable) {
|
||||
muteConversationItem.setTitle(
|
||||
if (status.muted == true) {
|
||||
if (status.muted) {
|
||||
R.string.action_unmute_conversation
|
||||
} else {
|
||||
R.string.action_mute_conversation
|
||||
|
@ -392,7 +392,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
|||
|
||||
R.id.status_mute_conversation -> {
|
||||
searchAdapter.peek(position)?.let { foundStatus ->
|
||||
viewModel.muteConversation(foundStatus, status.muted != true)
|
||||
viewModel.muteConversation(foundStatus, !status.muted)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
@ -438,7 +438,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
|||
}
|
||||
|
||||
R.id.pin -> {
|
||||
viewModel.pinAccount(status, !status.isPinned())
|
||||
viewModel.pinAccount(status, !status.pinned)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
||||
|
@ -562,7 +562,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
|||
{ deletedStatus ->
|
||||
removeItem(position)
|
||||
|
||||
val redraftStatus = if (deletedStatus.isEmpty()) {
|
||||
val redraftStatus = if (deletedStatus.isEmpty) {
|
||||
status.toDeletedStatus()
|
||||
} else {
|
||||
deletedStatus
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
package com.keylesspalace.tusky.components.timeline
|
||||
|
||||
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.TimelineStatusEntity
|
||||
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.viewdata.StatusViewData
|
||||
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import java.util.Date
|
||||
|
||||
private const val TAG = "TimelineTypeMappers"
|
||||
|
@ -39,12 +41,7 @@ data class Placeholder(
|
|||
val loading: Boolean
|
||||
)
|
||||
|
||||
private val attachmentArrayListType = object : TypeToken<ArrayList<Attachment>>() {}.type
|
||||
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 {
|
||||
fun TimelineAccount.toEntity(accountId: Long, moshi: Moshi): TimelineAccountEntity {
|
||||
return TimelineAccountEntity(
|
||||
serverId = id,
|
||||
timelineUserId = accountId,
|
||||
|
@ -53,12 +50,12 @@ fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity
|
|||
displayName = name,
|
||||
url = url,
|
||||
avatar = avatar,
|
||||
emojis = gson.toJson(emojis),
|
||||
emojis = moshi.adapter<List<Emoji>>().toJson(emojis),
|
||||
bot = bot
|
||||
)
|
||||
}
|
||||
|
||||
fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
||||
fun TimelineAccountEntity.toAccount(moshi: Moshi): TimelineAccount {
|
||||
return TimelineAccount(
|
||||
id = serverId,
|
||||
localUsername = localUsername,
|
||||
|
@ -68,7 +65,7 @@ fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
|||
url = url,
|
||||
avatar = avatar,
|
||||
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,
|
||||
repliesCount = 0,
|
||||
language = null,
|
||||
filtered = null
|
||||
filtered = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
fun Status.toEntity(
|
||||
timelineUserId: Long,
|
||||
gson: Gson,
|
||||
moshi: Moshi,
|
||||
expanded: Boolean,
|
||||
contentShowing: Boolean,
|
||||
contentCollapsed: Boolean
|
||||
|
@ -128,7 +125,7 @@ fun Status.toEntity(
|
|||
content = actionableStatus.content,
|
||||
createdAt = actionableStatus.createdAt.time,
|
||||
editedAt = actionableStatus.editedAt?.time,
|
||||
emojis = actionableStatus.emojis.let(gson::toJson),
|
||||
emojis = actionableStatus.emojis.let { moshi.adapter<List<Emoji>>().toJson(it) },
|
||||
reblogsCount = actionableStatus.reblogsCount,
|
||||
favouritesCount = actionableStatus.favouritesCount,
|
||||
reblogged = actionableStatus.reblogged,
|
||||
|
@ -137,44 +134,44 @@ fun Status.toEntity(
|
|||
sensitive = actionableStatus.sensitive,
|
||||
spoilerText = actionableStatus.spoilerText,
|
||||
visibility = actionableStatus.visibility,
|
||||
attachments = actionableStatus.attachments.let(gson::toJson),
|
||||
mentions = actionableStatus.mentions.let(gson::toJson),
|
||||
tags = actionableStatus.tags.let(gson::toJson),
|
||||
application = actionableStatus.application.let(gson::toJson),
|
||||
attachments = actionableStatus.attachments.let { moshi.adapter<List<Attachment>>().toJson(it) },
|
||||
mentions = actionableStatus.mentions.let { moshi.adapter<List<Status.Mention>>().toJson(it) },
|
||||
tags = actionableStatus.tags.let { moshi.adapter<List<HashTag>?>().toJson(it) },
|
||||
application = actionableStatus.application.let { moshi.adapter<Status.Application?>().toJson(it) },
|
||||
reblogServerId = reblog?.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,
|
||||
expanded = expanded,
|
||||
contentShowing = contentShowing,
|
||||
contentCollapsed = contentCollapsed,
|
||||
pinned = actionableStatus.pinned == true,
|
||||
card = actionableStatus.card?.let(gson::toJson),
|
||||
pinned = actionableStatus.pinned,
|
||||
card = actionableStatus.card?.let { moshi.adapter<Card>().toJson(it) },
|
||||
repliesCount = actionableStatus.repliesCount,
|
||||
language = actionableStatus.language,
|
||||
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) {
|
||||
Log.d(TAG, "Constructing 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 mentions: List<Status.Mention> = gson.fromJson(status.mentions, mentionListType) ?: emptyList()
|
||||
val tags: List<HashTag>? = gson.fromJson(status.tags, tagListType)
|
||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||
val emojis: List<Emoji> = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
|
||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||
val card: Card? = gson.fromJson(status.card, Card::class.java)
|
||||
val attachments: List<Attachment> = status.attachments?.let { moshi.adapter<List<Attachment>?>().fromJson(it) }.orEmpty()
|
||||
val mentions: List<Status.Mention> = status.mentions?.let { moshi.adapter<List<Status.Mention>?>().fromJson(it) }.orEmpty()
|
||||
val tags: List<HashTag>? = status.tags?.let { moshi.adapter<List<HashTag>?>().fromJson(it) }
|
||||
val application = status.application?.let { moshi.adapter<Status.Application?>().fromJson(it) }
|
||||
val emojis: List<Emoji> = status.emojis?.let { moshi.adapter<List<Emoji>?>().fromJson(it) }.orEmpty()
|
||||
val poll: Poll? = status.poll?.let { moshi.adapter<Poll?>().fromJson(it) }
|
||||
val card: Card? = status.card?.let { moshi.adapter<Card?>().fromJson(it) }
|
||||
|
||||
val reblog = status.reblogServerId?.let { id ->
|
||||
Status(
|
||||
id = id,
|
||||
url = status.url,
|
||||
account = account.toAccount(gson),
|
||||
account = account.toAccount(moshi),
|
||||
inReplyToId = status.inReplyToId,
|
||||
inReplyToAccountId = status.inReplyToAccountId,
|
||||
reblog = null,
|
||||
|
@ -195,12 +192,12 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
|||
tags = tags,
|
||||
application = application,
|
||||
pinned = false,
|
||||
muted = status.muted,
|
||||
muted = status.muted ?: false,
|
||||
poll = poll,
|
||||
card = card,
|
||||
repliesCount = status.repliesCount,
|
||||
language = status.language,
|
||||
filtered = status.filtered,
|
||||
filtered = status.filtered.orEmpty(),
|
||||
)
|
||||
}
|
||||
val status = if (reblog != null) {
|
||||
|
@ -208,7 +205,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
|||
id = status.serverId,
|
||||
// no url for reblogs
|
||||
url = null,
|
||||
account = this.reblogAccount!!.toAccount(gson),
|
||||
account = this.reblogAccount!!.toAccount(moshi),
|
||||
inReplyToId = null,
|
||||
inReplyToAccountId = null,
|
||||
reblog = reblog,
|
||||
|
@ -216,7 +213,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
|||
// lie but whatever?
|
||||
createdAt = Date(status.createdAt),
|
||||
editedAt = null,
|
||||
emojis = listOf(),
|
||||
emojis = emptyList(),
|
||||
reblogsCount = 0,
|
||||
favouritesCount = 0,
|
||||
reblogged = false,
|
||||
|
@ -225,23 +222,23 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
|||
sensitive = false,
|
||||
spoilerText = "",
|
||||
visibility = status.visibility,
|
||||
attachments = ArrayList(),
|
||||
mentions = listOf(),
|
||||
tags = listOf(),
|
||||
attachments = emptyList(),
|
||||
mentions = emptyList(),
|
||||
tags = emptyList(),
|
||||
application = null,
|
||||
pinned = status.pinned,
|
||||
muted = status.muted,
|
||||
muted = status.muted ?: false,
|
||||
poll = null,
|
||||
card = null,
|
||||
repliesCount = status.repliesCount,
|
||||
language = status.language,
|
||||
filtered = status.filtered
|
||||
filtered = status.filtered.orEmpty()
|
||||
)
|
||||
} else {
|
||||
Status(
|
||||
id = status.serverId,
|
||||
url = status.url,
|
||||
account = account.toAccount(gson),
|
||||
account = account.toAccount(moshi),
|
||||
inReplyToId = status.inReplyToId,
|
||||
inReplyToAccountId = status.inReplyToAccountId,
|
||||
reblog = null,
|
||||
|
@ -262,12 +259,12 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson, isDetailed: Boolean = false
|
|||
tags = tags,
|
||||
application = application,
|
||||
pinned = status.pinned,
|
||||
muted = status.muted,
|
||||
muted = status.muted ?: false,
|
||||
poll = poll,
|
||||
card = card,
|
||||
repliesCount = status.repliesCount,
|
||||
language = status.language,
|
||||
filtered = status.filtered
|
||||
filtered = status.filtered.orEmpty()
|
||||
)
|
||||
}
|
||||
return StatusViewData.Concrete(
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package com.keylesspalace.tusky.components.timeline.util
|
||||
|
||||
import com.google.gson.JsonParseException
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import java.io.IOException
|
||||
import retrofit2.HttpException
|
||||
|
||||
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 {
|
||||
if (t.isExpected()) {
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.paging.LoadType
|
|||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.room.withTransaction
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||
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.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.squareup.moshi.Moshi
|
||||
import retrofit2.HttpException
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
|
@ -38,7 +38,7 @@ class CachedTimelineRemoteMediator(
|
|||
accountManager: AccountManager,
|
||||
private val api: MastodonApi,
|
||||
private val db: AppDatabase,
|
||||
private val gson: Gson
|
||||
private val moshi: Moshi
|
||||
) : RemoteMediator<Int, TimelineStatusWithAccount>() {
|
||||
|
||||
private var initialRefresh = false
|
||||
|
@ -143,8 +143,8 @@ class CachedTimelineRemoteMediator(
|
|||
}
|
||||
|
||||
for (status in statuses) {
|
||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
||||
status.reblog?.account?.toEntity(activeAccount.id, gson)?.let { rebloggedAccount ->
|
||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, moshi))
|
||||
status.reblog?.account?.toEntity(activeAccount.id, moshi)?.let { rebloggedAccount ->
|
||||
timelineDao.insertAccount(rebloggedAccount)
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ class CachedTimelineRemoteMediator(
|
|||
timelineDao.insertStatus(
|
||||
status.toEntity(
|
||||
timelineUserId = activeAccount.id,
|
||||
gson = gson,
|
||||
moshi = moshi,
|
||||
expanded = expanded,
|
||||
contentShowing = contentShowing,
|
||||
contentCollapsed = contentCollapsed
|
||||
|
|
|
@ -29,7 +29,6 @@ import androidx.room.withTransaction
|
|||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import at.connyduck.calladapter.networkresult.map
|
||||
import at.connyduck.calladapter.networkresult.onFailure
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.NEWEST_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.viewdata.StatusViewData
|
||||
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
||||
import com.squareup.moshi.Moshi
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
|
@ -69,7 +69,7 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
sharedPreferences: SharedPreferences,
|
||||
filterModel: FilterModel,
|
||||
private val db: AppDatabase,
|
||||
private val gson: Gson
|
||||
private val moshi: Moshi
|
||||
) : TimelineViewModel(
|
||||
timelineCases,
|
||||
api,
|
||||
|
@ -87,7 +87,7 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
@OptIn(ExperimentalPagingApi::class)
|
||||
override val statuses = Pager(
|
||||
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
||||
remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, gson),
|
||||
remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, moshi),
|
||||
pagingSourceFactory = {
|
||||
val activeAccount = accountManager.activeAccount
|
||||
if (activeAccount == null) {
|
||||
|
@ -108,7 +108,7 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus ->
|
||||
val translation = translations[timelineStatus.status.serverId]
|
||||
timelineStatus.toViewData(
|
||||
gson,
|
||||
moshi,
|
||||
isDetailed = false,
|
||||
translation = translation
|
||||
)
|
||||
|
@ -218,15 +218,15 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
for (status in statuses) {
|
||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
||||
status.reblog?.account?.toEntity(activeAccount.id, gson)
|
||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, moshi))
|
||||
status.reblog?.account?.toEntity(activeAccount.id, moshi)
|
||||
?.let { rebloggedAccount ->
|
||||
timelineDao.insertAccount(rebloggedAccount)
|
||||
}
|
||||
timelineDao.insertStatus(
|
||||
status.toEntity(
|
||||
timelineUserId = activeAccount.id,
|
||||
gson = gson,
|
||||
moshi = moshi,
|
||||
expanded = activeAccount.alwaysOpenSpoiler,
|
||||
contentShowing = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||
contentCollapsed = true
|
||||
|
|
|
@ -259,7 +259,7 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
|
||||
override fun clearWarning(status: StatusViewData.Concrete) {
|
||||
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 } }
|
||||
.toViewData()
|
||||
|
||||
val header = TrendingViewData.Header(firstTag.start(), firstTag.end())
|
||||
val header = TrendingViewData.Header(firstTag.start, firstTag.end)
|
||||
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.map
|
||||
import at.connyduck.calladapter.networkresult.onFailure
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
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.viewdata.StatusViewData
|
||||
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
||||
import com.squareup.moshi.Moshi
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -51,6 +51,8 @@ import kotlinx.coroutines.channels.BufferOverflow
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -62,20 +64,18 @@ class ViewThreadViewModel @Inject constructor(
|
|||
eventHub: EventHub,
|
||||
private val accountManager: AccountManager,
|
||||
private val db: AppDatabase,
|
||||
private val gson: Gson
|
||||
private val moshi: Moshi
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ThreadUiState.Loading as ThreadUiState)
|
||||
val uiState: Flow<ThreadUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val _errors =
|
||||
MutableSharedFlow<Throwable>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val errors: Flow<Throwable>
|
||||
get() = _errors
|
||||
private val _errors = MutableSharedFlow<Throwable>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val errors: SharedFlow<Throwable> = _errors.asSharedFlow()
|
||||
|
||||
var isInitialLoad: Boolean = true
|
||||
|
||||
|
@ -113,7 +113,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||
var detailedStatus = if (timelineStatus != null) {
|
||||
Log.d(TAG, "Loaded status from local timeline")
|
||||
val viewData = timelineStatus.toViewData(
|
||||
gson,
|
||||
moshi,
|
||||
isDetailed = true,
|
||||
) as StatusViewData.Concrete
|
||||
|
||||
|
@ -148,8 +148,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||
api.status(id).getOrNull()?.let { result ->
|
||||
db.timelineDao().update(
|
||||
accountId = accountManager.activeAccount!!.id,
|
||||
status = result,
|
||||
gson = gson
|
||||
status = result
|
||||
)
|
||||
detailedStatus = result.toViewData(isDetailed = true)
|
||||
}
|
||||
|
@ -520,7 +519,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||
|
||||
fun clearWarning(viewData: StatusViewData.Concrete) {
|
||||
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.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
||||
import com.keylesspalace.tusky.createTabDataFromId
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Card
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.FilterResult
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@ProvidedTypeConverter
|
||||
@Singleton
|
||||
class Converters @Inject constructor(
|
||||
private val gson: Gson
|
||||
private val moshi: Moshi
|
||||
) {
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToEmojiList(emojiListJson: String?): List<Emoji>? {
|
||||
return gson.fromJson(emojiListJson, object : TypeToken<List<Emoji>>() {}.type)
|
||||
fun jsonToEmojiList(emojiListJson: String?): List<Emoji> {
|
||||
return emojiListJson?.let { moshi.adapter<List<Emoji>?>().fromJson(it) }.orEmpty()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun emojiListToJson(emojiList: List<Emoji>?): String {
|
||||
return gson.toJson(emojiList)
|
||||
fun emojiListToJson(emojiList: List<Emoji>): String {
|
||||
return moshi.adapter<List<Emoji>>().toJson(emojiList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
|
@ -83,55 +85,52 @@ class Converters @Inject constructor(
|
|||
|
||||
@TypeConverter
|
||||
fun accountToJson(account: ConversationAccountEntity?): String {
|
||||
return gson.toJson(account)
|
||||
return moshi.adapter<ConversationAccountEntity?>().toJson(account)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccount(accountJson: String?): ConversationAccountEntity? {
|
||||
return gson.fromJson(accountJson, ConversationAccountEntity::class.java)
|
||||
return accountJson?.let { moshi.adapter<ConversationAccountEntity?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun accountListToJson(accountList: List<ConversationAccountEntity>?): String {
|
||||
return gson.toJson(accountList)
|
||||
fun accountListToJson(accountList: List<ConversationAccountEntity>): String {
|
||||
return moshi.adapter<List<ConversationAccountEntity>>().toJson(accountList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccountList(accountListJson: String?): List<ConversationAccountEntity>? {
|
||||
return gson.fromJson(
|
||||
accountListJson,
|
||||
object : TypeToken<List<ConversationAccountEntity>>() {}.type
|
||||
)
|
||||
fun jsonToAccountList(accountListJson: String?): List<ConversationAccountEntity> {
|
||||
return accountListJson?.let { moshi.adapter<List<ConversationAccountEntity>?>().fromJson(it) }.orEmpty()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun attachmentListToJson(attachmentList: List<Attachment>?): String {
|
||||
return gson.toJson(attachmentList)
|
||||
fun attachmentListToJson(attachmentList: List<Attachment>): String {
|
||||
return moshi.adapter<List<Attachment>>().toJson(attachmentList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAttachmentList(attachmentListJson: String?): List<Attachment>? {
|
||||
return gson.fromJson(attachmentListJson, object : TypeToken<List<Attachment>>() {}.type)
|
||||
fun jsonToAttachmentList(attachmentListJson: String?): List<Attachment> {
|
||||
return attachmentListJson?.let { moshi.adapter<List<Attachment>?>().fromJson(it) }.orEmpty()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun mentionListToJson(mentionArray: List<Status.Mention>?): String? {
|
||||
return gson.toJson(mentionArray)
|
||||
fun mentionListToJson(mentionArray: List<Status.Mention>): String {
|
||||
return moshi.adapter<List<Status.Mention>>().toJson(mentionArray)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToMentionArray(mentionListJson: String?): List<Status.Mention>? {
|
||||
return gson.fromJson(mentionListJson, object : TypeToken<List<Status.Mention>>() {}.type)
|
||||
fun jsonToMentionArray(mentionListJson: String?): List<Status.Mention> {
|
||||
return mentionListJson?.let { moshi.adapter<List<Status.Mention>?>().fromJson(it) }.orEmpty()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun tagListToJson(tagArray: List<HashTag>?): String? {
|
||||
return gson.toJson(tagArray)
|
||||
fun tagListToJson(tagArray: List<HashTag>?): String {
|
||||
return moshi.adapter<List<HashTag>?>().toJson(tagArray)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToTagArray(tagListJson: String?): List<HashTag>? {
|
||||
return gson.fromJson(tagListJson, object : TypeToken<List<HashTag>>() {}.type)
|
||||
return tagListJson?.let { moshi.adapter<List<HashTag>?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
|
@ -145,45 +144,47 @@ class Converters @Inject constructor(
|
|||
}
|
||||
|
||||
@TypeConverter
|
||||
fun pollToJson(poll: Poll?): String? {
|
||||
return gson.toJson(poll)
|
||||
fun pollToJson(poll: Poll?): String {
|
||||
return moshi.adapter<Poll?>().toJson(poll)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToPoll(pollJson: String?): Poll? {
|
||||
return gson.fromJson(pollJson, Poll::class.java)
|
||||
return pollJson?.let { moshi.adapter<Poll?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun newPollToJson(newPoll: NewPoll?): String? {
|
||||
return gson.toJson(newPoll)
|
||||
fun newPollToJson(newPoll: NewPoll?): String {
|
||||
return moshi.adapter<NewPoll?>().toJson(newPoll)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToNewPoll(newPollJson: String?): NewPoll? {
|
||||
return gson.fromJson(newPollJson, NewPoll::class.java)
|
||||
return newPollJson?.let { moshi.adapter<NewPoll?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>?): String? {
|
||||
return gson.toJson(draftAttachments)
|
||||
fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>): String {
|
||||
return moshi.adapter<List<DraftAttachment>>().toJson(draftAttachments)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List<DraftAttachment>? {
|
||||
return gson.fromJson(
|
||||
draftAttachmentListJson,
|
||||
object : TypeToken<List<DraftAttachment>>() {}.type
|
||||
)
|
||||
fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List<DraftAttachment> {
|
||||
return draftAttachmentListJson?.let { moshi.adapter<List<DraftAttachment>?>().fromJson(it) }.orEmpty()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun filterResultListToJson(filterResults: List<FilterResult>?): String? {
|
||||
return gson.toJson(filterResults)
|
||||
fun filterResultListToJson(filterResults: List<FilterResult>?): String {
|
||||
return moshi.adapter<List<FilterResult>?>().toJson(filterResults)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToFilterResultList(filterResultListJson: String?): List<FilterResult>? {
|
||||
return gson.fromJson(filterResultListJson, object : TypeToken<List<FilterResult>>() {}.type)
|
||||
return filterResultListJson?.let { moshi.adapter<List<FilterResult>?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun 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.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity
|
||||
|
@ -46,21 +46,18 @@ data class DraftEntity(
|
|||
val statusId: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* The alternate names are here because we accidentally published versions were DraftAttachment was minified
|
||||
* Tusky 15: uriString = e, description = f, type = g
|
||||
* Tusky 16 beta: uriString = i, description = j, type = k
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class DraftAttachment(
|
||||
@SerializedName(value = "uriString", alternate = ["e", "i"]) val uriString: String,
|
||||
@SerializedName(value = "description", alternate = ["f", "j"]) val description: String?,
|
||||
@SerializedName(value = "focus") val focus: Attachment.Focus?,
|
||||
@SerializedName(value = "type", alternate = ["g", "k"]) val type: Type
|
||||
val uriString: String,
|
||||
val description: String?,
|
||||
val focus: Attachment.Focus?,
|
||||
val type: Type
|
||||
) : Parcelable {
|
||||
val uri: Uri
|
||||
get() = uriString.toUri()
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Type {
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
|
|
|
@ -21,7 +21,11 @@ import androidx.room.Insert
|
|||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||
import androidx.room.Query
|
||||
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
|
||||
|
||||
@Dao
|
||||
|
@ -89,13 +93,13 @@ AND
|
|||
)
|
||||
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(
|
||||
accountId = accountId,
|
||||
statusId = status.id,
|
||||
content = status.content,
|
||||
editedAt = status.editedAt?.time,
|
||||
emojis = gson.toJson(status.emojis),
|
||||
emojis = status.emojis,
|
||||
reblogsCount = status.reblogsCount,
|
||||
favouritesCount = status.favouritesCount,
|
||||
repliesCount = status.repliesCount,
|
||||
|
@ -105,13 +109,13 @@ AND
|
|||
sensitive = status.sensitive,
|
||||
spoilerText = status.spoilerText,
|
||||
visibility = status.visibility,
|
||||
attachments = gson.toJson(status.attachments),
|
||||
mentions = gson.toJson(status.mentions),
|
||||
tags = gson.toJson(status.tags),
|
||||
poll = gson.toJson(status.poll),
|
||||
attachments = status.attachments,
|
||||
mentions = status.mentions,
|
||||
tags = status.tags,
|
||||
poll = status.poll,
|
||||
muted = status.muted,
|
||||
pinned = status.pinned ?: false,
|
||||
card = gson.toJson(status.card),
|
||||
pinned = status.pinned,
|
||||
card = status.card,
|
||||
language = status.language
|
||||
)
|
||||
}
|
||||
|
@ -141,12 +145,12 @@ AND
|
|||
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract suspend fun update(
|
||||
protected abstract suspend fun update(
|
||||
accountId: Long,
|
||||
statusId: String,
|
||||
content: String?,
|
||||
editedAt: Long?,
|
||||
emojis: String?,
|
||||
emojis: List<Emoji>,
|
||||
reblogsCount: Int,
|
||||
favouritesCount: Int,
|
||||
repliesCount: Int,
|
||||
|
@ -156,13 +160,13 @@ AND
|
|||
sensitive: Boolean,
|
||||
spoilerText: String,
|
||||
visibility: Status.Visibility,
|
||||
attachments: String?,
|
||||
mentions: String?,
|
||||
tags: String?,
|
||||
poll: String?,
|
||||
attachments: List<Attachment>,
|
||||
mentions: List<Status.Mention>,
|
||||
tags: List<HashTag>?,
|
||||
poll: Poll?,
|
||||
muted: Boolean?,
|
||||
pinned: Boolean,
|
||||
card: String?,
|
||||
card: Card?,
|
||||
language: String?
|
||||
)
|
||||
|
||||
|
@ -243,7 +247,8 @@ AND serverId = :statusId"""
|
|||
"""UPDATE TimelineStatusEntity SET poll = :poll
|
||||
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(
|
||||
"""UPDATE TimelineStatusEntity SET expanded = :expanded
|
||||
|
|
|
@ -20,11 +20,12 @@ import android.content.SharedPreferences
|
|||
import android.os.Build
|
||||
import android.util.Log
|
||||
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.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.MastodonApi
|
||||
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.ProxyConfiguration
|
||||
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.Provides
|
||||
import java.net.IDN
|
||||
|
@ -46,7 +50,7 @@ import okhttp3.OkHttp
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.create
|
||||
|
||||
/**
|
||||
|
@ -54,13 +58,32 @@ import retrofit2.create
|
|||
*/
|
||||
|
||||
@Module
|
||||
class NetworkModule {
|
||||
object NetworkModule {
|
||||
|
||||
private const val TAG = "NetworkModule"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesGson(): Gson = GsonBuilder()
|
||||
.registerTypeAdapter(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.create()
|
||||
fun providesMoshi(): Moshi = Moshi.Builder()
|
||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.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
|
||||
@Singleton
|
||||
|
@ -113,10 +136,10 @@ class NetworkModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesRetrofit(httpClient: OkHttpClient, gson: Gson): Retrofit {
|
||||
fun providesRetrofit(httpClient: OkHttpClient, moshi: Moshi): Retrofit {
|
||||
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
|
||||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
@ -138,8 +161,4 @@ class NetworkModule {
|
|||
.build()
|
||||
.create()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NetworkModule"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,8 @@ object PlayerModule {
|
|||
context,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS,
|
||||
// enableDecoderFallback = true, helps playing videos even if one decoder fails
|
||||
true,
|
||||
eventHandler,
|
||||
videoRendererEventListener,
|
||||
DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY
|
||||
|
@ -77,6 +79,8 @@ object PlayerModule {
|
|||
MediaCodecAudioRenderer(
|
||||
context,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
// enableDecoderFallback = true
|
||||
true,
|
||||
eventHandler,
|
||||
audioRendererEventListener,
|
||||
audioSink
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccessToken(
|
||||
@SerializedName("access_token") val accessToken: String
|
||||
@Json(name = "access_token") val accessToken: String
|
||||
)
|
||||
|
|
|
@ -15,32 +15,34 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Account(
|
||||
val id: String,
|
||||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String,
|
||||
@Json(name = "acct") val username: String,
|
||||
// should never be null per Api definition, but some servers break the contract
|
||||
@SerializedName("display_name") val displayName: String?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@Json(name = "display_name") val displayName: String? = null,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
val note: String,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
val header: String,
|
||||
val locked: Boolean = false,
|
||||
@SerializedName("followers_count") val followersCount: Int = 0,
|
||||
@SerializedName("following_count") val followingCount: Int = 0,
|
||||
@SerializedName("statuses_count") val statusesCount: Int = 0,
|
||||
@Json(name = "followers_count") val followersCount: Int = 0,
|
||||
@Json(name = "following_count") val followingCount: Int = 0,
|
||||
@Json(name = "statuses_count") val statusesCount: Int = 0,
|
||||
val source: AccountSource? = null,
|
||||
val bot: Boolean = false,
|
||||
// nullable for backward compatibility
|
||||
val emojis: List<Emoji>? = emptyList(),
|
||||
// nullable for backward compatibility
|
||||
val fields: List<Field>? = emptyList(),
|
||||
// default value for backward compatibility
|
||||
val emojis: List<Emoji> = emptyList(),
|
||||
// default value for backward compatibility
|
||||
val fields: List<Field> = emptyList(),
|
||||
val moved: Account? = null,
|
||||
val roles: List<Role>? = emptyList()
|
||||
val roles: List<Role> = emptyList()
|
||||
) {
|
||||
|
||||
val name: String
|
||||
|
@ -50,28 +52,33 @@ data class Account(
|
|||
displayName
|
||||
}
|
||||
|
||||
fun isRemote(): Boolean = this.username != this.localUsername
|
||||
val isRemote: Boolean
|
||||
get() = this.username != this.localUsername
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccountSource(
|
||||
val privacy: Status.Visibility?,
|
||||
val sensitive: Boolean?,
|
||||
val note: String?,
|
||||
val fields: List<StringField>?,
|
||||
val language: String?
|
||||
val privacy: Status.Visibility = Status.Visibility.PUBLIC,
|
||||
val sensitive: Boolean? = null,
|
||||
val note: String? = null,
|
||||
val fields: List<StringField> = emptyList(),
|
||||
val language: String? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Field(
|
||||
val name: String,
|
||||
val value: String,
|
||||
@SerializedName("verified_at") val verifiedAt: Date?
|
||||
@Json(name = "verified_at") val verifiedAt: Date? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StringField(
|
||||
val name: String,
|
||||
val value: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Role(
|
||||
val name: String,
|
||||
val color: String
|
||||
|
|
|
@ -15,18 +15,20 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Announcement(
|
||||
val id: String,
|
||||
val content: String,
|
||||
@SerializedName("starts_at") val startsAt: Date?,
|
||||
@SerializedName("ends_at") val endsAt: Date?,
|
||||
@SerializedName("all_day") val allDay: Boolean,
|
||||
@SerializedName("published_at") val publishedAt: Date,
|
||||
@SerializedName("updated_at") val updatedAt: Date,
|
||||
val read: Boolean,
|
||||
@Json(name = "starts_at") val startsAt: Date? = null,
|
||||
@Json(name = "ends_at") val endsAt: Date? = null,
|
||||
@Json(name = "all_day") val allDay: Boolean,
|
||||
@Json(name = "published_at") val publishedAt: Date,
|
||||
@Json(name = "updated_at") val updatedAt: Date,
|
||||
val read: Boolean = false,
|
||||
val mentions: List<Status.Mention>,
|
||||
val statuses: List<Status>,
|
||||
val tags: List<HashTag>,
|
||||
|
@ -36,21 +38,21 @@ data class Announcement(
|
|||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
if (other !is Announcement) return false
|
||||
|
||||
val announcement = other as Announcement?
|
||||
return id == announcement?.id
|
||||
return id == other.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Reaction(
|
||||
val name: String,
|
||||
val count: Int,
|
||||
val me: Boolean,
|
||||
val url: String?,
|
||||
@SerializedName("static_url") val staticUrl: String?
|
||||
val me: Boolean = false,
|
||||
val url: String? = null,
|
||||
@Json(name = "static_url") val staticUrl: String? = null
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,9 +15,11 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AppCredentials(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("client_secret") val clientSecret: String
|
||||
@Json(name = "client_id") val clientId: String,
|
||||
@Json(name = "client_secret") val clientSecret: String
|
||||
)
|
||||
|
|
|
@ -16,70 +16,50 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Attachment(
|
||||
val id: String,
|
||||
val url: String,
|
||||
// can be null for e.g. audio attachments
|
||||
@SerializedName("preview_url") val previewUrl: String?,
|
||||
val meta: MetaData?,
|
||||
@Json(name = "preview_url") val previewUrl: String? = null,
|
||||
val meta: MetaData? = null,
|
||||
val type: Type,
|
||||
val description: String?,
|
||||
val blurhash: String?
|
||||
val description: String? = null,
|
||||
val blurhash: String? = null
|
||||
) : Parcelable {
|
||||
|
||||
@JsonAdapter(MediaTypeDeserializer::class)
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Type {
|
||||
@SerializedName("image")
|
||||
@Json(name = "image")
|
||||
IMAGE,
|
||||
|
||||
@SerializedName("gifv")
|
||||
@Json(name = "gifv")
|
||||
GIFV,
|
||||
|
||||
@SerializedName("video")
|
||||
@Json(name = "video")
|
||||
VIDEO,
|
||||
|
||||
@SerializedName("audio")
|
||||
@Json(name = "audio")
|
||||
AUDIO,
|
||||
|
||||
@SerializedName("unknown")
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
class MediaTypeDeserializer : JsonDeserializer<Type> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
classOfT: java.lang.reflect.Type,
|
||||
context: JsonDeserializationContext
|
||||
): Type {
|
||||
return when (json.toString()) {
|
||||
"\"image\"" -> Type.IMAGE
|
||||
"\"gifv\"" -> Type.GIFV
|
||||
"\"video\"" -> Type.VIDEO
|
||||
"\"audio\"" -> Type.AUDIO
|
||||
else -> Type.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The meta data of an [Attachment].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class MetaData(
|
||||
val focus: Focus?,
|
||||
val duration: Float?,
|
||||
val original: Size?,
|
||||
val small: Size?
|
||||
val focus: Focus? = null,
|
||||
val duration: Float? = null,
|
||||
val original: Size? = null,
|
||||
val small: Size? = null
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
|
@ -88,6 +68,7 @@ data class Attachment(
|
|||
* See here for more details what the x and y mean:
|
||||
* https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Focus(
|
||||
val x: Float,
|
||||
|
@ -99,10 +80,11 @@ data class Attachment(
|
|||
/**
|
||||
* The size of an image, used to specify the width/height.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Size(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val aspect: Double
|
||||
val width: Int = 0,
|
||||
val height: Int = 0,
|
||||
val aspect: Double = 0.0
|
||||
) : Parcelable
|
||||
}
|
||||
|
|
|
@ -15,19 +15,21 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Card(
|
||||
val url: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
@SerializedName("author_name") val authorName: String,
|
||||
val image: String,
|
||||
@Json(name = "author_name") val authorName: String,
|
||||
val image: String? = null,
|
||||
val type: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val blurhash: String?,
|
||||
@SerializedName("embed_url") val embedUrl: String?
|
||||
val blurhash: String? = null,
|
||||
@Json(name = "embed_url") val embedUrl: String? = null
|
||||
) {
|
||||
|
||||
override fun hashCode() = url.hashCode()
|
||||
|
@ -36,8 +38,7 @@ data class Card(
|
|||
if (other !is Card) {
|
||||
return false
|
||||
}
|
||||
val account = other as Card?
|
||||
return account?.url == this.url
|
||||
return other.url == this.url
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -15,12 +15,14 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Conversation(
|
||||
val id: String,
|
||||
val accounts: List<TimelineAccount>,
|
||||
// should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
|
||||
@SerializedName("last_status") val lastStatus: Status?,
|
||||
// should never be null, but apparently it's possible https://github.com/tuskyapp/Tusky/issues/1038
|
||||
@Json(name = "last_status") val lastStatus: Status? = null,
|
||||
val unread: Boolean
|
||||
)
|
||||
|
|
|
@ -15,21 +15,22 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DeletedStatus(
|
||||
val text: String?,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String? = null,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val visibility: Status.Visibility,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("media_attachments") val attachments: List<Attachment>?,
|
||||
val poll: Poll?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
val language: String?
|
||||
@Json(name = "media_attachments") val attachments: List<Attachment>,
|
||||
val poll: Poll? = null,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
val language: String? = null
|
||||
) {
|
||||
fun isEmpty(): Boolean {
|
||||
return text == null && attachments == null
|
||||
}
|
||||
val isEmpty: Boolean
|
||||
get() = text == null && attachments.isEmpty()
|
||||
}
|
||||
|
|
|
@ -16,13 +16,15 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Emoji(
|
||||
val shortcode: String,
|
||||
val url: String,
|
||||
@SerializedName("static_url") val staticUrl: String,
|
||||
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?
|
||||
@Json(name = "static_url") val staticUrl: String,
|
||||
@Json(name = "visible_in_picker") val visibleInPicker: Boolean = true
|
||||
) : Parcelable
|
||||
|
|
|
@ -17,8 +17,12 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** @see [Error](https://docs.joinmastodon.org/entities/Error/) */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Error(
|
||||
val error: String,
|
||||
val error_description: String?
|
||||
@Json(name = "error_description") val errorDescription: String? = null
|
||||
)
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Filter(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val context: List<String>,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@SerializedName("filter_action") private val filterAction: String,
|
||||
val keywords: List<FilterKeyword>
|
||||
@Json(name = "expires_at") val expiresAt: Date? = null,
|
||||
@Json(name = "filter_action") val filterAction: String,
|
||||
// This field is mandatory according to the API documentation but is in fact optional in some instances
|
||||
val keywords: List<FilterKeyword> = emptyList(),
|
||||
// val statuses: List<FilterStatus>,
|
||||
) : Parcelable {
|
||||
enum class Action(val action: String) {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class FilterKeyword(
|
||||
val id: String,
|
||||
val keyword: String,
|
||||
@SerializedName("whole_word") val wholeWord: Boolean
|
||||
@Json(name = "whole_word") val wholeWord: Boolean
|
||||
) : Parcelable
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FilterResult(
|
||||
val filter: Filter,
|
||||
@SerializedName("keyword_matches") val keywordMatches: List<String>?,
|
||||
@SerializedName("status_matches") val statusMatches: List<String>?
|
||||
// @Json(name = "keyword_matches") val keywordMatches: List<String>? = null,
|
||||
// @Json(name = "status_matches") val statusMatches: List<String>? = null
|
||||
)
|
||||
|
|
|
@ -15,16 +15,18 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FilterV1(
|
||||
val id: String,
|
||||
val phrase: String,
|
||||
val context: List<String>,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@Json(name = "expires_at") val expiresAt: Date? = null,
|
||||
val irreversible: Boolean,
|
||||
@SerializedName("whole_word") val wholeWord: Boolean
|
||||
@Json(name = "whole_word") val wholeWord: Boolean
|
||||
) {
|
||||
companion object {
|
||||
const val HOME = "home"
|
||||
|
@ -42,8 +44,7 @@ data class FilterV1(
|
|||
if (other !is FilterV1) {
|
||||
return false
|
||||
}
|
||||
val filter = other as FilterV1?
|
||||
return filter?.id.equals(id)
|
||||
return other.id == id
|
||||
}
|
||||
|
||||
fun toFilter(): Filter {
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
data class HashTag(val name: String, val url: String, val following: Boolean? = null)
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class HashTag(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val following: Boolean? = null
|
||||
)
|
||||
|
|
|
@ -1,72 +1,98 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Instance(
|
||||
val domain: String,
|
||||
// val title: String,
|
||||
val version: String,
|
||||
// @SerializedName("source_url") val sourceUrl: String,
|
||||
// @Json(name = "source_url") val sourceUrl: String,
|
||||
// val description: String,
|
||||
// val usage: Usage,
|
||||
// val thumbnail: Thumbnail,
|
||||
// val languages: List<String>,
|
||||
val configuration: Configuration?,
|
||||
val configuration: Configuration? = null,
|
||||
// val registrations: Registrations,
|
||||
// val contact: Contact,
|
||||
val rules: List<Rule>?,
|
||||
val pleroma: PleromaConfiguration?
|
||||
val rules: List<Rule> = emptyList(),
|
||||
val pleroma: PleromaConfiguration? = null
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Usage(val users: Users) {
|
||||
data class Users(@SerializedName("active_month") val activeMonth: Int)
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Users(@Json(name = "active_month") val activeMonth: Int)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Thumbnail(
|
||||
val url: String,
|
||||
val blurhash: String?,
|
||||
val versions: Versions?
|
||||
val blurhash: String? = null,
|
||||
val versions: Versions? = null
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Versions(
|
||||
@SerializedName("@1x") val at1x: String?,
|
||||
@SerializedName("@2x") val at2x: String?
|
||||
@Json(name = "@1x") val at1x: String? = null,
|
||||
@Json(name = "@2x") val at2x: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Configuration(
|
||||
val urls: Urls?,
|
||||
val accounts: Accounts?,
|
||||
val statuses: Statuses?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachments?,
|
||||
val polls: Polls?,
|
||||
val translation: Translation?
|
||||
val urls: Urls? = null,
|
||||
val accounts: Accounts? = null,
|
||||
val statuses: Statuses? = null,
|
||||
@Json(name = "media_attachments") val mediaAttachments: MediaAttachments? = null,
|
||||
val polls: Polls? = null,
|
||||
val translation: Translation? = null
|
||||
) {
|
||||
data class Urls(@SerializedName("streaming_api") val streamingApi: String)
|
||||
data class Accounts(@SerializedName("max_featured_tags") val maxFeaturedTags: Int)
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Urls(@Json(name = "streaming_api") val streamingApi: String? = null)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Accounts(@Json(name = "max_featured_tags") val maxFeaturedTags: Int)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Statuses(
|
||||
@SerializedName("max_characters") val maxCharacters: Int,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int,
|
||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int
|
||||
@Json(name = "max_characters") val maxCharacters: Int? = null,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaAttachments(
|
||||
// Warning: This is an array in mastodon and a dictionary in friendica
|
||||
// @SerializedName("supported_mime_types") val supportedMimeTypes: List<String>,
|
||||
@SerializedName("image_size_limit") val imageSizeLimitBytes: Long,
|
||||
@SerializedName("image_matrix_limit") val imagePixelCountLimit: Long,
|
||||
@SerializedName("video_size_limit") val videoSizeLimitBytes: Long,
|
||||
@SerializedName("video_matrix_limit") val videoPixelCountLimit: Long,
|
||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int
|
||||
// @Json(name = "supported_mime_types") val supportedMimeTypes: List<String> = emptyList(),
|
||||
@Json(name = "image_size_limit") val imageSizeLimitBytes: Long? = null,
|
||||
@Json(name = "image_matrix_limit") val imagePixelCountLimit: Long? = null,
|
||||
@Json(name = "video_size_limit") val videoSizeLimitBytes: Long? = null,
|
||||
@Json(name = "video_matrix_limit") val videoPixelCountLimit: Long? = null,
|
||||
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Polls(
|
||||
@SerializedName("max_options") val maxOptions: Int,
|
||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int,
|
||||
@SerializedName("min_expiration") val minExpirationSeconds: Int,
|
||||
@SerializedName("max_expiration") val maxExpirationSeconds: Int
|
||||
@Json(name = "max_options") val maxOptions: Int? = null,
|
||||
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int? = null,
|
||||
@Json(name = "min_expiration") val minExpirationSeconds: Int? = null,
|
||||
@Json(name = "max_expiration") val maxExpirationSeconds: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Translation(val enabled: Boolean)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Registrations(
|
||||
val enabled: Boolean,
|
||||
@SerializedName("approval_required") val approvalRequired: Boolean,
|
||||
val message: String?
|
||||
@Json(name = "approval_required") val approvalRequired: Boolean,
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Contact(val email: String, val account: Account)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Rule(val id: String, val text: String)
|
||||
}
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceV1(
|
||||
val uri: String,
|
||||
// val title: String,
|
||||
|
@ -27,14 +29,14 @@ data class InstanceV1(
|
|||
// val stats: Map<String, Int>?,
|
||||
// val thumbnail: String?,
|
||||
// val languages: List<String>,
|
||||
// @SerializedName("contact_account") val contactAccount: Account,
|
||||
@SerializedName("max_toot_chars") val maxTootChars: Int?,
|
||||
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?,
|
||||
val configuration: InstanceConfiguration?,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
||||
val pleroma: PleromaConfiguration?,
|
||||
@SerializedName("upload_limit") val uploadLimit: Int?,
|
||||
val rules: List<InstanceRules>?
|
||||
// @Json(name = "contact_account") val contactAccount: Account?,
|
||||
@Json(name = "max_toot_chars") val maxTootChars: Int? = null,
|
||||
@Json(name = "poll_limits") val pollConfiguration: PollConfiguration? = null,
|
||||
val configuration: InstanceConfiguration? = null,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||
val pleroma: PleromaConfiguration? = null,
|
||||
@Json(name = "upload_limit") val uploadLimit: Int? = null,
|
||||
val rules: List<InstanceRules> = emptyList()
|
||||
) {
|
||||
override fun hashCode(): Int {
|
||||
return uri.hashCode()
|
||||
|
@ -44,54 +46,61 @@ data class InstanceV1(
|
|||
if (other !is InstanceV1) {
|
||||
return false
|
||||
}
|
||||
val instance = other as InstanceV1?
|
||||
return instance?.uri.equals(uri)
|
||||
return other.uri == uri
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollConfiguration(
|
||||
@SerializedName("max_options") val maxOptions: Int?,
|
||||
@SerializedName("max_option_chars") val maxOptionChars: Int?,
|
||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
|
||||
@SerializedName("min_expiration") val minExpiration: Int?,
|
||||
@SerializedName("max_expiration") val maxExpiration: Int?
|
||||
@Json(name = "max_options") val maxOptions: Int? = null,
|
||||
@Json(name = "max_option_chars") val maxOptionChars: Int? = null,
|
||||
@Json(name = "max_characters_per_option") val maxCharactersPerOption: Int? = null,
|
||||
@Json(name = "min_expiration") val minExpiration: Int? = null,
|
||||
@Json(name = "max_expiration") val maxExpiration: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceConfiguration(
|
||||
val statuses: StatusConfiguration?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
||||
val polls: PollConfiguration?
|
||||
val statuses: StatusConfiguration? = null,
|
||||
@Json(name = "media_attachments") val mediaAttachments: MediaAttachmentConfiguration? = null,
|
||||
val polls: PollConfiguration? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusConfiguration(
|
||||
@SerializedName("max_characters") val maxCharacters: Int?,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?
|
||||
@Json(name = "max_characters") val maxCharacters: Int? = null,
|
||||
@Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null,
|
||||
@Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaAttachmentConfiguration(
|
||||
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>?,
|
||||
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
|
||||
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
|
||||
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
|
||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
||||
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?
|
||||
@Json(name = "supported_mime_types") val supportedMimeTypes: List<String> = emptyList(),
|
||||
@Json(name = "image_size_limit") val imageSizeLimit: Int? = null,
|
||||
@Json(name = "image_matrix_limit") val imageMatrixLimit: Int? = null,
|
||||
@Json(name = "video_size_limit") val videoSizeLimit: Int? = null,
|
||||
@Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int? = null,
|
||||
@Json(name = "video_matrix_limit") val videoMatrixLimit: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaConfiguration(
|
||||
val metadata: PleromaMetadata?
|
||||
val metadata: PleromaMetadata? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaMetadata(
|
||||
@SerializedName("fields_limits") val fieldLimits: PleromaFieldLimits
|
||||
@Json(name = "fields_limits") val fieldLimits: PleromaFieldLimits
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PleromaFieldLimits(
|
||||
@SerializedName("max_fields") val maxFields: Int?,
|
||||
@SerializedName("name_length") val nameLength: Int?,
|
||||
@SerializedName("value_length") val valueLength: Int?
|
||||
@Json(name = "max_fields") val maxFields: Int? = null,
|
||||
@Json(name = "name_length") val nameLength: Int? = null,
|
||||
@Json(name = "value_length") val valueLength: Int? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InstanceRules(
|
||||
val id: String,
|
||||
val text: String
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* API type for saving the scroll position of a timeline.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Marker(
|
||||
@SerializedName("last_read_id")
|
||||
@Json(name = "last_read_id")
|
||||
val lastReadId: String,
|
||||
val version: Int,
|
||||
@SerializedName("updated_at")
|
||||
@Json(name = "updated_at")
|
||||
val updatedAt: Date
|
||||
)
|
||||
|
|
|
@ -16,17 +16,18 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Created by charlag on 1/4/18.
|
||||
*/
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MastoList(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val exclusive: Boolean?,
|
||||
@SerializedName("replies_policy") val repliesPolicy: String?
|
||||
val exclusive: Boolean? = null,
|
||||
@Json(name = "replies_policy") val repliesPolicy: String? = null
|
||||
) {
|
||||
enum class ReplyPolicy(val policy: String) {
|
||||
NONE("none"),
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* The same as Attachment, except the url is null - see https://docs.joinmastodon.org/methods/statuses/media/
|
||||
* We are only interested in the id, so other attributes are omitted
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaUploadResult(
|
||||
val id: String
|
||||
)
|
||||
|
|
|
@ -16,35 +16,39 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewStatus(
|
||||
val status: String,
|
||||
@SerializedName("spoiler_text") val warningText: String,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@Json(name = "spoiler_text") val warningText: String,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String? = null,
|
||||
val visibility: String,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("media_ids") val mediaIds: List<String>?,
|
||||
@SerializedName("media_attributes") val mediaAttributes: List<MediaAttribute>?,
|
||||
@SerializedName("scheduled_at") val scheduledAt: String?,
|
||||
val poll: NewPoll?,
|
||||
val language: String?
|
||||
@Json(name = "media_ids") val mediaIds: List<String> = emptyList(),
|
||||
@Json(name = "media_attributes") val mediaAttributes: List<MediaAttribute> = emptyList(),
|
||||
@Json(name = "scheduled_at") val scheduledAt: String? = null,
|
||||
val poll: NewPoll? = null,
|
||||
val language: String? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class NewPoll(
|
||||
val options: List<String>,
|
||||
@SerializedName("expires_in") val expiresIn: Int,
|
||||
@Json(name = "expires_in") val expiresIn: Int,
|
||||
val multiple: Boolean
|
||||
) : Parcelable
|
||||
|
||||
// It would be nice if we could reuse MediaToSend,
|
||||
// but the server requires a different format for focus
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class MediaAttribute(
|
||||
val id: String,
|
||||
val description: String?,
|
||||
val focus: String?,
|
||||
val thumbnail: String?
|
||||
val description: String? = null,
|
||||
val focus: String? = null,
|
||||
val thumbnail: String? = null
|
||||
) : Parcelable
|
||||
|
|
|
@ -16,65 +16,68 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Notification(
|
||||
val type: Type,
|
||||
val id: String,
|
||||
val account: TimelineAccount,
|
||||
val status: Status?,
|
||||
val report: Report?
|
||||
val status: Status? = null,
|
||||
val report: Report? = null
|
||||
) {
|
||||
|
||||
/** From https://docs.joinmastodon.org/entities/Notification/#type */
|
||||
@JsonAdapter(NotificationTypeAdapter::class)
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Type(val presentation: String, @StringRes val uiString: Int) {
|
||||
UNKNOWN("unknown", R.string.notification_unknown_name),
|
||||
|
||||
/** Someone mentioned you */
|
||||
@Json(name = "mention")
|
||||
MENTION("mention", R.string.notification_mention_name),
|
||||
|
||||
/** Someone boosted one of your statuses */
|
||||
@Json(name = "reblog")
|
||||
REBLOG("reblog", R.string.notification_boost_name),
|
||||
|
||||
/** Someone favourited one of your statuses */
|
||||
@Json(name = "favourite")
|
||||
FAVOURITE("favourite", R.string.notification_favourite_name),
|
||||
|
||||
/** Someone followed you */
|
||||
@Json(name = "follow")
|
||||
FOLLOW("follow", R.string.notification_follow_name),
|
||||
|
||||
/** Someone requested to follow you */
|
||||
@Json(name = "follow_request")
|
||||
FOLLOW_REQUEST("follow_request", R.string.notification_follow_request_name),
|
||||
|
||||
/** A poll you have voted in or created has ended */
|
||||
@Json(name = "poll")
|
||||
POLL("poll", R.string.notification_poll_name),
|
||||
|
||||
/** Someone you enabled notifications for has posted a status */
|
||||
@Json(name = "status")
|
||||
STATUS("status", R.string.notification_subscription_name),
|
||||
|
||||
/** Someone signed up (optionally sent to admins) */
|
||||
@Json(name = "admin.sign_up")
|
||||
SIGN_UP("admin.sign_up", R.string.notification_sign_up_name),
|
||||
|
||||
/** A status you interacted with has been updated */
|
||||
@Json(name = "update")
|
||||
UPDATE("update", R.string.notification_update_name),
|
||||
|
||||
/** A new report has been filed */
|
||||
@Json(name = "admin.report")
|
||||
REPORT("admin.report", R.string.notification_report_name);
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun byString(s: String): Type {
|
||||
entries.forEach {
|
||||
if (s == it.presentation) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return UNKNOWN
|
||||
return entries.firstOrNull { it.presentation == s } ?: UNKNOWN
|
||||
}
|
||||
|
||||
/** Notification types for UI display (omits UNKNOWN) */
|
||||
|
@ -95,20 +98,7 @@ data class Notification(
|
|||
if (other !is Notification) {
|
||||
return false
|
||||
}
|
||||
val notification = other as Notification?
|
||||
return notification?.id == this.id
|
||||
}
|
||||
|
||||
class NotificationTypeAdapter : JsonDeserializer<Type> {
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: java.lang.reflect.Type,
|
||||
context: JsonDeserializationContext
|
||||
): Type {
|
||||
return Type.byString(json.asString)
|
||||
}
|
||||
return other.id == this.id
|
||||
}
|
||||
|
||||
/** Helper for Java */
|
||||
|
|
|
@ -15,10 +15,12 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NotificationSubscribeResult(
|
||||
val id: Int,
|
||||
val endpoint: String,
|
||||
@SerializedName("server_key") val serverKey: String
|
||||
@Json(name = "server_key") val serverKey: String
|
||||
)
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Poll(
|
||||
val id: String,
|
||||
@SerializedName("expires_at") val expiresAt: Date?,
|
||||
@Json(name = "expires_at") val expiresAt: Date? = null,
|
||||
val expired: Boolean,
|
||||
val multiple: Boolean,
|
||||
@SerializedName("votes_count") val votesCount: Int,
|
||||
@Json(name = "votes_count") val votesCount: Int,
|
||||
// nullable for compatibility with Pleroma
|
||||
@SerializedName("voters_count") val votersCount: Int?,
|
||||
@Json(name = "voters_count") val votersCount: Int? = null,
|
||||
val options: List<PollOption>,
|
||||
val voted: Boolean,
|
||||
@SerializedName("own_votes") val ownVotes: List<Int>?
|
||||
val voted: Boolean = false,
|
||||
@Json(name = "own_votes") val ownVotes: List<Int> = emptyList()
|
||||
) {
|
||||
|
||||
fun votedCopy(choices: List<Int>): Poll {
|
||||
val newOptions = options.mapIndexed { index, option ->
|
||||
if (choices.contains(index)) {
|
||||
option.copy(votesCount = option.votesCount + 1)
|
||||
option.copy(votesCount = (option.votesCount ?: 0) + 1)
|
||||
} else {
|
||||
option
|
||||
}
|
||||
|
@ -42,7 +44,8 @@ data class Poll(
|
|||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollOption(
|
||||
val title: String,
|
||||
@SerializedName("votes_count") val votesCount: Int
|
||||
@Json(name = "votes_count") val votesCount: Int? = null
|
||||
)
|
||||
|
|
|
@ -15,27 +15,28 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.json.GuardedBooleanAdapter
|
||||
import com.keylesspalace.tusky.json.Guarded
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Relationship(
|
||||
val id: String,
|
||||
val following: Boolean,
|
||||
@SerializedName("followed_by") val followedBy: Boolean,
|
||||
@Json(name = "followed_by") val followedBy: Boolean,
|
||||
val blocking: Boolean,
|
||||
val muting: Boolean,
|
||||
@SerializedName("muting_notifications") val mutingNotifications: Boolean,
|
||||
@Json(name = "muting_notifications") val mutingNotifications: Boolean,
|
||||
val requested: Boolean,
|
||||
@SerializedName("showing_reblogs") val showingReblogs: Boolean,
|
||||
@Json(name = "showing_reblogs") val showingReblogs: Boolean,
|
||||
/* Pleroma extension, same as 'notifying' on Mastodon.
|
||||
* Some instances like qoto.org have a custom subscription feature where 'subscribing' is a json object,
|
||||
* so we use the custom GuardedBooleanAdapter to ignore the field if it is not a boolean.
|
||||
* so we use GuardedAdapter to ignore the field if it is not a boolean.
|
||||
*/
|
||||
@JsonAdapter(GuardedBooleanAdapter::class) val subscribing: Boolean? = null,
|
||||
@SerializedName("domain_blocking") val blockingDomain: Boolean,
|
||||
@Guarded val subscribing: Boolean? = null,
|
||||
@Json(name = "domain_blocking") val blockingDomain: Boolean,
|
||||
// nullable for backward compatibility / feature detection
|
||||
val note: String?,
|
||||
val note: String? = null,
|
||||
// since 3.3.0rc
|
||||
val notifying: Boolean?
|
||||
val notifying: Boolean? = null
|
||||
)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Report(
|
||||
val id: String,
|
||||
val category: String,
|
||||
val status_ids: List<String>?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@SerializedName("target_account") val targetAccount: TimelineAccount
|
||||
@Json(name = "status_ids") val statusIds: List<String>? = null,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
@Json(name = "target_account") val targetAccount: TimelineAccount
|
||||
)
|
||||
|
|
|
@ -15,11 +15,13 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ScheduledStatus(
|
||||
val id: String,
|
||||
@SerializedName("scheduled_at") val scheduledAt: String,
|
||||
@Json(name = "scheduled_at") val scheduledAt: String,
|
||||
val params: StatusParams,
|
||||
@SerializedName("media_attachments") val mediaAttachments: ArrayList<Attachment>
|
||||
@Json(name = "media_attachments") val mediaAttachments: List<Attachment>
|
||||
)
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SearchResult(
|
||||
val accounts: List<TimelineAccount>,
|
||||
val statuses: List<Status>,
|
||||
|
|
|
@ -17,44 +17,47 @@ package com.keylesspalace.tusky.entity
|
|||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.URLSpan
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Status(
|
||||
val id: String,
|
||||
// not present if it's reblog
|
||||
val url: String?,
|
||||
val url: String? = null,
|
||||
val account: TimelineAccount,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?,
|
||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||
val reblog: Status?,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String? = null,
|
||||
@Json(name = "in_reply_to_account_id") val inReplyToAccountId: String? = null,
|
||||
val reblog: Status? = null,
|
||||
val content: String,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@SerializedName("edited_at") val editedAt: Date?,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
@Json(name = "edited_at") val editedAt: Date? = null,
|
||||
val emojis: List<Emoji>,
|
||||
@SerializedName("reblogs_count") val reblogsCount: Int,
|
||||
@SerializedName("favourites_count") val favouritesCount: Int,
|
||||
@SerializedName("replies_count") val repliesCount: Int,
|
||||
val reblogged: Boolean,
|
||||
val favourited: Boolean,
|
||||
val bookmarked: Boolean,
|
||||
@Json(name = "reblogs_count") val reblogsCount: Int,
|
||||
@Json(name = "favourites_count") val favouritesCount: Int,
|
||||
@Json(name = "replies_count") val repliesCount: Int,
|
||||
val reblogged: Boolean = false,
|
||||
val favourited: Boolean = false,
|
||||
val bookmarked: Boolean = false,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val visibility: Visibility,
|
||||
@SerializedName("media_attachments") val attachments: List<Attachment>,
|
||||
@Json(name = "media_attachments") val attachments: List<Attachment>,
|
||||
val mentions: List<Mention>,
|
||||
val tags: List<HashTag>?,
|
||||
val application: Application?,
|
||||
val pinned: Boolean?,
|
||||
val muted: Boolean?,
|
||||
val poll: Poll?,
|
||||
// Use null to mark the absence of tags because of semantic differences in LinkHelper
|
||||
val tags: List<HashTag>? = null,
|
||||
val application: Application? = null,
|
||||
val pinned: Boolean = false,
|
||||
val muted: Boolean = false,
|
||||
val poll: Poll? = null,
|
||||
/** Preview card for links included within status content. */
|
||||
val card: Card?,
|
||||
val card: Card? = null,
|
||||
/** ISO 639 language code for this status. */
|
||||
val language: String?,
|
||||
val language: String? = null,
|
||||
/** If the current token has an authorized user: The filter and keywords that matched this status. */
|
||||
val filtered: List<FilterResult>?
|
||||
val filtered: List<FilterResult> = emptyList()
|
||||
) {
|
||||
|
||||
val actionableId: String
|
||||
|
@ -70,30 +73,30 @@ data class Status(
|
|||
fun copyWithPoll(poll: Poll?): Status = copy(poll = poll)
|
||||
fun copyWithPinned(pinned: Boolean): Status = copy(pinned = pinned)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Visibility(val num: Int) {
|
||||
UNKNOWN(0),
|
||||
|
||||
@SerializedName("public")
|
||||
@Json(name = "public")
|
||||
PUBLIC(1),
|
||||
|
||||
@SerializedName("unlisted")
|
||||
@Json(name = "unlisted")
|
||||
UNLISTED(2),
|
||||
|
||||
@SerializedName("private")
|
||||
@Json(name = "private")
|
||||
PRIVATE(3),
|
||||
|
||||
@SerializedName("direct")
|
||||
@Json(name = "direct")
|
||||
DIRECT(4);
|
||||
|
||||
fun serverString(): String {
|
||||
return when (this) {
|
||||
val serverString: String
|
||||
get() = when (this) {
|
||||
PUBLIC -> "public"
|
||||
UNLISTED -> "unlisted"
|
||||
PRIVATE -> "private"
|
||||
DIRECT -> "direct"
|
||||
UNKNOWN -> "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
|
@ -123,13 +126,10 @@ data class Status(
|
|||
}
|
||||
}
|
||||
|
||||
fun rebloggingAllowed(): Boolean {
|
||||
return (visibility != Visibility.DIRECT && visibility != Visibility.UNKNOWN)
|
||||
}
|
||||
|
||||
fun isPinned(): Boolean {
|
||||
return pinned ?: false
|
||||
}
|
||||
val isRebloggingAllowed: Boolean
|
||||
get() {
|
||||
return (visibility != Visibility.DIRECT && visibility != Visibility.UNKNOWN)
|
||||
}
|
||||
|
||||
fun toDeletedStatus(): DeletedStatus {
|
||||
return DeletedStatus(
|
||||
|
@ -164,16 +164,18 @@ data class Status(
|
|||
return builder.toString()
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Mention(
|
||||
val id: String,
|
||||
val url: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@SerializedName("username") val localUsername: String
|
||||
@Json(name = "acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Application(
|
||||
val name: String,
|
||||
val website: String?
|
||||
val website: String? = null
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusContext(
|
||||
val ancestors: List<Status>,
|
||||
val descendants: List<Status>
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusEdit(
|
||||
val content: String,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@Json(name = "spoiler_text") val spoilerText: String,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@Json(name = "created_at") val createdAt: Date,
|
||||
val account: TimelineAccount,
|
||||
val poll: Poll?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: List<Attachment>,
|
||||
val poll: Poll? = null,
|
||||
@Json(name = "media_attachments") val mediaAttachments: List<Attachment>,
|
||||
val emojis: List<Emoji>
|
||||
)
|
||||
|
|
|
@ -15,12 +15,14 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusParams(
|
||||
val text: String,
|
||||
val sensitive: Boolean,
|
||||
val sensitive: Boolean? = null,
|
||||
val visibility: Status.Visibility,
|
||||
@SerializedName("spoiler_text") val spoilerText: String,
|
||||
@SerializedName("in_reply_to_id") val inReplyToId: String?
|
||||
@Json(name = "spoiler_text") val spoilerText: String? = null,
|
||||
@Json(name = "in_reply_to_id") val inReplyToId: String? = null
|
||||
)
|
||||
|
|
|
@ -15,10 +15,12 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusSource(
|
||||
val id: String,
|
||||
val text: String,
|
||||
@SerializedName("spoiler_text") val spoilerText: String
|
||||
@Json(name = "spoiler_text") val spoilerText: String
|
||||
)
|
||||
|
|
|
@ -15,24 +15,26 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Same as [Account], but only with the attributes required in timelines.
|
||||
* Prefer this class over [Account] because it uses way less memory & deserializes faster from json.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TimelineAccount(
|
||||
val id: String,
|
||||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@Json(name = "username") val localUsername: String,
|
||||
@Json(name = "acct") val username: String,
|
||||
// should never be null per Api definition, but some servers break the contract
|
||||
@SerializedName("display_name") val displayName: String?,
|
||||
@Json(name = "display_name") val displayName: String? = null,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
val note: String,
|
||||
val bot: Boolean = false,
|
||||
// nullable for backward compatibility
|
||||
val emojis: List<Emoji>? = emptyList()
|
||||
// optional for backward compatibility
|
||||
val emojis: List<Emoji> = emptyList()
|
||||
) {
|
||||
|
||||
val name: String
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MediaTranslation(
|
||||
val id: String,
|
||||
val description: String,
|
||||
|
@ -12,14 +14,25 @@ data class MediaTranslation(
|
|||
*
|
||||
* See [doc](https://docs.joinmastodon.org/entities/Translation/).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Translation(
|
||||
val content: String,
|
||||
@SerializedName("spoiler_warning")
|
||||
val spoilerWarning: String?,
|
||||
val poll: List<String>?,
|
||||
@SerializedName("media_attachments")
|
||||
@Json(name = "spoiler_text")
|
||||
val spoilerText: String? = null,
|
||||
val poll: TranslatedPoll? = null,
|
||||
@Json(name = "media_attachments")
|
||||
val mediaAttachments: List<MediaTranslation>,
|
||||
@SerializedName("detected_source_language")
|
||||
@Json(name = "detected_source_language")
|
||||
val detectedSourceLanguage: String,
|
||||
val provider: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TranslatedPoll(
|
||||
val options: List<TranslatedPollOption>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TranslatedPollOption(
|
||||
val title: String
|
||||
)
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
|
@ -25,6 +26,7 @@ import java.util.Date
|
|||
* @param history A list of [TrendingTagHistory]. Each element contains metrics per day for this hashtag.
|
||||
* (@param following This is not listed in the APIs at the time of writing, but an instance is delivering it.)
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TrendingTag(
|
||||
val name: String,
|
||||
val history: List<TrendingTagHistory>
|
||||
|
@ -37,11 +39,14 @@ data class TrendingTag(
|
|||
* @param accounts The number of accounts that have posted with this hashtag.
|
||||
* @param uses The number of posts with this hashtag.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TrendingTagHistory(
|
||||
val day: String,
|
||||
val accounts: String,
|
||||
val uses: String
|
||||
)
|
||||
|
||||
fun TrendingTag.start() = Date(history.last().day.toLong() * 1000L)
|
||||
fun TrendingTag.end() = Date(history.first().day.toLong() * 1000L)
|
||||
val TrendingTag.start
|
||||
get() = Date(history.last().day.toLong() * 1000L)
|
||||
val TrendingTag.end
|
||||
get() = Date(history.first().day.toLong() * 1000L)
|
||||
|
|
|
@ -180,7 +180,7 @@ abstract class SFragment : Fragment(), Injectable {
|
|||
R.id.pin,
|
||||
1,
|
||||
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
|
||||
if (mutable) {
|
||||
muteConversationItem.setTitle(
|
||||
if (status.muted != true) {
|
||||
if (!status.muted) {
|
||||
R.string.action_mute_conversation
|
||||
} else {
|
||||
R.string.action_unmute_conversation
|
||||
|
@ -328,10 +328,10 @@ abstract class SFragment : Fragment(), Injectable {
|
|||
|
||||
R.id.pin -> {
|
||||
lifecycleScope.launch {
|
||||
timelineCases.pin(status.id, !status.isPinned())
|
||||
timelineCases.pin(status.id, !status.pinned)
|
||||
.onFailure { e: Throwable ->
|
||||
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)
|
||||
.show()
|
||||
}
|
||||
|
@ -341,7 +341,7 @@ abstract class SFragment : Fragment(), Injectable {
|
|||
|
||||
R.id.status_mute_conversation -> {
|
||||
lifecycleScope.launch {
|
||||
timelineCases.muteConversation(status.id, status.muted != true)
|
||||
timelineCases.muteConversation(status.id, !status.muted)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
@ -444,7 +444,7 @@ abstract class SFragment : Fragment(), Injectable {
|
|||
timelineCases.delete(id).fold(
|
||||
{ deletedStatus ->
|
||||
removeItem(position)
|
||||
val sourceStatus = if (deletedStatus.isEmpty()) {
|
||||
val sourceStatus = if (deletedStatus.isEmpty) {
|
||||
status.toDeletedStatus()
|
||||
} else {
|
||||
deletedStatus
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* Copyright 2022 Tusky Contributors
|
||||
/*
|
||||
* Copyright 2024 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
@ -11,27 +12,13 @@
|
|||
* 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>. */
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.json
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import java.lang.reflect.Type
|
||||
import com.squareup.moshi.JsonQualifier
|
||||
|
||||
class GuardedBooleanAdapter : JsonDeserializer<Boolean?> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type,
|
||||
context: JsonDeserializationContext
|
||||
): Boolean? {
|
||||
return if (json.isJsonObject) {
|
||||
null
|
||||
} else {
|
||||
json.asBoolean
|
||||
}
|
||||
}
|
||||
}
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@JsonQualifier
|
||||
internal annotation class Guarded
|
|
@ -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)
|
||||
}
|
||||
|
||||
return if (matchingKind.isNullOrEmpty()) {
|
||||
return if (matchingKind.isEmpty()) {
|
||||
Filter.Action.NONE
|
||||
} else {
|
||||
matchingKind.maxOf { it.filter.action }
|
||||
|
|
|
@ -89,7 +89,9 @@ interface MastodonApi {
|
|||
): NetworkResult<InstanceV1>
|
||||
|
||||
@GET("api/v2/instance")
|
||||
suspend fun getInstance(): NetworkResult<Instance>
|
||||
suspend fun getInstance(
|
||||
@Header(DOMAIN_HEADER) domain: String? = null
|
||||
): NetworkResult<Instance>
|
||||
|
||||
@GET("api/v1/filters")
|
||||
suspend fun getFiltersV1(): NetworkResult<List<FilterV1>>
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* 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.network
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import okio.BufferedSink
|
||||
|
||||
class ProgressRequestBody(private val content: InputStream, private val contentLength: Long, private val mediaType: MediaType, private val uploadListener: UploadCallback) : RequestBody() {
|
||||
fun interface UploadCallback {
|
||||
fun onProgressUpdate(percentage: Int)
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType {
|
||||
return mediaType
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
return contentLength
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var uploaded: Long = 0
|
||||
|
||||
content.use { content ->
|
||||
var read: Int
|
||||
while (content.read(buffer).also { read = it } != -1) {
|
||||
uploadListener.onProgressUpdate((100 * uploaded / contentLength).toInt())
|
||||
uploaded += read.toLong()
|
||||
sink.write(buffer, 0, read)
|
||||
}
|
||||
uploadListener.onProgressUpdate((100 * uploaded / contentLength).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_BUFFER_SIZE = 2048
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/* 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.network
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import java.io.FileNotFoundException
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import okio.Buffer
|
||||
import okio.BufferedSink
|
||||
import okio.source
|
||||
|
||||
private const val DEFAULT_CHUNK_SIZE = 8192L
|
||||
|
||||
fun interface UploadCallback {
|
||||
fun onProgressUpdate(percentage: Int)
|
||||
}
|
||||
|
||||
fun Uri.asRequestBody(contentResolver: ContentResolver, contentType: MediaType? = null, contentLength: Long = -1L, uploadListener: UploadCallback? = null): RequestBody {
|
||||
return object : RequestBody() {
|
||||
override fun contentType(): MediaType? = contentType
|
||||
|
||||
override fun contentLength(): Long = contentLength
|
||||
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
val buffer = Buffer()
|
||||
var uploaded: Long = 0
|
||||
val inputStream = contentResolver.openInputStream(this@asRequestBody) ?: throw FileNotFoundException("Unavailable ContentProvider")
|
||||
|
||||
inputStream.source().use { source ->
|
||||
while (true) {
|
||||
val read = source.read(buffer, DEFAULT_CHUNK_SIZE)
|
||||
if (read == -1L) {
|
||||
break
|
||||
}
|
||||
sink.write(buffer, read)
|
||||
uploaded += read
|
||||
uploadListener?.let { if (contentLength > 0L) it.onProgressUpdate((100L * uploaded / contentLength).toInt()) }
|
||||
}
|
||||
uploadListener?.onProgressUpdate(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -94,7 +94,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
StatusToSend(
|
||||
text = text,
|
||||
warningText = spoiler,
|
||||
visibility = visibility.serverString(),
|
||||
visibility = visibility.serverString,
|
||||
sensitive = false,
|
||||
media = emptyList(),
|
||||
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)
|
||||
* @return the text with the shortcodes replaced by EmojiSpans
|
||||
*/
|
||||
fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean): CharSequence {
|
||||
if (emojis.isNullOrEmpty()) {
|
||||
fun CharSequence.emojify(emojis: List<Emoji>, view: View, animate: Boolean): CharSequence {
|
||||
if (emojis.isEmpty()) {
|
||||
return this
|
||||
}
|
||||
|
||||
|
|
|
@ -21,27 +21,29 @@ package com.keylesspalace.tusky.util
|
|||
* Class to represent sum type/tagged union/variant/ADT e.t.c.
|
||||
* It is either Left or Right.
|
||||
*/
|
||||
sealed class Either<out L, out R> {
|
||||
data class Left<out L, out R>(val value: L) : Either<L, R>()
|
||||
data class Right<out L, out R>(val value: R) : Either<L, R>()
|
||||
sealed interface Either<out L, out R> {
|
||||
data class Left<out L, out R>(val value: L) : Either<L, R>
|
||||
data class Right<out L, out R>(val value: R) : Either<L, R>
|
||||
|
||||
fun isRight() = this is Right
|
||||
fun isRight(): Boolean = this is Right
|
||||
|
||||
fun isLeft() = this is Left
|
||||
fun isLeft(): Boolean = this is Left
|
||||
|
||||
fun asLeftOrNull() = (this as? Left<L, R>)?.value
|
||||
fun asLeftOrNull(): L? = (this as? Left<L, R>)?.value
|
||||
|
||||
fun asRightOrNull() = (this as? Right<L, R>)?.value
|
||||
fun asRightOrNull(): R? = (this as? Right<L, R>)?.value
|
||||
|
||||
fun asLeft(): L = (this as Left<L, R>).value
|
||||
|
||||
fun asRight(): R = (this as Right<L, R>).value
|
||||
|
||||
inline fun <N> map(crossinline mapper: (R) -> N): Either<L, N> {
|
||||
return if (this.isLeft()) {
|
||||
Left(this.asLeft())
|
||||
} else {
|
||||
Right(mapper(this.asRight()))
|
||||
companion object {
|
||||
inline fun <L, R, N> Either<L, R>.map(mapper: (R) -> N): Either<L, N> {
|
||||
return if (this.isLeft()) {
|
||||
Left(this.asLeft())
|
||||
} else {
|
||||
Right(mapper(this.asRight()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,52 +15,33 @@
|
|||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
|
||||
private const val DEFAULT_BLOCKSIZE = 16384
|
||||
|
||||
fun Closeable?.closeQuietly() {
|
||||
fun Closeable.closeQuietly() {
|
||||
try {
|
||||
this?.close()
|
||||
close()
|
||||
} catch (e: IOException) {
|
||||
// intentionally unhandled
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle") // The linter can't tell that the stream gets closed by a helper method
|
||||
fun Uri.copyToFile(contentResolver: ContentResolver, file: File): Boolean {
|
||||
val from: InputStream?
|
||||
val to: FileOutputStream
|
||||
|
||||
try {
|
||||
from = contentResolver.openInputStream(this)
|
||||
to = FileOutputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (from == null) return false
|
||||
|
||||
val chunk = ByteArray(DEFAULT_BLOCKSIZE)
|
||||
try {
|
||||
while (true) {
|
||||
val bytes = from.read(chunk, 0, chunk.size)
|
||||
if (bytes < 0) break
|
||||
to.write(chunk, 0, bytes)
|
||||
return try {
|
||||
val inputStream = contentResolver.openInputStream(this) ?: return false
|
||||
inputStream.source().use { source ->
|
||||
file.sink().buffer().use { bufferedSink ->
|
||||
bufferedSink.writeAll(source)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
return false
|
||||
false
|
||||
}
|
||||
|
||||
from.closeQuietly()
|
||||
to.closeQuietly()
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class ListStatusAccessibilityDelegate(
|
|||
info.addAction(replyAction)
|
||||
|
||||
val actionable = status.actionable
|
||||
if (actionable.rebloggingAllowed()) {
|
||||
if (actionable.isRebloggingAllowed) {
|
||||
info.addAction(if (actionable.reblogged) unreblogAction else reblogAction)
|
||||
}
|
||||
info.addAction(if (actionable.favourited) unfavouriteAction else favouriteAction)
|
||||
|
|
|
@ -69,7 +69,7 @@ fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long {
|
|||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long {
|
||||
val input = contentResolver.openInputStream(uri)
|
||||
val input = contentResolver.openInputStream(uri) ?: throw FileNotFoundException("Unavailable ContentProvider")
|
||||
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
sealed class Resource<T>(open val data: T?)
|
||||
sealed interface Resource<T> {
|
||||
val data: T?
|
||||
}
|
||||
|
||||
class Loading<T>(override val data: T? = null) : Resource<T>(data)
|
||||
class Loading<T>(override val data: T? = null) : Resource<T>
|
||||
|
||||
class Success<T>(override val data: T? = null) : Resource<T>(data)
|
||||
class Success<T>(override val data: T? = null) : Resource<T>
|
||||
|
||||
class Error<T>(
|
||||
override val data: T? = null,
|
||||
val errorMessage: String? = null,
|
||||
var consumed: Boolean = false,
|
||||
val cause: Throwable? = null
|
||||
) : Resource<T>(data)
|
||||
) : Resource<T>
|
||||
|
|
|
@ -3,15 +3,14 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.text.Spanned
|
||||
import java.util.Random
|
||||
import kotlin.random.Random
|
||||
|
||||
private const val POSSIBLE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
fun randomAlphanumericString(count: Int): String {
|
||||
val chars = CharArray(count)
|
||||
val random = Random()
|
||||
for (i in 0 until count) {
|
||||
chars[i] = POSSIBLE_CHARS[random.nextInt(POSSIBLE_CHARS.length)]
|
||||
chars[i] = POSSIBLE_CHARS[Random.nextInt(POSSIBLE_CHARS.length)]
|
||||
}
|
||||
return String(chars)
|
||||
}
|
||||
|
|
|
@ -43,8 +43,8 @@ data class PollOptionViewData(
|
|||
var voted: Boolean
|
||||
)
|
||||
|
||||
fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int {
|
||||
return if (fraction == 0) {
|
||||
fun calculatePercent(fraction: Int?, totalVoters: Int?, totalVotes: Int): Int {
|
||||
return if (fraction == null || fraction == 0) {
|
||||
0
|
||||
} else {
|
||||
val total = totalVoters ?: totalVotes
|
||||
|
@ -76,7 +76,7 @@ fun Poll?.toViewData(): PollViewData? {
|
|||
votersCount = votersCount,
|
||||
options = options.mapIndexed { index, option ->
|
||||
option.toViewData(
|
||||
ownVotes?.contains(index) == true
|
||||
ownVotes.contains(index)
|
||||
)
|
||||
},
|
||||
voted = voted
|
||||
|
@ -86,7 +86,7 @@ fun Poll?.toViewData(): PollViewData? {
|
|||
fun PollOption.toViewData(voted: Boolean): PollOptionViewData {
|
||||
return PollOptionViewData(
|
||||
title = title,
|
||||
votesCount = votesCount,
|
||||
votesCount = votesCount ?: 0,
|
||||
selected = false,
|
||||
voted = voted
|
||||
)
|
||||
|
|
|
@ -22,12 +22,12 @@ import com.keylesspalace.tusky.entity.Translation
|
|||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
|
||||
sealed class TranslationViewData {
|
||||
abstract val data: Translation?
|
||||
sealed interface TranslationViewData {
|
||||
val data: Translation?
|
||||
|
||||
data class Loaded(override val data: Translation) : TranslationViewData()
|
||||
data class Loaded(override val data: Translation) : TranslationViewData
|
||||
|
||||
data object Loading : TranslationViewData() {
|
||||
data object Loading : TranslationViewData {
|
||||
override val data: Translation?
|
||||
get() = null
|
||||
}
|
||||
|
@ -67,10 +67,12 @@ sealed class StatusViewData {
|
|||
actionable.attachments.translated { translation -> map { it.translated(translation) } }
|
||||
|
||||
val spoilerText: String =
|
||||
actionable.spoilerText.translated { translation -> translation.spoilerWarning ?: this }
|
||||
actionable.spoilerText.translated { translation -> translation.spoilerText ?: this }
|
||||
|
||||
val poll = actionable.poll?.translated { translation ->
|
||||
val translatedOptionsText = translation.poll ?: return@translated this
|
||||
val translatedOptionsText = translation.poll?.options?.map { option ->
|
||||
option.title
|
||||
} ?: return@translated this
|
||||
val translatedOptions = options.zip(translatedOptionsText) { option, translatedText ->
|
||||
option.copy(title = translatedText)
|
||||
}
|
||||
|
|
|
@ -17,15 +17,14 @@ package com.keylesspalace.tusky.viewdata
|
|||
|
||||
import java.util.Date
|
||||
|
||||
sealed class TrendingViewData {
|
||||
abstract val id: String
|
||||
sealed interface TrendingViewData {
|
||||
val id: String
|
||||
|
||||
data class Header(
|
||||
val start: Date,
|
||||
val end: Date
|
||||
) : TrendingViewData() {
|
||||
override val id: String
|
||||
get() = start.toString() + end.toString()
|
||||
) : TrendingViewData {
|
||||
override val id: String = start.toString() + end.toString()
|
||||
}
|
||||
|
||||
data class Tag(
|
||||
|
@ -33,8 +32,7 @@ sealed class TrendingViewData {
|
|||
val usage: List<Long>,
|
||||
val accounts: List<Long>,
|
||||
val maxTrendingValue: Long
|
||||
) : TrendingViewData() {
|
||||
override val id: String
|
||||
get() = name
|
||||
) : TrendingViewData {
|
||||
override val id: String = name
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import at.connyduck.calladapter.networkresult.fold
|
|||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.Either.Companion.map
|
||||
import com.keylesspalace.tusky.util.Either.Left
|
||||
import com.keylesspalace.tusky.util.Either.Right
|
||||
import com.keylesspalace.tusky.util.withoutFirstWhich
|
||||
|
@ -117,7 +118,7 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi)
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun updateState(crossinline fn: State.() -> State) {
|
||||
private inline fun updateState(fn: State.() -> State) {
|
||||
_state.value = fn(_state.value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,12 @@ import java.io.IOException
|
|||
import java.net.ConnectException
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
|
||||
|
@ -49,15 +52,15 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi)
|
|||
|
||||
data class State(val lists: List<MastoList>, val loadingState: LoadingState)
|
||||
|
||||
val state: Flow<State> get() = _state
|
||||
val events: Flow<Event> get() = _events
|
||||
private val _state = MutableStateFlow(State(listOf(), LoadingState.INITIAL))
|
||||
private val _events =
|
||||
MutableSharedFlow<Event>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val state: StateFlow<State> = _state.asStateFlow()
|
||||
|
||||
private val _events = MutableSharedFlow<Event>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val events: SharedFlow<Event> = _events.asSharedFlow()
|
||||
|
||||
fun retryLoading() {
|
||||
loadIfNeeded()
|
||||
|
@ -140,7 +143,7 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi)
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun updateState(crossinline fn: State.() -> State) {
|
||||
private inline fun updateState(fn: State.() -> State) {
|
||||
_state.value = fn(_state.value)
|
||||
}
|
||||
|
||||
|
|
|
@ -101,8 +101,8 @@
|
|||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
license:license="@string/license_apache_2"
|
||||
license:link="https://github.com/google/gson"
|
||||
license:name="Gson" />
|
||||
license:link="https://github.com/square/moshi"
|
||||
license:name="Moshi" />
|
||||
|
||||
<com.keylesspalace.tusky.view.LicenseCard
|
||||
android:layout_width="match_parent"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue