Merge tag 'v16.0'
This commit is contained in:
commit
450f9f310e
435 changed files with 16070 additions and 12463 deletions
|
@ -11,17 +11,23 @@
|
||||||
All English text that will be visible to users should be put in ```app/src/main/res/values/strings.xml```. Any text that is missing in a translation will fall back to the version in this file. Be aware that anything added to this file will need to be translated, so be very concise with wording and try to add as few things as possible. Look for existing strings to use first. If there is untranslatable text that you don't want to keep as a string constant in a Java class, you can use the string resource file ```app/src/main/res/values/donottranslate.xml```.
|
All English text that will be visible to users should be put in ```app/src/main/res/values/strings.xml```. Any text that is missing in a translation will fall back to the version in this file. Be aware that anything added to this file will need to be translated, so be very concise with wording and try to add as few things as possible. Look for existing strings to use first. If there is untranslatable text that you don't want to keep as a string constant in a Java class, you can use the string resource file ```app/src/main/res/values/donottranslate.xml```.
|
||||||
|
|
||||||
### Translation
|
### Translation
|
||||||
Translations are done through https://weblate.tusky.app/projects/tusky/tusky/ .
|
Translations are done through our [Weblate](https://weblate.tusky.app/projects/tusky/tusky/).
|
||||||
To add a new language, clic on the 'Start a new translation' button on at the bottom of the page.
|
To add a new language, click on the 'Start a new translation' button on at the bottom of the page.
|
||||||
|
|
||||||
### Kotlin
|
### Kotlin
|
||||||
This project is in the process of migrating to Kotlin, we prefer new code to be written in Kotlin. We try to follow the [Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html) and make use of the [Kotlin Android Extensions](https://kotlinlang.org/docs/tutorials/android-plugin.html).
|
This project is in the process of migrating to Kotlin, all new code must be written in Kotlin.
|
||||||
|
We try to follow the [Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) and make format the code according to the default [ktlint codestyle](https://github.com/pinterest/ktlint).
|
||||||
|
You can check the codestyle by running `./gradlew ktlintCheck`.
|
||||||
|
|
||||||
### Java
|
### Java
|
||||||
Existing code in Java should follow the [Android Style Guide](https://source.android.com/source/code-style), which is what Android uses for their own source code. ```@Nullable``` and ```@NotNull``` annotations are really helpful for Kotlin interoperability.
|
Existing code in Java should follow the [Android Style Guide](https://source.android.com/source/code-style), which is what Android uses for their own source code. ```@Nullable``` and ```@NotNull``` annotations are really helpful for Kotlin interoperability. Please don't submit new features written in Java.
|
||||||
|
|
||||||
|
### Viewbinding
|
||||||
|
We use [Viewbinding](https://developer.android.com/topic/libraries/view-binding) to reference views. No contribution using another mechanism will be accepted.
|
||||||
|
There are useful extensions in `src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt` that make working with viewbinding easier.
|
||||||
|
|
||||||
### Visuals
|
### Visuals
|
||||||
There are three themes in the app, so any visual changes should be checked with each of them to ensure they look appropriate no matter which theme is selected. Usually, you can use existing color attributes like ```?attr/colorPrimary``` and ```?attr/textColorSecondary```. For icons and drawables, use a white drawable and tint it at runtime using ```ThemeUtils``` and specify an attribute that references different colours depending on the theme.
|
There are three themes in the app, so any visual changes should be checked with each of them to ensure they look appropriate no matter which theme is selected. Usually, you can use existing color attributes like ```?attr/colorPrimary``` and ```?attr/textColorSecondary```.
|
||||||
|
|
||||||
### Saving
|
### Saving
|
||||||
Any time you get a good chunk of work done it's good to make a commit. You can either uses Android Studio's built-in UI for doing this or running the commands:
|
Any time you get a good chunk of work done it's good to make a commit. You can either uses Android Studio's built-in UI for doing this or running the commands:
|
||||||
|
@ -31,15 +37,15 @@ git commit -m "Describe the changes in this commit here."
|
||||||
```
|
```
|
||||||
|
|
||||||
## Submitting Your Changes
|
## Submitting Your Changes
|
||||||
1. Make sure your branch is up-to-date with the ```master``` branch. Run:
|
1. Make sure your branch is up-to-date with the ```develop``` branch. Run:
|
||||||
```
|
```
|
||||||
git fetch
|
git fetch
|
||||||
git rebase origin/master
|
git rebase origin/develop
|
||||||
```
|
```
|
||||||
It may refuse to start the rebase if there's changes that haven't been committed, so make sure you've added and committed everything. If there were changes on master to any of the parts of files you worked on, a conflict will arise when you rebase. [Resolving a merge conflict](https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line) is a good guide to help with this. After committing the resolution, you can run ```git rebase --continue``` to finish the rebase. If you want to cancel, like if you make some mistake in resolving the conflict, you can always do ```git rebase --abort```.
|
It may refuse to start the rebase if there's changes that haven't been committed, so make sure you've added and committed everything. If there were changes on develop to any of the parts of files you worked on, a conflict will arise when you rebase. [Resolving a merge conflict](https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line) is a good guide to help with this. After committing the resolution, you can run ```git rebase --continue``` to finish the rebase. If you want to cancel, like if you make some mistake in resolving the conflict, you can always do ```git rebase --abort```.
|
||||||
|
|
||||||
2. Push your local branch to your fork on GitHub by running ```git push origin your-change-name```.
|
2. Push your local branch to your fork on GitHub by running ```git push origin your-change-name```.
|
||||||
3. Then, go to the original project page and make a pull request. Select your fork/branch and use ```master``` as the base branch.
|
3. Then, go to the original project page and make a pull request. Select your fork/branch and use ```develop``` as the base branch.
|
||||||
4. Wait for feedback on your pull request and be ready to make some changes
|
4. Wait for feedback on your pull request and be ready to make some changes
|
||||||
|
|
||||||
If you have any questions, don't hesitate to open an issue or contact [Tusky@mastodon.social](https://mastodon.social/@Tusky). Please also ask before you start implementing a new big feature.
|
If you have any questions, don't hesitate to open an issue or contact [Tusky@mastodon.social](https://mastodon.social/@Tusky). Please also ask before you start implementing a new big feature.
|
||||||
|
|
|
@ -15,13 +15,13 @@ def getGitSha = {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId APP_ID
|
applicationId APP_ID
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode 81
|
versionCode 87
|
||||||
versionName "15.1-CW1"
|
versionName "16.0-CW1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ android {
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
arg("room.incremental", "true")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,10 +60,6 @@ android {
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'MissingTranslation'
|
disable 'MissingTranslation'
|
||||||
}
|
}
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
|
@ -89,70 +84,76 @@ android {
|
||||||
enableSplit = false
|
enableSplit = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.lifecycleVersion = "2.2.0"
|
ext.coroutinesVersion = "1.6.0"
|
||||||
|
ext.lifecycleVersion = "2.3.1"
|
||||||
ext.roomVersion = '2.3.0'
|
ext.roomVersion = '2.3.0'
|
||||||
ext.retrofitVersion = '2.9.0'
|
ext.retrofitVersion = '2.9.0'
|
||||||
ext.okhttpVersion = '4.9.0'
|
ext.okhttpVersion = '4.9.3'
|
||||||
ext.glideVersion = '4.11.0'
|
ext.glideVersion = '4.12.0'
|
||||||
ext.daggerVersion = '2.30.1'
|
ext.daggerVersion = '2.40.5'
|
||||||
ext.materialdrawerVersion = '8.2.0'
|
ext.materialdrawerVersion = '8.4.5'
|
||||||
|
|
||||||
// if libraries are changed here, they should also be changed in LicenseActivity
|
// if libraries are changed here, they should also be changed in LicenseActivity
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.2.5"
|
|
||||||
|
implementation "androidx.core:core-ktx:1.5.0"
|
||||||
|
implementation "androidx.appcompat:appcompat:1.3.0"
|
||||||
|
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
||||||
implementation "androidx.browser:browser:1.3.0"
|
implementation "androidx.browser:browser:1.3.0"
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.0"
|
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
implementation "androidx.exifinterface:exifinterface:1.3.2"
|
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
implementation "androidx.cardview:cardview:1.0.0"
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||||
implementation "androidx.sharetarget:sharetarget:1.0.0"
|
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||||
implementation "androidx.emoji:emoji:1.1.0"
|
implementation "androidx.emoji:emoji:1.1.0"
|
||||||
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
||||||
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
|
||||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||||
implementation "androidx.work:work-runtime:2.4.0"
|
implementation "androidx.work:work-runtime:2.5.0"
|
||||||
implementation "androidx.room:room-runtime:$roomVersion"
|
implementation "androidx.room:room-ktx:$roomVersion"
|
||||||
implementation "androidx.room:room-rxjava2:$roomVersion"
|
implementation "androidx.room:room-rxjava3:$roomVersion"
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
kapt "androidx.room:room-compiler:$roomVersion"
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.3.0"
|
implementation "com.google.android.material:material:1.4.0"
|
||||||
|
|
||||||
|
implementation "com.google.code.gson:gson:2.8.9"
|
||||||
|
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
|
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
||||||
|
|
||||||
implementation "org.conscrypt:conscrypt-android:2.5.1"
|
implementation "org.conscrypt:conscrypt-android:2.5.2"
|
||||||
|
|
||||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
||||||
|
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||||
|
|
||||||
implementation "io.reactivex.rxjava2:rxjava:2.2.20"
|
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.17.0"
|
||||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
|
||||||
implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
|
|
||||||
|
|
||||||
implementation "com.uber.autodispose:autodispose-android-archcomponents:1.4.0"
|
implementation "io.reactivex.rxjava3:rxjava:3.0.12"
|
||||||
implementation "com.uber.autodispose:autodispose:1.4.0"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||||
|
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
|
||||||
|
|
||||||
|
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.0.0"
|
||||||
|
implementation "com.uber.autodispose2:autodispose:2.0.0"
|
||||||
|
|
||||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||||
|
@ -166,18 +167,21 @@ dependencies {
|
||||||
|
|
||||||
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
|
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
|
||||||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar'
|
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||||
|
|
||||||
implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0"
|
implementation "com.github.CanHub:Android-Image-Cropper:3.1.0"
|
||||||
|
|
||||||
implementation "de.c1710:filemojicompat:1.0.17"
|
implementation "de.c1710:filemojicompat:1.0.18"
|
||||||
|
|
||||||
testImplementation "androidx.test.ext:junit:1.1.2"
|
testImplementation "androidx.test.ext:junit:1.1.3"
|
||||||
testImplementation "org.robolectric:robolectric:4.4"
|
testImplementation "org.robolectric:robolectric:4.4"
|
||||||
testImplementation "org.mockito:mockito-inline:3.6.28"
|
testImplementation "org.mockito:mockito-inline:3.6.28"
|
||||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
|
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
||||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||||
|
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||||
|
|
||||||
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||||
}
|
}
|
||||||
|
|
38
app/proguard-rules.pro
vendored
38
app/proguard-rules.pro
vendored
|
@ -33,6 +33,10 @@
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class **.R$* {
|
||||||
|
public static <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
# TUSKY SPECIFIC OPTIONS
|
# TUSKY SPECIFIC OPTIONS
|
||||||
|
|
||||||
# keep members of our model classes, they are used in json de/serialization
|
# keep members of our model classes, they are used in json de/serialization
|
||||||
|
@ -43,10 +47,37 @@
|
||||||
public *;
|
public *;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class com.keylesspalace.tusky.components.conversation.ConversationAccountEntity { *; }
|
||||||
|
-keepclassmembers class com.keylesspalace.tusky.db.DraftAttachment { *; }
|
||||||
|
|
||||||
-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type {
|
-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type {
|
||||||
public *;
|
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
|
||||||
|
|
||||||
|
# Retain generic signatures of classes used in MastodonApi so Retrofit works
|
||||||
|
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Single
|
||||||
|
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||||
|
-keep,allowobfuscation,allowshrinking class kotlin.collections.List
|
||||||
|
-keep,allowobfuscation,allowshrinking class kotlin.collections.Map
|
||||||
|
-keep,allowobfuscation,allowshrinking class retrofit2.Call
|
||||||
|
|
||||||
|
# https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#retrofit
|
||||||
|
-keepattributes Signature
|
||||||
|
-keep class kotlin.coroutines.Continuation
|
||||||
|
|
||||||
# preserve line numbers for crash reporting
|
# preserve line numbers for crash reporting
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
-renamesourcefileattribute SourceFile
|
-renamesourcefileattribute SourceFile
|
||||||
|
@ -65,7 +96,14 @@
|
||||||
|
|
||||||
# remove some kotlin overhead
|
# remove some kotlin overhead
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
|
static void checkNotNull(java.lang.Object);
|
||||||
|
static void checkNotNull(java.lang.Object, java.lang.String);
|
||||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkNotNullParameter(java.lang.Object, java.lang.String);
|
||||||
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkNotNullExpressionValue(java.lang.Object, java.lang.String);
|
||||||
|
static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
|
||||||
static void throwUninitializedPropertyAccessException(java.lang.String);
|
static void throwUninitializedPropertyAccessException(java.lang.String);
|
||||||
}
|
}
|
||||||
|
|
747
app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json
Normal file
747
app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json
Normal file
|
@ -0,0 +1,747 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 26,
|
||||||
|
"identityHash": "14fb3d5743b7a89e8e62463e05f086ab",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "InstanceEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "instance",
|
||||||
|
"columnName": "instance",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojiList",
|
||||||
|
"columnName": "emojiList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTootCharacters",
|
||||||
|
"columnName": "maximumTootCharacters",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptions",
|
||||||
|
"columnName": "maxPollOptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptionLength",
|
||||||
|
"columnName": "maxPollOptionLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.bookmarked",
|
||||||
|
"columnName": "s_bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsible",
|
||||||
|
"columnName": "s_collapsible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14fb3d5743b7a89e8e62463e05f086ab')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
753
app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json
Normal file
753
app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json
Normal file
|
@ -0,0 +1,753 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 27,
|
||||||
|
"identityHash": "be914d4eb3f406b6970fef53a925afa1",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "InstanceEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "instance",
|
||||||
|
"columnName": "instance",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojiList",
|
||||||
|
"columnName": "emojiList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTootCharacters",
|
||||||
|
"columnName": "maximumTootCharacters",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptions",
|
||||||
|
"columnName": "maxPollOptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptionLength",
|
||||||
|
"columnName": "maxPollOptionLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.bookmarked",
|
||||||
|
"columnName": "s_bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsible",
|
||||||
|
"columnName": "s_collapsible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.muted",
|
||||||
|
"columnName": "s_muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be914d4eb3f406b6970fef53a925afa1')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
777
app/schemas/com.keylesspalace.tusky.db.AppDatabase/28.json
Normal file
777
app/schemas/com.keylesspalace.tusky.db.AppDatabase/28.json
Normal file
|
@ -0,0 +1,777 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 28,
|
||||||
|
"identityHash": "867026e095d84652026e902709389c00",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "InstanceEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "instance",
|
||||||
|
"columnName": "instance",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojiList",
|
||||||
|
"columnName": "emojiList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTootCharacters",
|
||||||
|
"columnName": "maximumTootCharacters",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptions",
|
||||||
|
"columnName": "maxPollOptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptionLength",
|
||||||
|
"columnName": "maxPollOptionLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.bookmarked",
|
||||||
|
"columnName": "s_bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsible",
|
||||||
|
"columnName": "s_collapsible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.muted",
|
||||||
|
"columnName": "s_muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '867026e095d84652026e902709389c00')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ package com.keylesspalace.tusky
|
||||||
|
|
||||||
import androidx.room.testing.MigrationTestHelper
|
import androidx.room.testing.MigrationTestHelper
|
||||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -33,12 +33,15 @@ class MigrationsTest {
|
||||||
val active = true
|
val active = true
|
||||||
val accountId = "accountId"
|
val accountId = "accountId"
|
||||||
val username = "username"
|
val username = "username"
|
||||||
val values = arrayOf(id, domain, token, active, accountId, username, "Display Name",
|
val values = arrayOf(
|
||||||
|
id, domain, token, active, accountId, username, "Display Name",
|
||||||
"https://picture.url", true, true, true, true, true, true, true,
|
"https://picture.url", true, true, true, true, true, true, true,
|
||||||
true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false,
|
true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false,
|
||||||
false, true)
|
false, true
|
||||||
|
)
|
||||||
|
|
||||||
db.execSQL("INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
|
db.execSQL(
|
||||||
|
"INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
|
||||||
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," +
|
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," +
|
||||||
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
|
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
|
||||||
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
|
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
|
||||||
|
@ -46,7 +49,8 @@ class MigrationsTest {
|
||||||
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
|
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
|
||||||
"`mediaPreviewEnabled`) " +
|
"`mediaPreviewEnabled`) " +
|
||||||
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
values)
|
values
|
||||||
|
)
|
||||||
|
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
|
@ -1,249 +0,0 @@
|
||||||
package com.keylesspalace.tusky
|
|
||||||
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import com.keylesspalace.tusky.db.*
|
|
||||||
import com.keylesspalace.tusky.entity.Status
|
|
||||||
import com.keylesspalace.tusky.repository.TimelineRepository
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class TimelineDAOTest {
|
|
||||||
private lateinit var timelineDao: TimelineDao
|
|
||||||
private lateinit var db: AppDatabase
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun createDb() {
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
|
|
||||||
timelineDao = db.timelineDao()
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun closeDb() {
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun insertGetStatus() {
|
|
||||||
val setOne = makeStatus(statusId = 3)
|
|
||||||
val setTwo = makeStatus(statusId = 20, reblog = true)
|
|
||||||
val ignoredOne = makeStatus(statusId = 1)
|
|
||||||
val ignoredTwo = makeStatus(accountId = 2)
|
|
||||||
|
|
||||||
for ((status, author, reblogger) in listOf(setOne, setTwo, ignoredOne, ignoredTwo)) {
|
|
||||||
timelineDao.insertInTransaction(status, author, reblogger)
|
|
||||||
}
|
|
||||||
|
|
||||||
val resultsFromDb = timelineDao.getStatusesForAccount(setOne.first.timelineUserId,
|
|
||||||
maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10)
|
|
||||||
.blockingGet()
|
|
||||||
|
|
||||||
assertEquals(2, resultsFromDb.size)
|
|
||||||
for ((set, fromDb) in listOf(setTwo, setOne).zip(resultsFromDb)) {
|
|
||||||
val (status, author, reblogger) = set
|
|
||||||
assertEquals(status, fromDb.status)
|
|
||||||
assertEquals(author, fromDb.account)
|
|
||||||
assertEquals(reblogger, fromDb.reblogAccount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun doNotOverwrite() {
|
|
||||||
val (status, author) = makeStatus()
|
|
||||||
timelineDao.insertInTransaction(status, author, null)
|
|
||||||
|
|
||||||
val placeholder = createPlaceholder(status.serverId, status.timelineUserId)
|
|
||||||
|
|
||||||
timelineDao.insertStatusIfNotThere(placeholder)
|
|
||||||
|
|
||||||
val fromDb = timelineDao.getStatusesForAccount(status.timelineUserId, null, null, 10)
|
|
||||||
.blockingGet()
|
|
||||||
val result = fromDb.first()
|
|
||||||
|
|
||||||
assertEquals(1, fromDb.size)
|
|
||||||
assertEquals(author, result.account)
|
|
||||||
assertEquals(status, result.status)
|
|
||||||
assertNull(result.reblogAccount)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun cleanup() {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000
|
|
||||||
val oldThisAccount = makeStatus(
|
|
||||||
statusId = 5,
|
|
||||||
createdAt = oldDate
|
|
||||||
)
|
|
||||||
val oldAnotherAccount = makeStatus(
|
|
||||||
statusId = 10,
|
|
||||||
createdAt = oldDate,
|
|
||||||
accountId = 2
|
|
||||||
)
|
|
||||||
val recentThisAccount = makeStatus(
|
|
||||||
statusId = 30,
|
|
||||||
createdAt = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
val recentAnotherAccount = makeStatus(
|
|
||||||
statusId = 60,
|
|
||||||
createdAt = System.currentTimeMillis(),
|
|
||||||
accountId = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
for ((status, author, reblogAuthor) in listOf(oldThisAccount, oldAnotherAccount, recentThisAccount, recentAnotherAccount)) {
|
|
||||||
timelineDao.insertInTransaction(status, author, reblogAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
timelineDao.cleanup(now - TimelineRepository.CLEANUP_INTERVAL)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
listOf(recentThisAccount),
|
|
||||||
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
|
|
||||||
.map { it.toTriple() }
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
listOf(recentAnotherAccount),
|
|
||||||
timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet()
|
|
||||||
.map { it.toTriple() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun overwriteDeletedStatus() {
|
|
||||||
|
|
||||||
val oldStatuses = listOf(
|
|
||||||
makeStatus(statusId = 3),
|
|
||||||
makeStatus(statusId = 2),
|
|
||||||
makeStatus(statusId = 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
timelineDao.deleteRange(1, oldStatuses.last().first.serverId, oldStatuses.first().first.serverId)
|
|
||||||
|
|
||||||
for ((status, author, reblogAuthor) in oldStatuses) {
|
|
||||||
timelineDao.insertInTransaction(status, author, reblogAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// status 2 gets deleted, newly loaded status contain only 1 + 3
|
|
||||||
val newStatuses = listOf(
|
|
||||||
makeStatus(statusId = 3),
|
|
||||||
makeStatus(statusId = 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
timelineDao.deleteRange(1, newStatuses.last().first.serverId, newStatuses.first().first.serverId)
|
|
||||||
|
|
||||||
for ((status, author, reblogAuthor) in newStatuses) {
|
|
||||||
timelineDao.insertInTransaction(status, author, reblogAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
//make sure status 2 is no longer in db
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
newStatuses,
|
|
||||||
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
|
|
||||||
.map { it.toTriple() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeStatus(
|
|
||||||
accountId: Long = 1,
|
|
||||||
statusId: Long = 10,
|
|
||||||
reblog: Boolean = false,
|
|
||||||
createdAt: Long = statusId,
|
|
||||||
authorServerId: String = "20"
|
|
||||||
): Triple<TimelineStatusEntity, TimelineAccountEntity, TimelineAccountEntity?> {
|
|
||||||
val author = TimelineAccountEntity(
|
|
||||||
authorServerId,
|
|
||||||
accountId,
|
|
||||||
"localUsername",
|
|
||||||
"username",
|
|
||||||
"displayName",
|
|
||||||
"blah",
|
|
||||||
"avatar",
|
|
||||||
"[\"tusky\": \"http://tusky.cool/emoji.jpg\"]",
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
val reblogAuthor = if (reblog) {
|
|
||||||
TimelineAccountEntity(
|
|
||||||
"R$authorServerId",
|
|
||||||
accountId,
|
|
||||||
"RlocalUsername",
|
|
||||||
"Rusername",
|
|
||||||
"RdisplayName",
|
|
||||||
"Rblah",
|
|
||||||
"Ravatar",
|
|
||||||
"[]",
|
|
||||||
false
|
|
||||||
)
|
|
||||||
} else null
|
|
||||||
|
|
||||||
|
|
||||||
val even = accountId % 2 == 0L
|
|
||||||
val status = TimelineStatusEntity(
|
|
||||||
serverId = statusId.toString(),
|
|
||||||
url = "url$statusId",
|
|
||||||
timelineUserId = accountId,
|
|
||||||
authorServerId = authorServerId,
|
|
||||||
inReplyToId = "inReplyToId$statusId",
|
|
||||||
inReplyToAccountId = "inReplyToAccountId$statusId",
|
|
||||||
content = "Content!$statusId",
|
|
||||||
createdAt = createdAt,
|
|
||||||
emojis = "emojis$statusId",
|
|
||||||
reblogsCount = 1 * statusId.toInt(),
|
|
||||||
favouritesCount = 2 * statusId.toInt(),
|
|
||||||
reblogged = even,
|
|
||||||
favourited = !even,
|
|
||||||
bookmarked = false,
|
|
||||||
sensitive = even,
|
|
||||||
spoilerText = "spoier$statusId",
|
|
||||||
visibility = Status.Visibility.PRIVATE,
|
|
||||||
attachments = "attachments$accountId",
|
|
||||||
mentions = "mentions$accountId",
|
|
||||||
application = "application$accountId",
|
|
||||||
reblogServerId = if (reblog) (statusId * 100).toString() else null,
|
|
||||||
reblogAccountId = reblogAuthor?.serverId,
|
|
||||||
poll = null,
|
|
||||||
muted = false
|
|
||||||
)
|
|
||||||
return Triple(status, author, reblogAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPlaceholder(serverId: String, timelineUserId: Long): TimelineStatusEntity {
|
|
||||||
return TimelineStatusEntity(
|
|
||||||
serverId = serverId,
|
|
||||||
url = null,
|
|
||||||
timelineUserId = timelineUserId,
|
|
||||||
authorServerId = null,
|
|
||||||
inReplyToId = null,
|
|
||||||
inReplyToAccountId = null,
|
|
||||||
content = null,
|
|
||||||
createdAt = 0L,
|
|
||||||
emojis = null,
|
|
||||||
reblogsCount = 0,
|
|
||||||
favouritesCount = 0,
|
|
||||||
reblogged = false,
|
|
||||||
favourited = false,
|
|
||||||
bookmarked = false,
|
|
||||||
sensitive = false,
|
|
||||||
spoilerText = null,
|
|
||||||
visibility = null,
|
|
||||||
attachments = null,
|
|
||||||
mentions = null,
|
|
||||||
application = null,
|
|
||||||
reblogServerId = null,
|
|
||||||
reblogAccountId = null,
|
|
||||||
poll = null,
|
|
||||||
muted = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TimelineStatusWithAccount.toTriple() = Triple(status, account, reblogAccount)
|
|
||||||
}
|
|
|
@ -34,9 +34,6 @@
|
||||||
android:resource="@xml/share_shortcuts" />
|
android:resource="@xml/share_shortcuts" />
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".SavedTootActivity"
|
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".LoginActivity"
|
android:name=".LoginActivity"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
@ -114,7 +111,7 @@
|
||||||
android:name=".ViewMediaActivity"
|
android:name=".ViewMediaActivity"
|
||||||
android:theme="@style/TuskyBaseTheme" />
|
android:theme="@style/TuskyBaseTheme" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".AccountActivity"
|
android:name=".components.account.AccountActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
||||||
<activity android:name=".EditProfileActivity" />
|
<activity android:name=".EditProfileActivity" />
|
||||||
<activity android:name=".components.preference.PreferencesActivity" />
|
<activity android:name=".components.preference.PreferencesActivity" />
|
||||||
|
@ -123,7 +120,7 @@
|
||||||
<activity android:name=".AboutActivity" />
|
<activity android:name=".AboutActivity" />
|
||||||
<activity android:name=".TabPreferenceActivity" />
|
<activity android:name=".TabPreferenceActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
android:name="com.canhub.cropper.CropImageActivity"
|
||||||
android:theme="@style/Base.Theme.AppCompat" />
|
android:theme="@style/Base.Theme.AppCompat" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.search.SearchActivity"
|
android:name=".components.search.SearchActivity"
|
||||||
|
|
|
@ -2,16 +2,16 @@ package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
|
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.util.CustomURLSpan
|
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
|
||||||
class AboutActivity : BottomSheetActivity(), Injectable {
|
class AboutActivity : BottomSheetActivity(), Injectable {
|
||||||
|
@ -63,7 +63,7 @@ private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
|
||||||
val end = builder.getSpanEnd(span)
|
val end = builder.getSpanEnd(span)
|
||||||
val flags = builder.getSpanFlags(span)
|
val flags = builder.getSpanFlags(span)
|
||||||
|
|
||||||
val customSpan = object : CustomURLSpan(span.url) {}
|
val customSpan = NoUnderlineURLSpan(span.url)
|
||||||
|
|
||||||
builder.removeSpan(span)
|
builder.removeSpan(span)
|
||||||
builder.setSpan(customSpan, start, end, flags)
|
builder.setSpan(customSpan, start, end, flags)
|
||||||
|
|
|
@ -28,18 +28,24 @@ import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
|
import autodispose2.autoDispose
|
||||||
import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
import com.keylesspalace.tusky.util.Either
|
||||||
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||||
import com.keylesspalace.tusky.viewmodel.State
|
import com.keylesspalace.tusky.viewmodel.State
|
||||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import com.uber.autodispose.autoDispose
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -146,11 +152,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
viewModel.load(listId)
|
viewModel.load(listId)
|
||||||
}
|
}
|
||||||
if (error is IOException) {
|
if (error is IOException) {
|
||||||
binding.messageView.setup(R.drawable.elephant_offline,
|
binding.messageView.setup(
|
||||||
R.string.error_network, retryAction)
|
R.drawable.elephant_offline,
|
||||||
|
R.string.error_network, retryAction
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
binding.messageView.setup(R.drawable.elephant_error,
|
binding.messageView.setup(
|
||||||
R.string.error_generic, retryAction)
|
R.drawable.elephant_error,
|
||||||
|
R.string.error_generic, retryAction
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,8 +213,8 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||||
return oldItem.second == newItem.second
|
return oldItem.second == newItem.second &&
|
||||||
&& oldItem.first.deepEquals(newItem.first)
|
oldItem.first.deepEquals(newItem.first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
if (requesters.containsKey(requestCode)) {
|
if (requesters.containsKey(requestCode)) {
|
||||||
PermissionRequester requester = requesters.remove(requestCode);
|
PermissionRequester requester = requesters.remove(requestCode);
|
||||||
requester.onRequestPermissionsResult(permissions, grantResults);
|
requester.onRequestPermissionsResult(permissions, grantResults);
|
||||||
|
|
|
@ -22,12 +22,13 @@ import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||||
|
import autodispose2.autoDispose
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
import com.keylesspalace.tusky.util.LinkHelper
|
||||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import com.uber.autodispose.autoDispose
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -60,7 +61,6 @@ abstract class BottomSheetActivity : BaseActivity() {
|
||||||
|
|
||||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
|
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
|
||||||
|
@ -74,7 +74,8 @@ abstract class BottomSheetActivity : BaseActivity() {
|
||||||
resolve = true
|
resolve = true
|
||||||
).observeOn(AndroidSchedulers.mainThread())
|
).observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||||
.subscribe({ (accounts, statuses) ->
|
.subscribe(
|
||||||
|
{ (accounts, statuses) ->
|
||||||
if (getCancelSearchRequested(url)) {
|
if (getCancelSearchRequested(url)) {
|
||||||
return@subscribe
|
return@subscribe
|
||||||
}
|
}
|
||||||
|
@ -90,12 +91,14 @@ abstract class BottomSheetActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
if (!getCancelSearchRequested(url)) {
|
if (!getCancelSearchRequested(url)) {
|
||||||
onEndSearch(url)
|
onEndSearch(url)
|
||||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onBeginSearch(url)
|
onBeginSearch(url)
|
||||||
}
|
}
|
||||||
|
@ -177,6 +180,8 @@ abstract class BottomSheetActivity : BaseActivity() {
|
||||||
// https://friendica.foo.bar/profile/user
|
// https://friendica.foo.bar/profile/user
|
||||||
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
||||||
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
|
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
|
||||||
|
// https://pixelfed.social/p/connyduck/391263492998670833
|
||||||
|
// https://pixelfed.social/connyduck
|
||||||
fun looksLikeMastodonUrl(urlString: String): Boolean {
|
fun looksLikeMastodonUrl(urlString: String): Boolean {
|
||||||
val uri: URI
|
val uri: URI
|
||||||
try {
|
try {
|
||||||
|
@ -187,7 +192,8 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
|
||||||
|
|
||||||
if (uri.query != null ||
|
if (uri.query != null ||
|
||||||
uri.fragment != null ||
|
uri.fragment != null ||
|
||||||
uri.path == null) {
|
uri.path == null
|
||||||
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,7 +205,9 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
|
||||||
path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
|
path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
|
||||||
path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
|
path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
|
||||||
path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
|
path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
|
||||||
path.matches("^/profile/\\w+$".toRegex())
|
path.matches("^/profile/\\w+$".toRegex()) ||
|
||||||
|
path.matches("^/p/\\w+/\\d+$".toRegex()) ||
|
||||||
|
path.matches("^/\\w+$".toRegex())
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PostLookupFallbackBehavior {
|
enum class PostLookupFallbackBehavior {
|
||||||
|
|
|
@ -36,18 +36,24 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
|
import com.canhub.cropper.CropImage
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||||
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.Error
|
||||||
|
import com.keylesspalace.tusky.util.Loading
|
||||||
|
import com.keylesspalace.tusky.util.Resource
|
||||||
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
import com.theartofdev.edmodo.cropper.CropImage
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class EditProfileActivity : BaseActivity(), Injectable {
|
class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
@ -150,7 +156,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
.load(me.header)
|
.load(me.header)
|
||||||
.into(binding.headerPreview)
|
.into(binding.headerPreview)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Error -> {
|
is Error -> {
|
||||||
|
@ -159,27 +164,27 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
viewModel.obtainProfile()
|
viewModel.obtainProfile()
|
||||||
}
|
}
|
||||||
snackbar.show()
|
snackbar.show()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
is Loading -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.obtainInstance()
|
viewModel.obtainInstance()
|
||||||
viewModel.instanceData.observe(this) { result ->
|
viewModel.instanceData.observe(this) { result ->
|
||||||
when (result) {
|
if (result is Success) {
|
||||||
is Success -> {
|
|
||||||
val instance = result.data
|
val instance = result.data
|
||||||
if (instance?.maxBioChars != null && instance.maxBioChars > 0) {
|
if (instance?.maxBioChars != null && instance.maxBioChars > 0) {
|
||||||
binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars
|
binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
|
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
|
||||||
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
|
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
|
||||||
|
|
||||||
viewModel.saveData.observe(this, {
|
viewModel.saveData.observe(
|
||||||
|
this,
|
||||||
|
{
|
||||||
when (it) {
|
when (it) {
|
||||||
is Success -> {
|
is Success -> {
|
||||||
finish()
|
finish()
|
||||||
|
@ -191,8 +196,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
onSaveFailure(it.errorMessage)
|
onSaveFailure(it.errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
@ -203,18 +208,24 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
if (!isFinishing) {
|
if (!isFinishing) {
|
||||||
viewModel.updateProfile(binding.displayNameEditText.text.toString(),
|
viewModel.updateProfile(
|
||||||
|
binding.displayNameEditText.text.toString(),
|
||||||
binding.noteEditText.text.toString(),
|
binding.noteEditText.text.toString(),
|
||||||
binding.lockedCheckBox.isChecked,
|
binding.lockedCheckBox.isChecked,
|
||||||
accountFieldEditAdapter.getFieldData())
|
accountFieldEditAdapter.getFieldData()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeImage(liveData: LiveData<Resource<Bitmap>>,
|
private fun observeImage(
|
||||||
|
liveData: LiveData<Resource<Bitmap>>,
|
||||||
imageView: ImageView,
|
imageView: ImageView,
|
||||||
progressBar: View,
|
progressBar: View,
|
||||||
roundedCorners: Boolean) {
|
roundedCorners: Boolean
|
||||||
liveData.observe(this, {
|
) {
|
||||||
|
liveData.observe(
|
||||||
|
this,
|
||||||
|
{
|
||||||
|
|
||||||
when (it) {
|
when (it) {
|
||||||
is Success -> {
|
is Success -> {
|
||||||
|
@ -242,10 +253,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
onResizeFailure()
|
onResizeFailure()
|
||||||
it.consumed = true
|
it.consumed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onMediaPick(pickType: PickType) {
|
private fun onMediaPick(pickType: PickType) {
|
||||||
|
@ -261,8 +272,12 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
|
override fun onRequestPermissionsResult(
|
||||||
grantResults: IntArray) {
|
requestCode: Int,
|
||||||
|
permissions: Array<String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
|
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
@ -310,11 +325,13 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.save(binding.displayNameEditText.text.toString(),
|
viewModel.save(
|
||||||
|
binding.displayNameEditText.text.toString(),
|
||||||
binding.noteEditText.text.toString(),
|
binding.noteEditText.text.toString(),
|
||||||
binding.lockedCheckBox.isChecked,
|
binding.lockedCheckBox.isChecked,
|
||||||
accountFieldEditAdapter.getFieldData(),
|
accountFieldEditAdapter.getFieldData(),
|
||||||
this)
|
this
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSaveFailure(msg: String?) {
|
private fun onSaveFailure(msg: String?) {
|
||||||
|
@ -374,7 +391,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> {
|
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> {
|
||||||
val result = CropImage.getActivityResult(data)
|
val result = CropImage.getActivityResult(data)
|
||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
Activity.RESULT_OK -> beginResize(result.uri)
|
Activity.RESULT_OK -> beginResize(result?.uriContent)
|
||||||
CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure()
|
CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure()
|
||||||
else -> endMediaPicking()
|
else -> endMediaPicking()
|
||||||
}
|
}
|
||||||
|
@ -382,7 +399,12 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun beginResize(uri: Uri) {
|
private fun beginResize(uri: Uri?) {
|
||||||
|
if (uri == null) {
|
||||||
|
currentlyPicking = PickType.NOTHING
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
beginMediaPicking()
|
beginMediaPicking()
|
||||||
|
|
||||||
when (currentlyPicking) {
|
when (currentlyPicking) {
|
||||||
|
@ -398,12 +420,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
currentlyPicking = PickType.NOTHING
|
currentlyPicking = PickType.NOTHING
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onResizeFailure() {
|
private fun onResizeFailure() {
|
||||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||||
endMediaPicking()
|
endMediaPicking()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.databinding.ActivityFiltersBinding
|
import com.keylesspalace.tusky.databinding.ActivityFiltersBinding
|
||||||
|
@ -14,6 +15,8 @@ import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.rx3.await
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
|
@ -90,8 +93,10 @@ class FiltersActivity: BaseActivity() {
|
||||||
} else {
|
} else {
|
||||||
// Keep the filter, but remove it from this context
|
// Keep the filter, but remove it from this context
|
||||||
val oldFilter = filters[itemIndex]
|
val oldFilter = filters[itemIndex]
|
||||||
val newFilter = Filter(oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
val newFilter = Filter(
|
||||||
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord)
|
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
||||||
|
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
|
||||||
|
)
|
||||||
updateFilter(newFilter, itemIndex)
|
updateFilter(newFilter, itemIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,8 +144,10 @@ class FiltersActivity: BaseActivity() {
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
|
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
|
||||||
val oldFilter = filters[itemIndex]
|
val oldFilter = filters[itemIndex]
|
||||||
val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
|
val newFilter = Filter(
|
||||||
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked)
|
oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
|
||||||
|
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked
|
||||||
|
)
|
||||||
updateFilter(newFilter, itemIndex)
|
updateFilter(newFilter, itemIndex)
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
||||||
|
@ -162,39 +169,35 @@ class FiltersActivity: BaseActivity() {
|
||||||
binding.addFilterButton.hide()
|
binding.addFilterButton.hide()
|
||||||
binding.filterProgressBar.show()
|
binding.filterProgressBar.show()
|
||||||
|
|
||||||
api.getFilters().enqueue(object : Callback<List<Filter>> {
|
lifecycleScope.launch {
|
||||||
override fun onResponse(call: Call<List<Filter>>, response: Response<List<Filter>>) {
|
val newFilters = try {
|
||||||
val filterResponse = response.body()
|
api.getFilters().await()
|
||||||
if(response.isSuccessful && filterResponse != null) {
|
} catch (t: Exception) {
|
||||||
|
binding.filterProgressBar.hide()
|
||||||
|
binding.filterMessageView.show()
|
||||||
|
if (t is IOException) {
|
||||||
|
binding.filterMessageView.setup(
|
||||||
|
R.drawable.elephant_offline,
|
||||||
|
R.string.error_network
|
||||||
|
) { loadFilters() }
|
||||||
|
} else {
|
||||||
|
binding.filterMessageView.setup(
|
||||||
|
R.drawable.elephant_error,
|
||||||
|
R.string.error_generic
|
||||||
|
) { loadFilters() }
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
filters = filterResponse.filter { filter -> filter.context.contains(context) }.toMutableList()
|
filters = newFilters.filter { it.context.contains(context) }.toMutableList()
|
||||||
refreshFilterDisplay()
|
refreshFilterDisplay()
|
||||||
|
|
||||||
binding.filtersView.show()
|
binding.filtersView.show()
|
||||||
binding.addFilterButton.show()
|
binding.addFilterButton.show()
|
||||||
binding.filterProgressBar.hide()
|
binding.filterProgressBar.hide()
|
||||||
} else {
|
|
||||||
binding.filterProgressBar.hide()
|
|
||||||
binding.filterMessageView.show()
|
|
||||||
binding.filterMessageView.setup(R.drawable.elephant_error,
|
|
||||||
R.string.error_generic) { loadFilters() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Filter>>, t: Throwable) {
|
|
||||||
binding.filterProgressBar.hide()
|
|
||||||
binding.filterMessageView.show()
|
|
||||||
if (t is IOException) {
|
|
||||||
binding.filterMessageView.setup(R.drawable.elephant_offline,
|
|
||||||
R.string.error_network) { loadFilters() }
|
|
||||||
} else {
|
|
||||||
binding.filterMessageView.setup(R.drawable.elephant_error,
|
|
||||||
R.string.error_generic) { loadFilters() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val FILTERS_CONTEXT = "filters_context"
|
const val FILTERS_CONTEXT = "filters_context"
|
||||||
const val FILTERS_TITLE = "filters_title"
|
const val FILTERS_TITLE = "filters_title"
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.RawRes
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.RawRes
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
||||||
import com.keylesspalace.tusky.util.IOUtils
|
import com.keylesspalace.tusky.util.IOUtils
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
@ -41,7 +41,6 @@ class LicenseActivity : BaseActivity() {
|
||||||
setTitle(R.string.title_licenses)
|
setTitle(R.string.title_licenses)
|
||||||
|
|
||||||
loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView)
|
loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
|
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
|
||||||
|
|
|
@ -23,32 +23,48 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.recyclerview.widget.*
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
|
import autodispose2.autoDispose
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||||
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.MastoList
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.onTextChanged
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.*
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_NETWORK
|
||||||
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
|
||||||
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
|
||||||
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
|
||||||
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
|
||||||
import com.uber.autodispose.autoDispose
|
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,7 +100,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
binding.listsRecycler.adapter = adapter
|
binding.listsRecycler.adapter = adapter
|
||||||
binding.listsRecycler.layoutManager = LinearLayoutManager(this)
|
binding.listsRecycler.layoutManager = LinearLayoutManager(this)
|
||||||
binding.listsRecycler.addItemDecoration(
|
binding.listsRecycler.addItemDecoration(
|
||||||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||||
|
)
|
||||||
|
|
||||||
viewModel.state
|
viewModel.state
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -101,9 +118,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
.subscribe { event ->
|
.subscribe { event ->
|
||||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||||
when (event) {
|
when (event) {
|
||||||
CREATE_ERROR -> showMessage(R.string.error_create_list)
|
Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
|
||||||
RENAME_ERROR -> showMessage(R.string.error_rename_list)
|
Event.RENAME_ERROR -> showMessage(R.string.error_rename_list)
|
||||||
DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,7 +138,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
.setView(layout)
|
.setView(layout)
|
||||||
.setPositiveButton(
|
.setPositiveButton(
|
||||||
if (list == null) R.string.action_create_list
|
if (list == null) R.string.action_create_list
|
||||||
else R.string.action_rename_list) { _, _ ->
|
else R.string.action_rename_list
|
||||||
|
) { _, _ ->
|
||||||
onPickedDialogName(editText.text, list?.id)
|
onPickedDialogName(editText.text, list?.id)
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
@ -145,7 +163,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun update(state: ListsViewModel.State) {
|
private fun update(state: ListsViewModel.State) {
|
||||||
adapter.submitList(state.lists)
|
adapter.submitList(state.lists)
|
||||||
binding.progressBar.visible(state.loadingState == LOADING)
|
binding.progressBar.visible(state.loadingState == LOADING)
|
||||||
|
@ -166,8 +183,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
LOADED ->
|
LOADED ->
|
||||||
if (state.lists.isEmpty()) {
|
if (state.lists.isEmpty()) {
|
||||||
binding.messageView.show()
|
binding.messageView.show()
|
||||||
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
|
binding.messageView.setup(
|
||||||
null)
|
R.drawable.elephant_friend_empty, R.string.message_empty,
|
||||||
|
null
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
binding.messageView.hide()
|
binding.messageView.hide()
|
||||||
}
|
}
|
||||||
|
@ -182,7 +201,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
|
|
||||||
private fun onListSelected(listId: String) {
|
private fun onListSelected(listId: String) {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithSlideInAnimation(
|
||||||
ModalTimelineActivity.newIntent(this, TimelineFragment.Kind.LIST, listId))
|
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openListSettings(list: MastoList) {
|
private fun openListSettings(list: MastoList) {
|
||||||
|
@ -219,8 +239,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ListsAdapter
|
private inner class ListsAdapter :
|
||||||
: ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
|
ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
|
||||||
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
|
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
|
||||||
|
@ -238,7 +258,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
holder.nameTextView.text = getItem(position).title
|
holder.nameTextView.text = getItem(position).title
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view),
|
private inner class ListViewHolder(view: View) :
|
||||||
|
RecyclerView.ViewHolder(view),
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
|
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
|
||||||
val moreButton: ImageButton = view.findViewById(R.id.editListButton)
|
val moreButton: ImageButton = view.findViewById(R.id.editListButton)
|
||||||
|
|
|
@ -34,7 +34,11 @@ import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.AccessToken
|
import com.keylesspalace.tusky.entity.AccessToken
|
||||||
import com.keylesspalace.tusky.entity.AppCredentials
|
import com.keylesspalace.tusky.entity.AppCredentials
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
|
import com.keylesspalace.tusky.util.rickRoll
|
||||||
|
import com.keylesspalace.tusky.util.shouldRickRoll
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
|
@ -75,7 +79,8 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences = getSharedPreferences(
|
preferences = getSharedPreferences(
|
||||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
binding.loginButton.setOnClickListener { onButtonClick() }
|
binding.loginButton.setOnClickListener { onButtonClick() }
|
||||||
binding.registerButton.setOnClickListener { onRegisterClick() }
|
binding.registerButton.setOnClickListener { onRegisterClick() }
|
||||||
|
@ -96,7 +101,6 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
} else {
|
} else {
|
||||||
binding.toolbar.visibility = View.GONE
|
binding.toolbar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requiresLogin(): Boolean {
|
override fun requiresLogin(): Boolean {
|
||||||
|
@ -150,8 +154,10 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
val callback = object : Callback<AppCredentials> {
|
val callback = object : Callback<AppCredentials> {
|
||||||
override fun onResponse(call: Call<AppCredentials>,
|
override fun onResponse(
|
||||||
response: Response<AppCredentials>) {
|
call: Call<AppCredentials>,
|
||||||
|
response: Response<AppCredentials>
|
||||||
|
) {
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
binding.loginButton.isEnabled = true
|
binding.loginButton.isEnabled = true
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
|
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
|
||||||
|
@ -181,11 +187,12 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
mastodonApi
|
mastodonApi
|
||||||
.authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri,
|
.authenticateApp(
|
||||||
OAUTH_SCOPES, getString(R.string.tusky_website))
|
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||||
|
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||||
|
)
|
||||||
.enqueue(callback)
|
.enqueue(callback)
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
|
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
|
||||||
|
@ -240,31 +247,27 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
} else {
|
} else {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
||||||
Log.e(TAG, String.format("%s %s",
|
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message()))
|
||||||
getString(R.string.error_retrieving_oauth_token),
|
|
||||||
response.message()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
|
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
||||||
Log.e(TAG, String.format("%s %s",
|
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message))
|
||||||
getString(R.string.error_retrieving_oauth_token),
|
|
||||||
t.message))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
|
mastodonApi.fetchOAuthToken(
|
||||||
"authorization_code").enqueue(callback)
|
domain, clientId, clientSecret, redirectUri, code,
|
||||||
|
"authorization_code"
|
||||||
|
).enqueue(callback)
|
||||||
} else if (error != null) {
|
} else if (error != null) {
|
||||||
/* Authorization failed. Put the error response where the user can read it and they
|
/* Authorization failed. Put the error response where the user can read it and they
|
||||||
* can try again. */
|
* can try again. */
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
|
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
|
||||||
Log.e(TAG, String.format("%s %s",
|
Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
|
||||||
getString(R.string.error_authorization_denied),
|
|
||||||
error))
|
|
||||||
} else {
|
} else {
|
||||||
// This case means a junk response was received somehow.
|
// This case means a junk response was received somehow.
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
|
@ -19,7 +19,10 @@ import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -31,24 +34,30 @@ import android.widget.ImageView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
import androidx.emoji.text.EmojiCompat.InitCallback
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.viewpager2.widget.MarginPageTransformer
|
import androidx.viewpager2.widget.MarginPageTransformer
|
||||||
|
import autodispose2.androidx.lifecycle.autoDispose
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
import com.bumptech.glide.request.target.FixedSizeDrawable
|
import com.bumptech.glide.request.target.FixedSizeDrawable
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.keylesspalace.tusky.appstore.*
|
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.CacheUpdater
|
||||||
|
import com.keylesspalace.tusky.appstore.Event
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||||
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
||||||
|
@ -61,15 +70,20 @@ import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.fragment.SFragment
|
|
||||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
|
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
||||||
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.removeShortcut
|
||||||
|
import com.keylesspalace.tusky.util.updateShortcut
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
|
@ -78,15 +92,30 @@ import com.mikepenz.materialdrawer.holder.BadgeStyle
|
||||||
import com.mikepenz.materialdrawer.holder.ColorHolder
|
import com.mikepenz.materialdrawer.holder.ColorHolder
|
||||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||||
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
||||||
import com.mikepenz.materialdrawer.model.*
|
import com.mikepenz.materialdrawer.model.AbstractDrawerItem
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
import com.mikepenz.materialdrawer.model.DividerDrawerItem
|
||||||
import com.mikepenz.materialdrawer.util.*
|
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||||
|
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||||
|
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||||
|
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
|
||||||
|
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||||
|
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||||
|
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
|
||||||
|
import com.mikepenz.materialdrawer.model.interfaces.iconRes
|
||||||
|
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
|
||||||
|
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
||||||
|
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
||||||
|
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||||
|
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||||
|
import com.mikepenz.materialdrawer.util.addItems
|
||||||
|
import com.mikepenz.materialdrawer.util.addItemsAtPosition
|
||||||
|
import com.mikepenz.materialdrawer.util.updateBadge
|
||||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
|
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
|
||||||
|
@ -102,9 +131,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var conversationRepository: ConversationsRepository
|
lateinit var conversationRepository: ConversationsRepository
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var appDb: AppDatabase
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var draftHelper: DraftHelper
|
lateinit var draftHelper: DraftHelper
|
||||||
|
|
||||||
|
@ -135,10 +161,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val activeAccount = accountManager.activeAccount
|
val activeAccount = accountManager.activeAccount
|
||||||
if (activeAccount == null) {
|
?: return // will be redirected to LoginActivity by BaseActivity
|
||||||
// will be redirected to LoginActivity by BaseActivity
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var showNotificationTab = false
|
var showNotificationTab = false
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
/** there are two possibilities the accountId can be passed to MainActivity:
|
/** there are two possibilities the accountId can be passed to MainActivity:
|
||||||
|
@ -163,7 +187,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
forwardShare(intent)
|
forwardShare(intent)
|
||||||
} else {
|
} else {
|
||||||
// No account was provided, show the chooser
|
// No account was provided, show the chooser
|
||||||
showAccountChooserDialog(getString(R.string.action_share_as), true, object : AccountSelectionListener {
|
showAccountChooserDialog(
|
||||||
|
getString(R.string.action_share_as), true,
|
||||||
|
object : AccountSelectionListener {
|
||||||
override fun onAccountSelected(account: AccountEntity) {
|
override fun onAccountSelected(account: AccountEntity) {
|
||||||
val requestedId = account.id
|
val requestedId = account.id
|
||||||
if (requestedId == activeAccount.id) {
|
if (requestedId == activeAccount.id) {
|
||||||
|
@ -175,7 +201,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
changeAccount(requestedId, intent)
|
changeAccount(requestedId, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if (accountRequested && savedInstanceState == null) {
|
} else if (accountRequested && savedInstanceState == null) {
|
||||||
// user clicked a notification, show notification tab
|
// user clicked a notification, show notification tab
|
||||||
|
@ -243,7 +270,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
// Flush old media that was cached for sharing
|
// Flush old media that was cached for sharing
|
||||||
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
||||||
}
|
}
|
||||||
draftWarning()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -331,12 +357,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
currentHiddenInList = true
|
currentHiddenInList = true
|
||||||
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) }
|
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) }
|
||||||
addProfile(ProfileSettingDrawerItem().apply {
|
addProfile(
|
||||||
|
ProfileSettingDrawerItem().apply {
|
||||||
identifier = DRAWER_ITEM_ADD_ACCOUNT
|
identifier = DRAWER_ITEM_ADD_ACCOUNT
|
||||||
nameRes = R.string.add_account_name
|
nameRes = R.string.add_account_name
|
||||||
descriptionRes = R.string.add_account_description
|
descriptionRes = R.string.add_account_description
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_add
|
iconicsIcon = GoogleMaterial.Icon.gmd_add
|
||||||
}, 0)
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
attachToSliderView(binding.mainDrawer)
|
attachToSliderView(binding.mainDrawer)
|
||||||
dividerBelowHeader = false
|
dividerBelowHeader = false
|
||||||
closeDrawerOnProfileListClick = true
|
closeDrawerOnProfileListClick = true
|
||||||
|
@ -417,7 +446,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.action_access_saved_toot
|
nameRes = R.string.action_access_drafts
|
||||||
iconRes = R.drawable.ic_notebook
|
iconRes = R.drawable.ic_notebook
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = DraftsActivity.newIntent(context)
|
val intent = DraftsActivity.newIntent(context)
|
||||||
|
@ -476,14 +505,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
)
|
)
|
||||||
|
|
||||||
if (addSearchButton) {
|
if (addSearchButton) {
|
||||||
binding.mainDrawer.addItemsAtPosition(4,
|
binding.mainDrawer.addItemsAtPosition(
|
||||||
|
4,
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.action_search
|
nameRes = R.string.action_search
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
|
startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setSavedInstance(savedInstanceState)
|
setSavedInstance(savedInstanceState)
|
||||||
|
@ -580,7 +611,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
binding.mainToolbar.setOnClickListener {
|
binding.mainToolbar.setOnClickListener {
|
||||||
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
|
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
|
||||||
|
@ -604,7 +634,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
private fun changeAccount(newSelectedId: Long, forward: Intent?) {
|
private fun changeAccount(newSelectedId: Long, forward: Intent?) {
|
||||||
cacheUpdater.stop()
|
cacheUpdater.stop()
|
||||||
SFragment.flushFilters()
|
|
||||||
accountManager.setActiveAccount(newSelectedId)
|
accountManager.setActiveAccount(newSelectedId)
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
@ -623,25 +652,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.action_logout)
|
.setTitle(R.string.action_logout)
|
||||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||||
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this)
|
lifecycleScope.launch {
|
||||||
|
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
|
||||||
cacheUpdater.clearForUser(activeAccount.id)
|
cacheUpdater.clearForUser(activeAccount.id)
|
||||||
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
||||||
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
|
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
|
||||||
removeShortcut(this, activeAccount)
|
removeShortcut(this@MainActivity, activeAccount)
|
||||||
val newAccount = accountManager.logActiveAccountOut()
|
val newAccount = accountManager.logActiveAccountOut()
|
||||||
if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
if (!NotificationHelper.areNotificationsEnabled(
|
||||||
NotificationHelper.disablePullNotifications(this)
|
this@MainActivity,
|
||||||
|
accountManager
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
NotificationHelper.disablePullNotifications(this@MainActivity)
|
||||||
}
|
}
|
||||||
val intent = if (newAccount == null) {
|
val intent = if (newAccount == null) {
|
||||||
LoginActivity.getIntent(this, false)
|
LoginActivity.getIntent(this@MainActivity, false)
|
||||||
} else {
|
} else {
|
||||||
Intent(this, MainActivity::class.java)
|
Intent(this@MainActivity, MainActivity::class.java)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishWithoutSlideOutAnimation()
|
finishWithoutSlideOutAnimation()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no, null)
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -679,6 +714,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
|
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
|
||||||
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
|
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
|
||||||
|
|
||||||
|
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
|
||||||
|
|
||||||
|
if (animateAvatars) {
|
||||||
glide.asDrawable()
|
glide.asDrawable()
|
||||||
.load(avatarUrl)
|
.load(avatarUrl)
|
||||||
.transform(
|
.transform(
|
||||||
|
@ -698,6 +736,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||||
|
if (resource is Animatable) {
|
||||||
|
resource.start()
|
||||||
|
}
|
||||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
|
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -707,6 +748,36 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
glide.asBitmap()
|
||||||
|
.load(avatarUrl)
|
||||||
|
.transform(
|
||||||
|
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
if (showPlaceholder) {
|
||||||
|
placeholder(R.drawable.avatar_default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
|
||||||
|
|
||||||
|
override fun onLoadStarted(placeholder: Drawable?) {
|
||||||
|
if (placeholder != null) {
|
||||||
|
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||||
|
binding.mainToolbar.navigationIcon = FixedSizeDrawable(BitmapDrawable(resources, resource), navIconSize, navIconSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadCleared(placeholder: Drawable?) {
|
||||||
|
if (placeholder != null) {
|
||||||
|
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAnnouncements() {
|
private fun fetchAnnouncements() {
|
||||||
|
@ -755,30 +826,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
header.setActiveProfile(accountManager.activeAccount!!.id)
|
header.setActiveProfile(accountManager.activeAccount!!.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun draftWarning() {
|
override fun getActionButton() = binding.composeButton
|
||||||
val sharedPrefsKey = "show_draft_warning"
|
|
||||||
appDb.tootDao().savedTootCount()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
|
||||||
.subscribe { draftCount ->
|
|
||||||
val showDraftWarning = preferences.getBoolean(sharedPrefsKey, true)
|
|
||||||
if (draftCount > 0 && showDraftWarning) {
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setMessage(R.string.new_drafts_warning)
|
|
||||||
.setNegativeButton("Don't show again") { _, _ ->
|
|
||||||
preferences.edit(commit = true) {
|
|
||||||
putBoolean(sharedPrefsKey, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getActionButton(): FloatingActionButton? = binding.composeButton
|
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
override fun androidInjector() = androidInjector
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||||
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||||
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
|
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
|
@ -29,8 +30,8 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
|
if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
|
||||||
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind
|
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind
|
||||||
?: TimelineFragment.Kind.HOME
|
?: TimelineViewModel.Kind.HOME
|
||||||
val argument = intent?.getStringExtra(ARG_ARG)
|
val argument = intent?.getStringExtra(ARG_ARG)
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
|
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
|
||||||
|
@ -47,13 +48,15 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
|
||||||
private const val ARG_ARG = "arg"
|
private const val ARG_ARG = "arg"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun newIntent(context: Context, kind: TimelineFragment.Kind,
|
fun newIntent(
|
||||||
argument: String?): Intent {
|
context: Context,
|
||||||
|
kind: TimelineViewModel.Kind,
|
||||||
|
argument: String?
|
||||||
|
): Intent {
|
||||||
val intent = Intent(context, ModalTimelineActivity::class.java)
|
val intent = Intent(context, ModalTimelineActivity::class.java)
|
||||||
intent.putExtra(ARG_KIND, kind)
|
intent.putExtra(ARG_KIND, kind)
|
||||||
intent.putExtra(ARG_ARG, argument)
|
intent.putExtra(ARG_ARG, argument)
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,209 +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;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.lifecycle.Lifecycle;
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import com.keylesspalace.tusky.adapter.SavedTootAdapter;
|
|
||||||
import com.keylesspalace.tusky.appstore.EventHub;
|
|
||||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
|
||||||
import com.keylesspalace.tusky.db.AppDatabase;
|
|
||||||
import com.keylesspalace.tusky.db.TootDao;
|
|
||||||
import com.keylesspalace.tusky.db.TootEntity;
|
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
|
||||||
import com.keylesspalace.tusky.util.SaveTootHelper;
|
|
||||||
import com.keylesspalace.tusky.view.BackgroundMessageView;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
|
|
||||||
import static com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
|
||||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
|
||||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
|
||||||
|
|
||||||
public final class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction,
|
|
||||||
Injectable {
|
|
||||||
|
|
||||||
// ui
|
|
||||||
private SavedTootAdapter adapter;
|
|
||||||
private BackgroundMessageView errorMessageView;
|
|
||||||
|
|
||||||
private List<TootEntity> toots = new ArrayList<>();
|
|
||||||
@Nullable
|
|
||||||
private AsyncTask<?, ?, ?> asyncTask;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
EventHub eventHub;
|
|
||||||
@Inject
|
|
||||||
AppDatabase database;
|
|
||||||
@Inject
|
|
||||||
SaveTootHelper saveTootHelper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
eventHub.getEvents()
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.ofType(StatusComposedEvent.class)
|
|
||||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
|
||||||
.subscribe((__) -> this.fetchToots());
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_saved_toot);
|
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
ActionBar bar = getSupportActionBar();
|
|
||||||
if (bar != null) {
|
|
||||||
bar.setTitle(getString(R.string.title_drafts));
|
|
||||||
bar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
bar.setDisplayShowHomeEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
RecyclerView recyclerView = findViewById(R.id.recyclerView);
|
|
||||||
errorMessageView = findViewById(R.id.errorMessageView);
|
|
||||||
recyclerView.setHasFixedSize(true);
|
|
||||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
|
||||||
DividerItemDecoration divider = new DividerItemDecoration(
|
|
||||||
this, layoutManager.getOrientation());
|
|
||||||
recyclerView.addItemDecoration(divider);
|
|
||||||
adapter = new SavedTootAdapter(this);
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
fetchToots();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
if (asyncTask != null) asyncTask.cancel(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchToots() {
|
|
||||||
asyncTask = new FetchPojosTask(this, database.tootDao())
|
|
||||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setNoContent(int size) {
|
|
||||||
if (size == 0) {
|
|
||||||
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status, null);
|
|
||||||
errorMessageView.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
errorMessageView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(int position, TootEntity item) {
|
|
||||||
|
|
||||||
saveTootHelper.deleteDraft(item);
|
|
||||||
|
|
||||||
toots.remove(position);
|
|
||||||
// update adapter
|
|
||||||
if (adapter != null) {
|
|
||||||
adapter.removeItem(position);
|
|
||||||
setNoContent(toots.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void click(int position, TootEntity item) {
|
|
||||||
Gson gson = new Gson();
|
|
||||||
Type stringListType = new TypeToken<List<String>>() {}.getType();
|
|
||||||
List<String> jsonUrls = gson.fromJson(item.getUrls(), stringListType);
|
|
||||||
List<String> descriptions = gson.fromJson(item.getDescriptions(), stringListType);
|
|
||||||
|
|
||||||
ComposeOptions composeOptions = new ComposeOptions(
|
|
||||||
/*scheduledTootUid*/null,
|
|
||||||
item.getUid(),
|
|
||||||
/*drafId*/null,
|
|
||||||
item.getText(),
|
|
||||||
jsonUrls,
|
|
||||||
descriptions,
|
|
||||||
/*mentionedUsernames*/null,
|
|
||||||
item.getInReplyToId(),
|
|
||||||
/*replyVisibility*/null,
|
|
||||||
item.getVisibility(),
|
|
||||||
item.getContentWarning(),
|
|
||||||
item.getInReplyToUsername(),
|
|
||||||
item.getInReplyToText(),
|
|
||||||
/*mediaAttachments*/null,
|
|
||||||
/*draftAttachments*/null,
|
|
||||||
/*scheduledAt*/null,
|
|
||||||
/*sensitive*/null,
|
|
||||||
/*poll*/null,
|
|
||||||
/* modifiedInitialState */ true
|
|
||||||
);
|
|
||||||
Intent intent = ComposeActivity.startIntent(this, composeOptions);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class FetchPojosTask extends AsyncTask<Void, Void, List<TootEntity>> {
|
|
||||||
|
|
||||||
private final WeakReference<SavedTootActivity> activityRef;
|
|
||||||
private final TootDao tootDao;
|
|
||||||
|
|
||||||
FetchPojosTask(SavedTootActivity activity, TootDao tootDao) {
|
|
||||||
this.activityRef = new WeakReference<>(activity);
|
|
||||||
this.tootDao = tootDao;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<TootEntity> doInBackground(Void... voids) {
|
|
||||||
return tootDao.loadAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(List<TootEntity> pojos) {
|
|
||||||
super.onPostExecute(pojos);
|
|
||||||
SavedTootActivity activity = activityRef.get();
|
|
||||||
if (activity == null) return;
|
|
||||||
|
|
||||||
activity.toots.clear();
|
|
||||||
activity.toots.addAll(pojos);
|
|
||||||
|
|
||||||
// set ui
|
|
||||||
activity.setNoContent(pojos.size());
|
|
||||||
activity.adapter.setItems(activity.toots);
|
|
||||||
activity.adapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,10 +18,9 @@ package com.keylesspalace.tusky
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SplashActivity : AppCompatActivity(), Injectable {
|
class SplashActivity : AppCompatActivity(), Injectable {
|
||||||
|
@ -46,5 +45,4 @@ class SplashActivity : AppCompatActivity(), Injectable {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,12 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||||
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
|
||||||
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
|
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
|
||||||
|
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment.Kind
|
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
|
@ -60,7 +57,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
val fragment = TimelineFragment.newInstance(kind)
|
val fragment = TimelineFragment.newInstance(kind)
|
||||||
replace(R.id.fragment_container, fragment)
|
replace(R.id.fragment_container, fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = dispatchingAndroidInjector
|
override fun androidInjector() = dispatchingAndroidInjector
|
||||||
|
@ -81,5 +77,4 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,9 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||||
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||||
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||||
import com.keylesspalace.tusky.fragment.NotificationsFragment
|
import com.keylesspalace.tusky.fragment.NotificationsFragment
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
|
||||||
|
|
||||||
/** this would be a good case for a sealed class, but that does not work nice with Room */
|
/** this would be a good case for a sealed class, but that does not work nice with Room */
|
||||||
|
|
||||||
|
@ -33,7 +34,8 @@ const val DIRECT = "Direct"
|
||||||
const val HASHTAG = "Hashtag"
|
const val HASHTAG = "Hashtag"
|
||||||
const val LIST = "List"
|
const val LIST = "List"
|
||||||
|
|
||||||
data class TabData(val id: String,
|
data class TabData(
|
||||||
|
val id: String,
|
||||||
@StringRes val text: Int,
|
@StringRes val text: Int,
|
||||||
@DrawableRes val icon: Int,
|
@DrawableRes val icon: Int,
|
||||||
val fragment: (List<String>) -> Fragment,
|
val fragment: (List<String>) -> Fragment,
|
||||||
|
@ -47,7 +49,7 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
||||||
HOME,
|
HOME,
|
||||||
R.string.title_home,
|
R.string.title_home,
|
||||||
R.drawable.ic_home_24dp,
|
R.drawable.ic_home_24dp,
|
||||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.HOME) }
|
{ TimelineFragment.newInstance(TimelineViewModel.Kind.HOME) }
|
||||||
)
|
)
|
||||||
NOTIFICATIONS -> TabData(
|
NOTIFICATIONS -> TabData(
|
||||||
NOTIFICATIONS,
|
NOTIFICATIONS,
|
||||||
|
@ -59,13 +61,13 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
||||||
LOCAL,
|
LOCAL,
|
||||||
R.string.title_public_local,
|
R.string.title_public_local,
|
||||||
R.drawable.ic_local_24dp,
|
R.drawable.ic_local_24dp,
|
||||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL) }
|
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL) }
|
||||||
)
|
)
|
||||||
FEDERATED -> TabData(
|
FEDERATED -> TabData(
|
||||||
FEDERATED,
|
FEDERATED,
|
||||||
R.string.title_public_federated,
|
R.string.title_public_federated,
|
||||||
R.drawable.ic_public_24dp,
|
R.drawable.ic_public_24dp,
|
||||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED) }
|
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED) }
|
||||||
)
|
)
|
||||||
DIRECT -> TabData(
|
DIRECT -> TabData(
|
||||||
DIRECT,
|
DIRECT,
|
||||||
|
@ -85,7 +87,7 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
||||||
LIST,
|
LIST,
|
||||||
R.string.list,
|
R.string.list,
|
||||||
R.drawable.ic_list,
|
R.drawable.ic_list,
|
||||||
{ args -> TimelineFragment.newInstance(TimelineFragment.Kind.LIST, args.getOrNull(0).orEmpty()) },
|
{ args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) },
|
||||||
arguments,
|
arguments,
|
||||||
{ arguments.getOrNull(1).orEmpty() }
|
{ arguments.getOrNull(1).orEmpty() }
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,6 +31,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
|
import autodispose2.autoDispose
|
||||||
import com.google.android.material.transition.MaterialArcMotion
|
import com.google.android.material.transition.MaterialArcMotion
|
||||||
import com.google.android.material.transition.MaterialContainerTransform
|
import com.google.android.material.transition.MaterialContainerTransform
|
||||||
import com.keylesspalace.tusky.adapter.ItemInteractionListener
|
import com.keylesspalace.tusky.adapter.ItemInteractionListener
|
||||||
|
@ -44,11 +46,9 @@ import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.onTextChanged
|
import com.keylesspalace.tusky.util.onTextChanged
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import com.uber.autodispose.autoDispose
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.Single
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -333,7 +333,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
|
||||||
}
|
}
|
||||||
tabsChanged = true
|
tabsChanged = true
|
||||||
}
|
}
|
||||||
|
@ -357,5 +356,4 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
private const val MIN_TAB_COUNT = 2
|
private const val MIN_TAB_COUNT = 2
|
||||||
private const val MAX_TAB_COUNT = 5
|
private const val MAX_TAB_COUNT = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,16 +22,16 @@ import android.util.Log
|
||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import autodispose2.AutoDisposePlugins
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
||||||
import com.keylesspalace.tusky.di.AppInjector
|
import com.keylesspalace.tusky.di.AppInjector
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
import com.keylesspalace.tusky.util.EmojiCompatFont
|
||||||
import com.keylesspalace.tusky.util.LocaleManager
|
import com.keylesspalace.tusky.util.LocaleManager
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.uber.autodispose.AutoDisposePlugins
|
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
import io.reactivex.plugins.RxJavaPlugins
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
|
@ -41,27 +41,27 @@ import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||||
|
import autodispose2.autoDispose
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.FutureTarget
|
import com.bumptech.glide.request.FutureTarget
|
||||||
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
||||||
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
|
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.fragment.ViewImageFragment
|
import com.keylesspalace.tusky.fragment.ViewImageFragment
|
||||||
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
|
||||||
import com.keylesspalace.tusky.pager.ImagePagerAdapter
|
import com.keylesspalace.tusky.pager.ImagePagerAdapter
|
||||||
|
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
||||||
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
|
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import com.uber.autodispose.autoDispose
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.Single
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
|
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
|
||||||
|
|
||||||
|
@ -102,7 +102,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
|
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
|
||||||
// Setup the view pager.
|
// Setup the view pager.
|
||||||
ImagePagerAdapter(this, realAttachs, initialPosition)
|
ImagePagerAdapter(this, realAttachs, initialPosition)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
|
imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
|
||||||
?: throw IllegalArgumentException("attachment list or image url has to be set")
|
?: throw IllegalArgumentException("attachment list or image url has to be set")
|
||||||
|
@ -138,6 +137,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
}
|
}
|
||||||
|
|
||||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
|
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
|
||||||
|
|
||||||
window.statusBarColor = Color.BLACK
|
window.statusBarColor = Color.BLACK
|
||||||
window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
|
window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
|
||||||
override fun onTransitionEnd(transition: Transition) {
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
@ -205,8 +205,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
|
|
||||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
val request = DownloadManager.Request(Uri.parse(url))
|
||||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES,
|
request.setDestinationInExternalPublicDir(
|
||||||
getString(R.string.app_name) + "/" + filename)
|
Environment.DIRECTORY_PICTURES,
|
||||||
|
getString(R.string.app_name) + "/" + filename
|
||||||
|
)
|
||||||
downloadManager.enqueue(request)
|
downloadManager.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,7 +262,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
|
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private var isCreating: Boolean = false
|
private var isCreating: Boolean = false
|
||||||
|
|
||||||
private fun shareImage(directory: File, url: String) {
|
private fun shareImage(directory: File, url: String) {
|
||||||
|
@ -283,7 +284,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
Log.e(TAG, "Error writing temporary media.")
|
Log.e(TAG, "Error writing temporary media.")
|
||||||
}
|
}
|
||||||
return@fromCallable false
|
return@fromCallable false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
@ -308,7 +308,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
Log.e(TAG, "Failed to download image", error)
|
Log.e(TAG, "Failed to download image", error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareMediaFile(directory: File, url: String) {
|
private fun shareMediaFile(directory: File, url: String) {
|
||||||
|
|
|
@ -25,7 +25,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
|
|
@ -1,116 +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.adapter;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
|
||||||
import com.keylesspalace.tusky.util.ListUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
|
||||||
static final int VIEW_TYPE_ACCOUNT = 0;
|
|
||||||
static final int VIEW_TYPE_FOOTER = 1;
|
|
||||||
|
|
||||||
List<Account> accountList;
|
|
||||||
AccountActionListener accountActionListener;
|
|
||||||
private boolean bottomLoading;
|
|
||||||
protected final boolean animateEmojis;
|
|
||||||
protected final boolean animateAvatar;
|
|
||||||
|
|
||||||
AccountAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
|
||||||
this.accountList = new ArrayList<>();
|
|
||||||
this.accountActionListener = accountActionListener;
|
|
||||||
this.animateAvatar = animateAvatar;
|
|
||||||
this.animateEmojis = animateEmojis;
|
|
||||||
bottomLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return accountList.size() + (bottomLoading ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
if (position == accountList.size() && bottomLoading) {
|
|
||||||
return VIEW_TYPE_FOOTER;
|
|
||||||
} else {
|
|
||||||
return VIEW_TYPE_ACCOUNT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(@NonNull List<Account> newAccounts) {
|
|
||||||
accountList = ListUtils.removeDuplicates(newAccounts);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addItems(@NonNull List<Account> newAccounts) {
|
|
||||||
int end = accountList.size();
|
|
||||||
Account last = accountList.get(end - 1);
|
|
||||||
if (last != null && !findAccount(newAccounts, last.getId())) {
|
|
||||||
accountList.addAll(newAccounts);
|
|
||||||
notifyItemRangeInserted(end, newAccounts.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBottomLoading(boolean loading) {
|
|
||||||
boolean wasLoading = bottomLoading;
|
|
||||||
if(wasLoading == loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bottomLoading = loading;
|
|
||||||
if(loading) {
|
|
||||||
notifyItemInserted(accountList.size());
|
|
||||||
} else {
|
|
||||||
notifyItemRemoved(accountList.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean findAccount(@NonNull List<Account> accounts, String id) {
|
|
||||||
for (Account account : accounts) {
|
|
||||||
if (account.getId().equals(id)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Account removeItem(int position) {
|
|
||||||
if (position < 0 || position >= accountList.size()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Account account = accountList.remove(position);
|
|
||||||
notifyItemRemoved(position);
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addItem(@NonNull Account account, int position) {
|
|
||||||
if (position < 0 || position > accountList.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
accountList.add(position, account);
|
|
||||||
notifyItemInserted(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
/* Copyright 2021 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.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
|
import com.keylesspalace.tusky.util.removeDuplicates
|
||||||
|
|
||||||
|
/** Generic adapter with bottom loading indicator. */
|
||||||
|
abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructor(
|
||||||
|
var accountActionListener: AccountActionListener,
|
||||||
|
protected val animateAvatar: Boolean,
|
||||||
|
protected val animateEmojis: Boolean
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
|
||||||
|
var accountList = mutableListOf<Account>()
|
||||||
|
private var bottomLoading: Boolean = false
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return accountList.size + if (bottomLoading) 1 else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun createAccountViewHolder(parent: ViewGroup): AVH
|
||||||
|
|
||||||
|
abstract fun onBindAccountViewHolder(viewHolder: AVH, position: Int)
|
||||||
|
|
||||||
|
final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
this.onBindAccountViewHolder(holder as AVH, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): RecyclerView.ViewHolder {
|
||||||
|
return when (viewType) {
|
||||||
|
VIEW_TYPE_ACCOUNT -> this.createAccountViewHolder(parent)
|
||||||
|
VIEW_TYPE_FOOTER -> this.createFooterViewHolder(parent)
|
||||||
|
else -> error("Unknown item type: $viewType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFooterViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
): RecyclerView.ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_footer, parent, false)
|
||||||
|
return LoadingFooterViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return if (position == accountList.size && bottomLoading) {
|
||||||
|
VIEW_TYPE_FOOTER
|
||||||
|
} else {
|
||||||
|
VIEW_TYPE_ACCOUNT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(newAccounts: List<Account>) {
|
||||||
|
accountList = removeDuplicates(newAccounts)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItems(newAccounts: List<Account>) {
|
||||||
|
val end = accountList.size
|
||||||
|
val last = accountList[end - 1]
|
||||||
|
if (newAccounts.none { it.id == last.id }) {
|
||||||
|
accountList.addAll(newAccounts)
|
||||||
|
notifyItemRangeInserted(end, newAccounts.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBottomLoading(loading: Boolean) {
|
||||||
|
val wasLoading = bottomLoading
|
||||||
|
if (wasLoading == loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bottomLoading = loading
|
||||||
|
if (loading) {
|
||||||
|
notifyItemInserted(accountList.size)
|
||||||
|
} else {
|
||||||
|
notifyItemRemoved(accountList.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeItem(position: Int): Account? {
|
||||||
|
if (position < 0 || position >= accountList.size) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val account = accountList.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItem(account: Account, position: Int) {
|
||||||
|
if (position < 0 || position > accountList.size) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accountList.add(position, account)
|
||||||
|
notifyItemInserted(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val VIEW_TYPE_ACCOUNT = 0
|
||||||
|
const val VIEW_TYPE_FOOTER = 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,9 +82,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MutableStringPair(var first: String, var second: String)
|
class MutableStringPair(var first: String, var second: String)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,8 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
|
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
|
||||||
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
|
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
|
||||||
|
|
||||||
|
@ -48,7 +49,6 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
|
||||||
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
|
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
|
||||||
|
|
||||||
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
|
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
|
|
@ -1,106 +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.adapter;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
|
||||||
|
|
||||||
public class BlocksAdapter extends AccountAdapter {
|
|
||||||
|
|
||||||
public BlocksAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
|
||||||
super(accountActionListener, animateAvatar, animateEmojis);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
switch (viewType) {
|
|
||||||
default:
|
|
||||||
case VIEW_TYPE_ACCOUNT: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_blocked_user, parent, false);
|
|
||||||
return new BlockedUserViewHolder(view);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOOTER: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
|
||||||
return new LoadingFooterViewHolder(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
|
||||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
|
||||||
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
|
|
||||||
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
|
|
||||||
holder.setupActionListener(accountActionListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class BlockedUserViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
private ImageView avatar;
|
|
||||||
private TextView username;
|
|
||||||
private TextView displayName;
|
|
||||||
private ImageButton unblock;
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
BlockedUserViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
avatar = itemView.findViewById(R.id.blocked_user_avatar);
|
|
||||||
username = itemView.findViewById(R.id.blocked_user_username);
|
|
||||||
displayName = itemView.findViewById(R.id.blocked_user_display_name);
|
|
||||||
unblock = itemView.findViewById(R.id.blocked_user_unblock);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
|
|
||||||
id = account.getId();
|
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
|
||||||
displayName.setText(emojifiedName);
|
|
||||||
String format = username.getContext().getString(R.string.status_username_format);
|
|
||||||
String formattedUsername = String.format(format, account.getUsername());
|
|
||||||
username.setText(formattedUsername);
|
|
||||||
int avatarRadius = avatar.getContext().getResources()
|
|
||||||
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
|
||||||
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupActionListener(final AccountActionListener listener) {
|
|
||||||
unblock.setOnClickListener(v -> {
|
|
||||||
int position = getBindingAdapterPosition();
|
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
|
||||||
listener.onBlock(false, id, position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
itemView.setOnClickListener(v -> listener.onViewAccount(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/* 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.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
|
||||||
|
/** Displays a list of blocked accounts. */
|
||||||
|
class BlocksAdapter(
|
||||||
|
accountActionListener: AccountActionListener,
|
||||||
|
animateAvatar: Boolean,
|
||||||
|
animateEmojis: Boolean
|
||||||
|
) : AccountAdapter<BlocksAdapter.BlockedUserViewHolder>(
|
||||||
|
accountActionListener,
|
||||||
|
animateAvatar,
|
||||||
|
animateEmojis
|
||||||
|
) {
|
||||||
|
override fun createAccountViewHolder(parent: ViewGroup): BlockedUserViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_blocked_user, parent, false)
|
||||||
|
return BlockedUserViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindAccountViewHolder(viewHolder: BlockedUserViewHolder, position: Int) {
|
||||||
|
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
|
||||||
|
viewHolder.setupActionListener(accountActionListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val avatar: ImageView = itemView.findViewById(R.id.blocked_user_avatar)
|
||||||
|
private val username: TextView = itemView.findViewById(R.id.blocked_user_username)
|
||||||
|
private val displayName: TextView = itemView.findViewById(R.id.blocked_user_display_name)
|
||||||
|
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
|
||||||
|
private var id: String? = null
|
||||||
|
|
||||||
|
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||||
|
id = account.id
|
||||||
|
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||||
|
displayName.text = emojifiedName
|
||||||
|
val format = username.context.getString(R.string.status_username_format)
|
||||||
|
val formattedUsername = String.format(format, account.username)
|
||||||
|
username.text = formattedUsername
|
||||||
|
val avatarRadius = avatar.context.resources
|
||||||
|
.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||||
|
loadAvatar(account.avatar, avatar, avatarRadius, animateAvatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setupActionListener(listener: AccountActionListener) {
|
||||||
|
unblock.setOnClickListener {
|
||||||
|
val position = bindingAdapterPosition
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
listener.onBlock(false, id, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemView.setOnClickListener { listener.onViewAccount(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ import com.bumptech.glide.Glide
|
||||||
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
|
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
class EmojiAdapter(
|
class EmojiAdapter(
|
||||||
emojiList: List<Emoji>,
|
emojiList: List<Emoji>,
|
||||||
|
@ -30,7 +30,7 @@ class EmojiAdapter(
|
||||||
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
|
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
|
||||||
|
|
||||||
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
|
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
|
||||||
.sortedBy { it.shortcode.toLowerCase(Locale.ROOT) }
|
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
|
||||||
|
|
||||||
override fun getItemCount() = emojiList.size
|
override fun getItemCount() = emojiList.size
|
||||||
|
|
||||||
|
|
|
@ -1,61 +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.adapter;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
|
||||||
|
|
||||||
/** Both for follows and following lists. */
|
|
||||||
public class FollowAdapter extends AccountAdapter {
|
|
||||||
|
|
||||||
public FollowAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
|
||||||
super(accountActionListener, animateAvatar, animateEmojis);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
switch (viewType) {
|
|
||||||
default:
|
|
||||||
case VIEW_TYPE_ACCOUNT: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_account, parent, false);
|
|
||||||
return new AccountViewHolder(view);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOOTER: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
|
||||||
return new LoadingFooterViewHolder(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
|
||||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
|
||||||
AccountViewHolder holder = (AccountViewHolder) viewHolder;
|
|
||||||
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
|
|
||||||
holder.setupActionListener(accountActionListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/* Copyright 2021 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.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
|
|
||||||
|
/** Displays either a follows or following list. */
|
||||||
|
class FollowAdapter(
|
||||||
|
accountActionListener: AccountActionListener,
|
||||||
|
animateAvatar: Boolean,
|
||||||
|
animateEmojis: Boolean
|
||||||
|
) : AccountAdapter<AccountViewHolder>(accountActionListener, animateAvatar, animateEmojis) {
|
||||||
|
override fun createAccountViewHolder(parent: ViewGroup): AccountViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_account, parent, false)
|
||||||
|
return AccountViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindAccountViewHolder(viewHolder: AccountViewHolder, position: Int) {
|
||||||
|
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
|
||||||
|
viewHolder.setupActionListener(accountActionListener)
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,10 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
import com.keylesspalace.tusky.util.unicodeWrap
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
|
||||||
class FollowRequestViewHolder(
|
class FollowRequestViewHolder(
|
||||||
private val binding: ItemFollowRequestBinding,
|
private val binding: ItemFollowRequestBinding,
|
||||||
|
|
|
@ -1,60 +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.adapter;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
|
||||||
|
|
||||||
public class FollowRequestsAdapter extends AccountAdapter {
|
|
||||||
|
|
||||||
public FollowRequestsAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
|
||||||
super(accountActionListener, animateAvatar, animateEmojis);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
switch (viewType) {
|
|
||||||
default:
|
|
||||||
case VIEW_TYPE_ACCOUNT: {
|
|
||||||
ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
|
||||||
return new FollowRequestViewHolder(binding, false);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOOTER: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
|
||||||
return new LoadingFooterViewHolder(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
|
||||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
|
||||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
|
||||||
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
|
|
||||||
holder.setupActionListener(accountActionListener, accountList.get(position).getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* Copyright 2021 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.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
|
|
||||||
|
/** Displays a list of follow requests with accept/reject buttons. */
|
||||||
|
class FollowRequestsAdapter(
|
||||||
|
accountActionListener: AccountActionListener,
|
||||||
|
animateAvatar: Boolean,
|
||||||
|
animateEmojis: Boolean
|
||||||
|
) : AccountAdapter<FollowRequestViewHolder>(accountActionListener, animateAvatar, animateEmojis) {
|
||||||
|
override fun createAccountViewHolder(parent: ViewGroup): FollowRequestViewHolder {
|
||||||
|
val binding = ItemFollowRequestBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
return FollowRequestViewHolder(binding, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindAccountViewHolder(viewHolder: FollowRequestViewHolder, position: Int) {
|
||||||
|
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
|
||||||
|
viewHolder.setupActionListener(accountActionListener, accountList[position].id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,6 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = if (accountLocked) 0 else 1
|
override fun getItemCount() = if (accountLocked) 0 else 1
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)
|
class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter
|
package com.keylesspalace.tusky.adapter
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
|
@ -1,131 +0,0 @@
|
||||||
package com.keylesspalace.tusky.adapter;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
public class MutesAdapter extends AccountAdapter {
|
|
||||||
private HashMap<String, Boolean> mutingNotificationsMap;
|
|
||||||
|
|
||||||
public MutesAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
|
||||||
super(accountActionListener, animateAvatar, animateEmojis);
|
|
||||||
mutingNotificationsMap = new HashMap<String, Boolean>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
switch (viewType) {
|
|
||||||
default:
|
|
||||||
case VIEW_TYPE_ACCOUNT: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_muted_user, parent, false);
|
|
||||||
return new MutesAdapter.MutedUserViewHolder(view);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOOTER: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
|
||||||
return new LoadingFooterViewHolder(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
|
||||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
|
||||||
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
|
|
||||||
Account account = accountList.get(position);
|
|
||||||
holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId()), animateAvatar, animateEmojis);
|
|
||||||
holder.setupActionListener(accountActionListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateMutingNotifications(String id, boolean mutingNotifications, int position) {
|
|
||||||
mutingNotificationsMap.put(id, mutingNotifications);
|
|
||||||
notifyItemChanged(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateMutingNotificationsMap(HashMap<String, Boolean> newMutingNotificationsMap) {
|
|
||||||
mutingNotificationsMap.putAll(newMutingNotificationsMap);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class MutedUserViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
private ImageView avatar;
|
|
||||||
private TextView username;
|
|
||||||
private TextView displayName;
|
|
||||||
private ImageButton unmute;
|
|
||||||
private ImageButton muteNotifications;
|
|
||||||
private String id;
|
|
||||||
private boolean notifications;
|
|
||||||
|
|
||||||
MutedUserViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
avatar = itemView.findViewById(R.id.muted_user_avatar);
|
|
||||||
username = itemView.findViewById(R.id.muted_user_username);
|
|
||||||
displayName = itemView.findViewById(R.id.muted_user_display_name);
|
|
||||||
unmute = itemView.findViewById(R.id.muted_user_unmute);
|
|
||||||
muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupWithAccount(Account account, Boolean mutingNotifications, boolean animateAvatar, boolean animateEmojis) {
|
|
||||||
id = account.getId();
|
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
|
||||||
displayName.setText(emojifiedName);
|
|
||||||
String format = username.getContext().getString(R.string.status_username_format);
|
|
||||||
String formattedUsername = String.format(format, account.getUsername());
|
|
||||||
username.setText(formattedUsername);
|
|
||||||
int avatarRadius = avatar.getContext().getResources()
|
|
||||||
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
|
||||||
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
|
|
||||||
|
|
||||||
String unmuteString = unmute.getContext().getString(R.string.action_unmute_desc, formattedUsername);
|
|
||||||
unmute.setContentDescription(unmuteString);
|
|
||||||
ViewCompat.setTooltipText(unmute, unmuteString);
|
|
||||||
|
|
||||||
if (mutingNotifications == null) {
|
|
||||||
muteNotifications.setEnabled(false);
|
|
||||||
notifications = true;
|
|
||||||
} else {
|
|
||||||
muteNotifications.setEnabled(true);
|
|
||||||
notifications = mutingNotifications;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notifications) {
|
|
||||||
muteNotifications.setImageResource(R.drawable.ic_notifications_24dp);
|
|
||||||
String unmuteNotificationsString = muteNotifications.getContext()
|
|
||||||
.getString(R.string.action_unmute_notifications_desc, formattedUsername);
|
|
||||||
muteNotifications.setContentDescription(unmuteNotificationsString);
|
|
||||||
ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString);
|
|
||||||
} else {
|
|
||||||
muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp);
|
|
||||||
String muteNotificationsString = muteNotifications.getContext()
|
|
||||||
.getString(R.string.action_mute_notifications_desc, formattedUsername);
|
|
||||||
muteNotifications.setContentDescription(muteNotificationsString);
|
|
||||||
ViewCompat.setTooltipText(muteNotifications, muteNotificationsString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupActionListener(final AccountActionListener listener) {
|
|
||||||
unmute.setOnClickListener(v -> listener.onMute(false, id, getBindingAdapterPosition(), false));
|
|
||||||
muteNotifications.setOnClickListener(
|
|
||||||
v -> listener.onMute(true, id, getBindingAdapterPosition(), !notifications));
|
|
||||||
itemView.setOnClickListener(v -> listener.onViewAccount(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
package com.keylesspalace.tusky.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
import java.util.HashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a list of muted accounts with mute/unmute account and mute/unmute notifications
|
||||||
|
* buttons.
|
||||||
|
* */
|
||||||
|
class MutesAdapter(
|
||||||
|
accountActionListener: AccountActionListener,
|
||||||
|
animateAvatar: Boolean,
|
||||||
|
animateEmojis: Boolean
|
||||||
|
) : AccountAdapter<MutesAdapter.MutedUserViewHolder>(
|
||||||
|
accountActionListener,
|
||||||
|
animateAvatar,
|
||||||
|
animateEmojis
|
||||||
|
) {
|
||||||
|
private val mutingNotificationsMap = HashMap<String, Boolean>()
|
||||||
|
|
||||||
|
override fun createAccountViewHolder(parent: ViewGroup): MutedUserViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_muted_user, parent, false)
|
||||||
|
return MutedUserViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindAccountViewHolder(viewHolder: MutedUserViewHolder, position: Int) {
|
||||||
|
val account = accountList[position]
|
||||||
|
viewHolder.setupWithAccount(
|
||||||
|
account,
|
||||||
|
mutingNotificationsMap[account.id],
|
||||||
|
animateAvatar,
|
||||||
|
animateEmojis
|
||||||
|
)
|
||||||
|
viewHolder.setupActionListener(accountActionListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMutingNotifications(id: String, mutingNotifications: Boolean, position: Int) {
|
||||||
|
mutingNotificationsMap[id] = mutingNotifications
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMutingNotificationsMap(newMutingNotificationsMap: HashMap<String, Boolean>?) {
|
||||||
|
mutingNotificationsMap.putAll(newMutingNotificationsMap!!)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MutedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val avatar: ImageView = itemView.findViewById(R.id.muted_user_avatar)
|
||||||
|
private val username: TextView = itemView.findViewById(R.id.muted_user_username)
|
||||||
|
private val displayName: TextView = itemView.findViewById(R.id.muted_user_display_name)
|
||||||
|
private val unmute: ImageButton = itemView.findViewById(R.id.muted_user_unmute)
|
||||||
|
private val muteNotifications: ImageButton =
|
||||||
|
itemView.findViewById(R.id.muted_user_mute_notifications)
|
||||||
|
|
||||||
|
private var id: String? = null
|
||||||
|
private var notifications = false
|
||||||
|
|
||||||
|
fun setupWithAccount(
|
||||||
|
account: Account,
|
||||||
|
mutingNotifications: Boolean?,
|
||||||
|
animateAvatar: Boolean,
|
||||||
|
animateEmojis: Boolean
|
||||||
|
) {
|
||||||
|
id = account.id
|
||||||
|
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||||
|
displayName.text = emojifiedName
|
||||||
|
val format = username.context.getString(R.string.status_username_format)
|
||||||
|
val formattedUsername = String.format(format, account.username)
|
||||||
|
username.text = formattedUsername
|
||||||
|
val avatarRadius = avatar.context.resources
|
||||||
|
.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||||
|
loadAvatar(account.avatar, avatar, avatarRadius, animateAvatar)
|
||||||
|
val unmuteString =
|
||||||
|
unmute.context.getString(R.string.action_unmute_desc, formattedUsername)
|
||||||
|
unmute.contentDescription = unmuteString
|
||||||
|
ViewCompat.setTooltipText(unmute, unmuteString)
|
||||||
|
if (mutingNotifications == null) {
|
||||||
|
muteNotifications.isEnabled = false
|
||||||
|
notifications = true
|
||||||
|
} else {
|
||||||
|
muteNotifications.isEnabled = true
|
||||||
|
notifications = mutingNotifications
|
||||||
|
}
|
||||||
|
if (notifications) {
|
||||||
|
muteNotifications.setImageResource(R.drawable.ic_notifications_24dp)
|
||||||
|
val unmuteNotificationsString = muteNotifications.context
|
||||||
|
.getString(R.string.action_unmute_notifications_desc, formattedUsername)
|
||||||
|
muteNotifications.contentDescription = unmuteNotificationsString
|
||||||
|
ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString)
|
||||||
|
} else {
|
||||||
|
muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp)
|
||||||
|
val muteNotificationsString = muteNotifications.context
|
||||||
|
.getString(R.string.action_mute_notifications_desc, formattedUsername)
|
||||||
|
muteNotifications.contentDescription = muteNotificationsString
|
||||||
|
ViewCompat.setTooltipText(muteNotifications, muteNotificationsString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setupActionListener(listener: AccountActionListener) {
|
||||||
|
unmute.setOnClickListener {
|
||||||
|
listener.onMute(
|
||||||
|
false,
|
||||||
|
id,
|
||||||
|
bindingAdapterPosition,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
muteNotifications.setOnClickListener {
|
||||||
|
listener.onMute(
|
||||||
|
true,
|
||||||
|
id,
|
||||||
|
bindingAdapterPosition,
|
||||||
|
!notifications
|
||||||
|
)
|
||||||
|
}
|
||||||
|
itemView.setOnClickListener { listener.onViewAccount(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,30 +15,28 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter
|
package com.keylesspalace.tusky.adapter
|
||||||
|
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
||||||
import com.keylesspalace.tusky.util.NetworkState
|
|
||||||
import com.keylesspalace.tusky.util.Status
|
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
|
||||||
class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding,
|
class NetworkStateViewHolder(
|
||||||
private val retryCallback: () -> Unit)
|
private val binding: ItemNetworkStateBinding,
|
||||||
: RecyclerView.ViewHolder(binding.root) {
|
private val retryCallback: () -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) {
|
fun setUpWithNetworkState(state: LoadState) {
|
||||||
binding.progressBar.visible(state?.status == Status.RUNNING)
|
binding.progressBar.visible(state == LoadState.Loading)
|
||||||
binding.retryButton.visible(state?.status == Status.FAILED)
|
binding.retryButton.visible(state is LoadState.Error)
|
||||||
binding.errorMsg.visible(state?.msg != null)
|
val msg = if (state is LoadState.Error) {
|
||||||
binding.errorMsg.text = state?.msg
|
state.error.message
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
binding.errorMsg.visible(msg != null)
|
||||||
|
binding.errorMsg.text = msg
|
||||||
binding.retryButton.setOnClickListener {
|
binding.retryButton.setOnClickListener {
|
||||||
retryCallback()
|
retryCallback()
|
||||||
}
|
}
|
||||||
if(fullScreen) {
|
|
||||||
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
} else {
|
|
||||||
binding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
/* Copyright 2021 Tusky Contributors
|
||||||
*
|
*
|
||||||
* This file is a part of Tusky.
|
* This file is a part of Tusky.
|
||||||
*
|
*
|
||||||
|
@ -43,6 +43,7 @@ import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
@ -195,14 +196,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
} else {
|
} else {
|
||||||
holder.showNotificationContent(true);
|
holder.showNotificationContent(true);
|
||||||
|
|
||||||
holder.setDisplayName(statusViewData.getUserFullName(), statusViewData.getAccountEmojis());
|
Status status = statusViewData.getActionable();
|
||||||
holder.setUsername(statusViewData.getNickname());
|
holder.setDisplayName(status.getAccount().getDisplayName(), status.getAccount().getEmojis());
|
||||||
holder.setCreatedAt(statusViewData.getCreatedAt());
|
holder.setUsername(status.getAccount().getUsername());
|
||||||
|
holder.setCreatedAt(status.getCreatedAt());
|
||||||
|
|
||||||
if (concreteNotificaton.getType() == Notification.Type.STATUS) {
|
if (concreteNotificaton.getType() == Notification.Type.STATUS) {
|
||||||
holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot());
|
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
|
||||||
} else {
|
} else {
|
||||||
holder.setAvatars(statusViewData.getAvatar(),
|
holder.setAvatars(status.getAccount().getAvatar(),
|
||||||
concreteNotificaton.getAccount().getAvatar());
|
concreteNotificaton.getAccount().getAvatar());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,7 +217,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
if (payloadForHolder instanceof List)
|
if (payloadForHolder instanceof List)
|
||||||
for (Object item : (List) payloadForHolder) {
|
for (Object item : (List) payloadForHolder) {
|
||||||
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) {
|
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) {
|
||||||
holder.setCreatedAt(statusViewData.getCreatedAt());
|
holder.setCreatedAt(statusViewData.getStatus().getActionableStatus().getCreatedAt());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,6 +258,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
statusDisplayOptions.useBlurhash(),
|
statusDisplayOptions.useBlurhash(),
|
||||||
CardViewMode.NONE,
|
CardViewMode.NONE,
|
||||||
statusDisplayOptions.confirmReblogs(),
|
statusDisplayOptions.confirmReblogs(),
|
||||||
|
statusDisplayOptions.confirmFavourites(),
|
||||||
statusDisplayOptions.hideStats(),
|
statusDisplayOptions.hideStats(),
|
||||||
statusDisplayOptions.animateEmojis()
|
statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
|
@ -531,7 +534,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
message.setText(emojifiedText);
|
message.setText(emojifiedText);
|
||||||
|
|
||||||
if (statusViewData != null) {
|
if (statusViewData != null) {
|
||||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText());
|
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus().getSpoilerText());
|
||||||
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||||
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||||
if (statusViewData.isExpanded()) {
|
if (statusViewData.isExpanded()) {
|
||||||
|
@ -607,7 +610,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
private void setupContentAndSpoiler(final LinkListener listener) {
|
private void setupContentAndSpoiler(final LinkListener listener) {
|
||||||
|
|
||||||
boolean shouldShowContentIfSpoiler = statusViewData.isExpanded();
|
boolean shouldShowContentIfSpoiler = statusViewData.isExpanded();
|
||||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText());
|
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus(). getSpoilerText());
|
||||||
if (!shouldShowContentIfSpoiler && hasSpoiler) {
|
if (!shouldShowContentIfSpoiler && hasSpoiler) {
|
||||||
statusContent.setVisibility(View.GONE);
|
statusContent.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
|
@ -615,7 +618,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
Spanned content = statusViewData.getContent();
|
Spanned content = statusViewData.getContent();
|
||||||
List<Emoji> emojis = statusViewData.getStatusEmojis();
|
List<Emoji> emojis = statusViewData.getActionable().getEmojis();
|
||||||
|
|
||||||
if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) {
|
if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) {
|
||||||
contentCollapseButton.setOnClickListener(view -> {
|
contentCollapseButton.setOnClickListener(view -> {
|
||||||
|
@ -641,14 +644,19 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||||
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
|
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener);
|
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), listener);
|
||||||
|
|
||||||
CharSequence emojifiedContentWarning = CustomEmojiHelper.emojify(
|
CharSequence emojifiedContentWarning;
|
||||||
|
if (statusViewData.getSpoilerText() != null) {
|
||||||
|
emojifiedContentWarning = CustomEmojiHelper.emojify(
|
||||||
statusViewData.getSpoilerText(),
|
statusViewData.getSpoilerText(),
|
||||||
statusViewData.getStatusEmojis(),
|
statusViewData.getActionable().getEmojis(),
|
||||||
contentWarningDescriptionTextView,
|
contentWarningDescriptionTextView,
|
||||||
statusDisplayOptions.animateEmojis()
|
statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
emojifiedContentWarning = "";
|
||||||
|
}
|
||||||
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
|
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,49 +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.adapter;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|
||||||
|
|
||||||
public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
private Button loadMoreButton;
|
|
||||||
private ProgressBar progressBar;
|
|
||||||
|
|
||||||
PlaceholderViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
loadMoreButton = itemView.findViewById(R.id.button_load_more);
|
|
||||||
progressBar = itemView.findViewById(R.id.progressBar);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setup(final StatusActionListener listener, boolean progress) {
|
|
||||||
loadMoreButton.setVisibility(progress ? View.GONE : View.VISIBLE);
|
|
||||||
progressBar.setVisibility(progress ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
loadMoreButton.setEnabled(true);
|
|
||||||
loadMoreButton.setOnClickListener(v -> {
|
|
||||||
loadMoreButton.setEnabled(false);
|
|
||||||
listener.onLoadMore(getBindingAdapterPosition());
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* Copyright 2021 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.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for different timelines.
|
||||||
|
* Either displays "load more" button or a progress indicator.
|
||||||
|
**/
|
||||||
|
class PlaceholderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val loadMoreButton: Button = itemView.findViewById(R.id.button_load_more)
|
||||||
|
private val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar)
|
||||||
|
|
||||||
|
fun setup(listener: StatusActionListener, progress: Boolean) {
|
||||||
|
loadMoreButton.visibility = if (progress) View.GONE else View.VISIBLE
|
||||||
|
progressBar.visibility = if (progress) View.VISIBLE else View.GONE
|
||||||
|
loadMoreButton.isEnabled = true
|
||||||
|
loadMoreButton.setOnClickListener { v: View? ->
|
||||||
|
loadMoreButton.isEnabled = false
|
||||||
|
listener.onLoadMore(bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,8 +18,10 @@ package com.keylesspalace.tusky.adapter
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemPollBinding
|
import com.keylesspalace.tusky.databinding.ItemPollBinding
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
@ -46,7 +48,8 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
emojis: List<Emoji>,
|
emojis: List<Emoji>,
|
||||||
mode: Int,
|
mode: Int,
|
||||||
resultClickListener: View.OnClickListener?,
|
resultClickListener: View.OnClickListener?,
|
||||||
animateEmojis: Boolean) {
|
animateEmojis: Boolean
|
||||||
|
) {
|
||||||
this.pollOptions = options
|
this.pollOptions = options
|
||||||
this.voteCount = voteCount
|
this.voteCount = voteCount
|
||||||
this.votersCount = votersCount
|
this.votersCount = votersCount
|
||||||
|
@ -62,7 +65,6 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
.map { pollOptions.indexOf(it) }
|
.map { pollOptions.indexOf(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
|
||||||
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
|
@ -85,13 +87,19 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
when (mode) {
|
when (mode) {
|
||||||
RESULT -> {
|
RESULT -> {
|
||||||
val percent = calculatePercent(option.votesCount, votersCount, voteCount)
|
val percent = calculatePercent(option.votesCount, votersCount, voteCount)
|
||||||
val emojifiedPollOptionText = buildDescription(option.title, percent, resultTextView.context)
|
val emojifiedPollOptionText = buildDescription(option.title, percent, option.voted, resultTextView.context)
|
||||||
.emojify(emojis, resultTextView, animateEmojis)
|
.emojify(emojis, resultTextView, animateEmojis)
|
||||||
resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
|
resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
|
||||||
|
|
||||||
val level = percent * 100
|
val level = percent * 100
|
||||||
|
val optionColor = if (option.voted) {
|
||||||
|
R.color.colorBackgroundHighlight
|
||||||
|
} else {
|
||||||
|
R.color.colorBackgroundAccent
|
||||||
|
}
|
||||||
|
|
||||||
resultTextView.background.level = level
|
resultTextView.background.level = level
|
||||||
|
resultTextView.background.setTint(ContextCompat.getColor(resultTextView.context, optionColor))
|
||||||
resultTextView.setOnClickListener(resultClickListener)
|
resultTextView.setOnClickListener(resultClickListener)
|
||||||
}
|
}
|
||||||
SINGLE -> {
|
SINGLE -> {
|
||||||
|
@ -114,7 +122,6 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -60,7 +60,6 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
|
||||||
|
|
||||||
textView.setOnClickListener(clickListener)
|
textView.setOnClickListener(clickListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
class PreviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||||
|
|
|
@ -1,122 +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.adapter;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.db.TootEntity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class SavedTootAdapter extends RecyclerView.Adapter {
|
|
||||||
private List<TootEntity> list;
|
|
||||||
private SavedTootAction handler;
|
|
||||||
|
|
||||||
public SavedTootAdapter(Context context) {
|
|
||||||
super();
|
|
||||||
list = new ArrayList<>();
|
|
||||||
handler = (SavedTootAction) context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_saved_toot, parent, false);
|
|
||||||
return new TootViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
|
||||||
TootViewHolder holder = (TootViewHolder) viewHolder;
|
|
||||||
holder.bind(getItem(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return list.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setItems(List<TootEntity> newToot) {
|
|
||||||
list = new ArrayList<>();
|
|
||||||
list.addAll(newToot);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addItems(List<TootEntity> newToot) {
|
|
||||||
int end = list.size();
|
|
||||||
list.addAll(newToot);
|
|
||||||
notifyItemRangeInserted(end, newToot.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public TootEntity removeItem(int position) {
|
|
||||||
if (position < 0 || position >= list.size()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TootEntity toot = list.remove(position);
|
|
||||||
notifyItemRemoved(position);
|
|
||||||
return toot;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TootEntity getItem(int position) {
|
|
||||||
if (position >= 0 && position < list.size()) {
|
|
||||||
return list.get(position);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handler saved toot
|
|
||||||
public interface SavedTootAction {
|
|
||||||
void delete(int position, TootEntity item);
|
|
||||||
|
|
||||||
void click(int position, TootEntity item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TootViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
View view;
|
|
||||||
TextView content;
|
|
||||||
ImageButton suppr;
|
|
||||||
|
|
||||||
TootViewHolder(View view) {
|
|
||||||
super(view);
|
|
||||||
this.view = view;
|
|
||||||
this.content = view.findViewById(R.id.content);
|
|
||||||
this.suppr = view.findViewById(R.id.suppr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bind(final TootEntity item) {
|
|
||||||
suppr.setEnabled(true);
|
|
||||||
|
|
||||||
if (item != null) {
|
|
||||||
content.setText(item.getText());
|
|
||||||
|
|
||||||
suppr.setOnClickListener(v -> {
|
|
||||||
v.setEnabled(false);
|
|
||||||
handler.delete(getBindingAdapterPosition(), item);
|
|
||||||
});
|
|
||||||
view.setOnClickListener(v -> handler.click(getBindingAdapterPosition(), item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -201,7 +201,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
protected void setSpoilerAndContent(boolean expanded,
|
protected void setSpoilerAndContent(boolean expanded,
|
||||||
@NonNull Spanned content,
|
@NonNull Spanned content,
|
||||||
@Nullable String spoilerText,
|
@Nullable String spoilerText,
|
||||||
@Nullable Status.Mention[] mentions,
|
@Nullable List<Status.Mention> mentions,
|
||||||
@NonNull List<Emoji> emojis,
|
@NonNull List<Emoji> emojis,
|
||||||
@Nullable PollViewData poll,
|
@Nullable PollViewData poll,
|
||||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||||
|
@ -243,7 +243,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private void setTextVisible(boolean sensitive,
|
private void setTextVisible(boolean sensitive,
|
||||||
boolean expanded,
|
boolean expanded,
|
||||||
Spanned content,
|
Spanned content,
|
||||||
Status.Mention[] mentions,
|
List<Status.Mention> mentions,
|
||||||
List<Emoji> emojis,
|
List<Emoji> emojis,
|
||||||
@Nullable PollViewData poll,
|
@Nullable PollViewData poll,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
|
@ -651,11 +651,19 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
favouriteButton.setEventListener((button, buttonState) -> {
|
favouriteButton.setEventListener((button, buttonState) -> {
|
||||||
|
// return true to play animaion
|
||||||
int position = getBindingAdapterPosition();
|
int position = getBindingAdapterPosition();
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
if (statusDisplayOptions.confirmFavourites()) {
|
||||||
|
showConfirmFavouriteDialog(listener, statusContent, buttonState, position);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
listener.onFavourite(!buttonState, position);
|
listener.onFavourite(!buttonState, position);
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bookmarkButton.setEventListener((button, buttonState) -> {
|
bookmarkButton.setEventListener((button, buttonState) -> {
|
||||||
|
@ -703,26 +711,45 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showConfirmFavouriteDialog(StatusActionListener listener,
|
||||||
|
String statusContent,
|
||||||
|
boolean buttonState,
|
||||||
|
int position) {
|
||||||
|
int okButtonTextId = buttonState ? R.string.action_unfavourite : R.string.action_favourite;
|
||||||
|
new AlertDialog.Builder(favouriteButton.getContext())
|
||||||
|
.setMessage(statusContent)
|
||||||
|
.setPositiveButton(okButtonTextId, (__, ___) -> {
|
||||||
|
listener.onFavourite(!buttonState, position);
|
||||||
|
if (!buttonState) {
|
||||||
|
// Play animation only when it's favourite, not unfavourite
|
||||||
|
favouriteButton.playAnimation();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||||
StatusDisplayOptions statusDisplayOptions) {
|
StatusDisplayOptions statusDisplayOptions) {
|
||||||
this.setupWithStatus(status, listener, statusDisplayOptions, null);
|
this.setupWithStatus(status, listener, statusDisplayOptions, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setupWithStatus(StatusViewData.Concrete status,
|
public void setupWithStatus(StatusViewData.Concrete status,
|
||||||
final StatusActionListener listener,
|
final StatusActionListener listener,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
if (payloads == null) {
|
if (payloads == null) {
|
||||||
setDisplayName(status.getUserFullName(), status.getAccountEmojis(), statusDisplayOptions);
|
Status actionable = status.getActionable();
|
||||||
setUsername(status.getNickname());
|
setDisplayName(actionable.getAccount().getDisplayName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
|
||||||
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
setUsername(status.getUsername());
|
||||||
setIsReply(status.getInReplyToId() != null);
|
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
|
||||||
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions);
|
setIsReply(actionable.getInReplyToId() != null);
|
||||||
setReblogged(status.isReblogged());
|
setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(),
|
||||||
setFavourited(status.isFavourited());
|
actionable.getAccount().getBot(), statusDisplayOptions);
|
||||||
setBookmarked(status.isBookmarked());
|
setReblogged(actionable.getReblogged());
|
||||||
List<Attachment> attachments = status.getAttachments();
|
setFavourited(actionable.getFavourited());
|
||||||
boolean sensitive = status.isSensitive();
|
setBookmarked(actionable.getBookmarked());
|
||||||
|
List<Attachment> attachments = actionable.getAttachments();
|
||||||
|
boolean sensitive = actionable.getSensitive();
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||||
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
|
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
|
||||||
|
|
||||||
|
@ -744,14 +771,17 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cardView != null) {
|
if (cardView != null) {
|
||||||
setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions);
|
setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupButtons(listener, status.getSenderId(), status.getContent().toString(),
|
setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(),
|
||||||
statusDisplayOptions);
|
statusDisplayOptions);
|
||||||
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
|
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
||||||
|
|
||||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener);
|
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(),
|
||||||
|
actionable.getMentions(), actionable.getEmojis(),
|
||||||
|
PollViewDataKt.toViewData(actionable.getPoll()), statusDisplayOptions,
|
||||||
|
listener);
|
||||||
|
|
||||||
setDescriptionForStatus(status, statusDisplayOptions);
|
setDescriptionForStatus(status, statusDisplayOptions);
|
||||||
|
|
||||||
|
@ -765,7 +795,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
if (payloads instanceof List)
|
if (payloads instanceof List)
|
||||||
for (Object item : (List<?>) payloads) {
|
for (Object item : (List<?>) payloads) {
|
||||||
if (Key.KEY_CREATED.equals(item)) {
|
if (Key.KEY_CREATED.equals(item)) {
|
||||||
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
setCreatedAt(status.getActionable().getCreatedAt(), statusDisplayOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -784,21 +814,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status,
|
private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status,
|
||||||
StatusDisplayOptions statusDisplayOptions) {
|
StatusDisplayOptions statusDisplayOptions) {
|
||||||
Context context = itemView.getContext();
|
Context context = itemView.getContext();
|
||||||
|
Status actionable = status.getActionable();
|
||||||
|
|
||||||
String description = context.getString(R.string.description_status,
|
String description = context.getString(R.string.description_status,
|
||||||
status.getUserFullName(),
|
actionable.getAccount().getDisplayName(),
|
||||||
getContentWarningDescription(context, status),
|
getContentWarningDescription(context, status),
|
||||||
(TextUtils.isEmpty(status.getSpoilerText()) || !status.isSensitive() || status.isExpanded() ? status.getContent() : ""),
|
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
|
||||||
getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions),
|
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
||||||
getReblogDescription(context, status),
|
getReblogDescription(context, status),
|
||||||
status.getNickname(),
|
status.getUsername(),
|
||||||
status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
actionable.getReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
||||||
status.isFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
actionable.getFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
||||||
status.isBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
actionable.getBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
||||||
getMediaDescription(context, status),
|
getMediaDescription(context, status),
|
||||||
getVisibilityDescription(context, status.getVisibility()),
|
getVisibilityDescription(context, actionable.getVisibility()),
|
||||||
getFavsText(context, status.getFavouritesCount()),
|
getFavsText(context, actionable.getFavouritesCount()),
|
||||||
getReblogsText(context, status.getReblogsCount()),
|
getReblogsText(context, actionable.getReblogsCount()),
|
||||||
getPollDescription(status, context, statusDisplayOptions)
|
getPollDescription(status, context, statusDisplayOptions)
|
||||||
);
|
);
|
||||||
itemView.setContentDescription(description);
|
itemView.setContentDescription(description);
|
||||||
|
@ -806,10 +837,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private static CharSequence getReblogDescription(Context context,
|
private static CharSequence getReblogDescription(Context context,
|
||||||
@NonNull StatusViewData.Concrete status) {
|
@NonNull StatusViewData.Concrete status) {
|
||||||
String rebloggedUsername = status.getRebloggedByUsername();
|
Status reblog = status.getRebloggingStatus();
|
||||||
if (rebloggedUsername != null) {
|
if (reblog != null) {
|
||||||
return context
|
return context
|
||||||
.getString(R.string.status_boosted_format, rebloggedUsername);
|
.getString(R.string.status_boosted_format, reblog.getAccount().getUsername());
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -817,11 +848,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private static CharSequence getMediaDescription(Context context,
|
private static CharSequence getMediaDescription(Context context,
|
||||||
@NonNull StatusViewData.Concrete status) {
|
@NonNull StatusViewData.Concrete status) {
|
||||||
if (status.getAttachments().isEmpty()) {
|
if (status.getActionable().getAttachments().isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
StringBuilder mediaDescriptions = CollectionsKt.fold(
|
StringBuilder mediaDescriptions = CollectionsKt.fold(
|
||||||
status.getAttachments(),
|
status.getActionable().getAttachments(),
|
||||||
new StringBuilder(),
|
new StringBuilder(),
|
||||||
(builder, a) -> {
|
(builder, a) -> {
|
||||||
if (a.getDescription() == null) {
|
if (a.getDescription() == null) {
|
||||||
|
@ -874,7 +905,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status,
|
private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status,
|
||||||
Context context,
|
Context context,
|
||||||
StatusDisplayOptions statusDisplayOptions) {
|
StatusDisplayOptions statusDisplayOptions) {
|
||||||
PollViewData poll = status.getPoll();
|
PollViewData poll = PollViewDataKt.toViewData(status.getActionable().getPoll());
|
||||||
if (poll == null) {
|
if (poll == null) {
|
||||||
return "";
|
return "";
|
||||||
} else {
|
} else {
|
||||||
|
@ -883,7 +914,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
if (i < options.size()) {
|
if (i < options.size()) {
|
||||||
int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), poll.getVotesCount());
|
int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), poll.getVotesCount());
|
||||||
args[i] = buildDescription(options.get(i).getTitle(), percent, context);
|
args[i] = buildDescription(options.get(i).getTitle(), percent, options.get(i).getVoted(), context);
|
||||||
} else {
|
} else {
|
||||||
args[i] = "";
|
args[i] = "";
|
||||||
}
|
}
|
||||||
|
@ -1003,13 +1034,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
|
return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) {
|
protected void setupCard(
|
||||||
|
StatusViewData.Concrete status,
|
||||||
|
CardViewMode cardViewMode,
|
||||||
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
|
final StatusActionListener listener
|
||||||
|
) {
|
||||||
|
final Card card = status.getActionable().getCard();
|
||||||
if (cardViewMode != CardViewMode.NONE &&
|
if (cardViewMode != CardViewMode.NONE &&
|
||||||
status.getAttachments().size() == 0 &&
|
status.getActionable().getAttachments().size() == 0 &&
|
||||||
status.getCard() != null &&
|
card != null &&
|
||||||
!TextUtils.isEmpty(status.getCard().getUrl()) &&
|
!TextUtils.isEmpty(card.getUrl()) &&
|
||||||
(!status.isCollapsible() || !status.isCollapsed())) {
|
(!status.isCollapsible() || !status.isCollapsed())) {
|
||||||
final Card card = status.getCard();
|
|
||||||
cardView.setVisibility(View.VISIBLE);
|
cardView.setVisibility(View.VISIBLE);
|
||||||
cardTitle.setText(card.getTitle());
|
cardTitle.setText(card.getTitle());
|
||||||
if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) {
|
if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) {
|
||||||
|
@ -1028,7 +1064,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
|
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
|
||||||
// so let's blur the preview in that case
|
// so let's blur the preview in that case
|
||||||
// If media previews are disabled, show placeholder for cards as well
|
// If media previews are disabled, show placeholder for cards as well
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && !status.isSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
if (statusDisplayOptions.mediaPreviewEnabled() && !status.getActionable().getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||||
|
|
||||||
int topLeftRadius = 0;
|
int topLeftRadius = 0;
|
||||||
int topRightRadius = 0;
|
int topRightRadius = 0;
|
||||||
|
@ -1094,7 +1130,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
cardImage.setImageResource(R.drawable.card_image_placeholder);
|
cardImage.setImageResource(R.drawable.card_image_placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
View.OnClickListener visitLink = v -> LinkHelper.openLink(card.getUrl(), v.getContext());
|
View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl());
|
||||||
View.OnClickListener openImage = v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbed_url()));
|
View.OnClickListener openImage = v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbed_url()));
|
||||||
|
|
||||||
cardInfo.setOnClickListener(visitLink);
|
cardInfo.setOnClickListener(visitLink);
|
||||||
|
|
|
@ -101,21 +101,22 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setupWithStatus(final StatusViewData.Concrete status,
|
public void setupWithStatus(final StatusViewData.Concrete status,
|
||||||
final StatusActionListener listener,
|
final StatusActionListener listener,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
||||||
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status
|
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
||||||
if (payloads == null) {
|
if (payloads == null) {
|
||||||
|
|
||||||
if (!statusDisplayOptions.hideStats()) {
|
if (!statusDisplayOptions.hideStats()) {
|
||||||
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
|
setReblogAndFavCount(status.getActionable().getReblogsCount(),
|
||||||
|
status.getActionable().getFavouritesCount(), listener);
|
||||||
} else {
|
} else {
|
||||||
hideQuantitativeStats();
|
hideQuantitativeStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
setApplication(status.getApplication());
|
setApplication(status.getActionable().getApplication());
|
||||||
|
|
||||||
View.OnLongClickListener longClickListener = view -> {
|
View.OnLongClickListener longClickListener = view -> {
|
||||||
TextView textView = (TextView) view;
|
TextView textView = (TextView) view;
|
||||||
|
@ -130,7 +131,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
content.setOnLongClickListener(longClickListener);
|
content.setOnLongClickListener(longClickListener);
|
||||||
contentWarningDescription.setOnLongClickListener(longClickListener);
|
contentWarningDescription.setOnLongClickListener(longClickListener);
|
||||||
setStatusVisibility(status.getVisibility());
|
setStatusVisibility(status.getActionable().getVisibility());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||||
|
@ -33,6 +35,8 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||||
import com.keylesspalace.tusky.util.StringUtils;
|
import com.keylesspalace.tusky.util.StringUtils;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
import at.connyduck.sparkbutton.helpers.Utils;
|
||||||
|
|
||||||
public class StatusViewHolder extends StatusBaseViewHolder {
|
public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
|
@ -54,7 +58,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setupWithStatus(StatusViewData.Concrete status,
|
public void setupWithStatus(StatusViewData.Concrete status,
|
||||||
final StatusActionListener listener,
|
final StatusActionListener listener,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
|
@ -62,11 +66,13 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
setupCollapsedState(status, listener);
|
setupCollapsedState(status, listener);
|
||||||
|
|
||||||
String rebloggedByDisplayName = status.getRebloggedByUsername();
|
Status reblogging = status.getRebloggingStatus();
|
||||||
if (rebloggedByDisplayName == null) {
|
if (reblogging == null) {
|
||||||
hideStatusInfo();
|
hideStatusInfo();
|
||||||
} else {
|
} else {
|
||||||
setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions);
|
String rebloggedByDisplayName = reblogging.getAccount().getDisplayName();
|
||||||
|
setRebloggedByDisplayName(rebloggedByDisplayName,
|
||||||
|
reblogging.getAccount().getEmojis(), statusDisplayOptions);
|
||||||
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
|
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,13 +82,13 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRebloggedByDisplayName(final CharSequence name,
|
private void setRebloggedByDisplayName(final CharSequence name,
|
||||||
final StatusViewData.Concrete status,
|
final List<Emoji> accountEmoji,
|
||||||
final StatusDisplayOptions statusDisplayOptions) {
|
final StatusDisplayOptions statusDisplayOptions) {
|
||||||
Context context = statusInfo.getContext();
|
Context context = statusInfo.getContext();
|
||||||
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
||||||
CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName);
|
CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName);
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||||
boostedText, status.getRebloggedByAccountEmojis(), statusInfo, statusDisplayOptions.animateEmojis()
|
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
statusInfo.setText(emojifiedText);
|
statusInfo.setText(emojifiedText);
|
||||||
statusInfo.setVisibility(View.VISIBLE);
|
statusInfo.setVisibility(View.VISIBLE);
|
||||||
|
|
|
@ -43,7 +43,8 @@ interface ItemInteractionListener {
|
||||||
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
|
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabAdapter(private var data: List<TabData>,
|
class TabAdapter(
|
||||||
|
private var data: List<TabData>,
|
||||||
private val small: Boolean,
|
private val small: Boolean,
|
||||||
private val listener: ItemInteractionListener,
|
private val listener: ItemInteractionListener,
|
||||||
private var removeButtonEnabled: Boolean = false
|
private var removeButtonEnabled: Boolean = false
|
||||||
|
@ -77,7 +78,6 @@ class TabAdapter(private var data: List<TabData>,
|
||||||
binding.textView.setOnClickListener {
|
binding.textView.setOnClickListener {
|
||||||
listener.onTabAdded(tab)
|
listener.onTabAdded(tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
val binding = holder.binding as ItemTabPreferenceBinding
|
val binding = holder.binding as ItemTabPreferenceBinding
|
||||||
|
|
||||||
|
@ -143,7 +143,6 @@ class TabAdapter(private var data: List<TabData>,
|
||||||
binding.actionChip.setOnClickListener {
|
binding.actionChip.setOnClickListener {
|
||||||
listener.onActionChipClicked(tab, holder.bindingAdapterPosition)
|
listener.onActionChipClicked(tab, holder.bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
binding.chipGroup.hide()
|
binding.chipGroup.hide()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,164 +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.adapter;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ThreadAdapter extends RecyclerView.Adapter {
|
|
||||||
private static final int VIEW_TYPE_STATUS = 0;
|
|
||||||
private static final int VIEW_TYPE_STATUS_DETAILED = 1;
|
|
||||||
|
|
||||||
private List<StatusViewData.Concrete> statuses;
|
|
||||||
private StatusDisplayOptions statusDisplayOptions;
|
|
||||||
private StatusActionListener statusActionListener;
|
|
||||||
private int detailedStatusPosition;
|
|
||||||
|
|
||||||
public ThreadAdapter(StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) {
|
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
|
||||||
this.statusActionListener = listener;
|
|
||||||
this.statuses = new ArrayList<>();
|
|
||||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
switch (viewType) {
|
|
||||||
default:
|
|
||||||
case VIEW_TYPE_STATUS: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_status, parent, false);
|
|
||||||
return new StatusViewHolder(view);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_STATUS_DETAILED: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_status_detailed, parent, false);
|
|
||||||
return new StatusDetailedViewHolder(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
|
||||||
StatusViewData.Concrete status = statuses.get(position);
|
|
||||||
if (position == detailedStatusPosition) {
|
|
||||||
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
|
|
||||||
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
|
|
||||||
} else {
|
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
|
||||||
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
if (position == detailedStatusPosition) {
|
|
||||||
return VIEW_TYPE_STATUS_DETAILED;
|
|
||||||
} else {
|
|
||||||
return VIEW_TYPE_STATUS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return statuses.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatuses(List<StatusViewData.Concrete> statuses) {
|
|
||||||
this.statuses.clear();
|
|
||||||
this.statuses.addAll(statuses);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addItem(int position, StatusViewData.Concrete statusViewData) {
|
|
||||||
statuses.add(position, statusViewData);
|
|
||||||
notifyItemInserted(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearItems() {
|
|
||||||
int oldSize = statuses.size();
|
|
||||||
statuses.clear();
|
|
||||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
|
||||||
notifyItemRangeRemoved(0, oldSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAll(int position, List<StatusViewData.Concrete> statuses) {
|
|
||||||
this.statuses.addAll(position, statuses);
|
|
||||||
notifyItemRangeInserted(position, statuses.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAll(List<StatusViewData.Concrete> statuses) {
|
|
||||||
int end = statuses.size();
|
|
||||||
this.statuses.addAll(statuses);
|
|
||||||
notifyItemRangeInserted(end, statuses.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeItem(int position) {
|
|
||||||
statuses.remove(position);
|
|
||||||
notifyItemRemoved(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
statuses.clear();
|
|
||||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setItem(int position, StatusViewData.Concrete status, boolean notifyAdapter) {
|
|
||||||
statuses.set(position, status);
|
|
||||||
if (notifyAdapter) {
|
|
||||||
notifyItemChanged(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public StatusViewData.Concrete getItem(int position) {
|
|
||||||
if (position >= 0 && position < statuses.size()) {
|
|
||||||
return statuses.get(position);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDetailedStatusPosition(int position) {
|
|
||||||
if (position != detailedStatusPosition
|
|
||||||
&& detailedStatusPosition != RecyclerView.NO_POSITION) {
|
|
||||||
int prior = detailedStatusPosition;
|
|
||||||
detailedStatusPosition = position;
|
|
||||||
notifyItemChanged(prior);
|
|
||||||
} else {
|
|
||||||
detailedStatusPosition = position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDetailedStatusPosition() {
|
|
||||||
return detailedStatusPosition;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
/* Copyright 2021 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.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
|
||||||
|
class ThreadAdapter(
|
||||||
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
|
private val statusActionListener: StatusActionListener
|
||||||
|
) : RecyclerView.Adapter<StatusBaseViewHolder>() {
|
||||||
|
private val statuses = mutableListOf<StatusViewData.Concrete>()
|
||||||
|
var detailedStatusPosition: Int = RecyclerView.NO_POSITION
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder {
|
||||||
|
return when (viewType) {
|
||||||
|
VIEW_TYPE_STATUS -> {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_status, parent, false)
|
||||||
|
StatusViewHolder(view)
|
||||||
|
}
|
||||||
|
VIEW_TYPE_STATUS_DETAILED -> {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_status_detailed, parent, false)
|
||||||
|
StatusDetailedViewHolder(view)
|
||||||
|
}
|
||||||
|
else -> error("Unknown item type: $viewType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) {
|
||||||
|
val status = statuses[position]
|
||||||
|
viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return if (position == detailedStatusPosition) {
|
||||||
|
VIEW_TYPE_STATUS_DETAILED
|
||||||
|
} else {
|
||||||
|
VIEW_TYPE_STATUS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = statuses.size
|
||||||
|
|
||||||
|
fun setStatuses(statuses: List<StatusViewData.Concrete>?) {
|
||||||
|
this.statuses.clear()
|
||||||
|
this.statuses.addAll(statuses!!)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItem(position: Int, statusViewData: StatusViewData.Concrete) {
|
||||||
|
statuses.add(position, statusViewData)
|
||||||
|
notifyItemInserted(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearItems() {
|
||||||
|
val oldSize = statuses.size
|
||||||
|
statuses.clear()
|
||||||
|
detailedStatusPosition = RecyclerView.NO_POSITION
|
||||||
|
notifyItemRangeRemoved(0, oldSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAll(position: Int, statuses: List<StatusViewData.Concrete>) {
|
||||||
|
this.statuses.addAll(position, statuses)
|
||||||
|
notifyItemRangeInserted(position, statuses.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAll(statuses: List<StatusViewData.Concrete>) {
|
||||||
|
val end = statuses.size
|
||||||
|
this.statuses.addAll(statuses)
|
||||||
|
notifyItemRangeInserted(end, statuses.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeItem(position: Int) {
|
||||||
|
statuses.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
statuses.clear()
|
||||||
|
detailedStatusPosition = RecyclerView.NO_POSITION
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItem(position: Int, status: StatusViewData.Concrete, notifyAdapter: Boolean) {
|
||||||
|
statuses[position] = status
|
||||||
|
if (notifyAdapter) {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItem(position: Int): StatusViewData.Concrete? = statuses.getOrNull(position)
|
||||||
|
|
||||||
|
fun setDetailedStatusPosition(position: Int) {
|
||||||
|
if (position != detailedStatusPosition &&
|
||||||
|
detailedStatusPosition != RecyclerView.NO_POSITION
|
||||||
|
) {
|
||||||
|
val prior = detailedStatusPosition
|
||||||
|
detailedStatusPosition = position
|
||||||
|
notifyItemChanged(prior)
|
||||||
|
} else {
|
||||||
|
detailedStatusPosition = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VIEW_TYPE_STATUS = 0
|
||||||
|
private const val VIEW_TYPE_STATUS_DETAILED = 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,135 +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.adapter;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class TimelineAdapter extends RecyclerView.Adapter {
|
|
||||||
|
|
||||||
public interface AdapterDataSource<T> {
|
|
||||||
int getItemCount();
|
|
||||||
|
|
||||||
T getItemAt(int pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int VIEW_TYPE_STATUS = 0;
|
|
||||||
private static final int VIEW_TYPE_PLACEHOLDER = 2;
|
|
||||||
|
|
||||||
private final AdapterDataSource<StatusViewData> dataSource;
|
|
||||||
private StatusDisplayOptions statusDisplayOptions;
|
|
||||||
private final StatusActionListener statusListener;
|
|
||||||
|
|
||||||
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
|
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
|
||||||
StatusActionListener statusListener) {
|
|
||||||
this.dataSource = dataSource;
|
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
|
||||||
this.statusListener = statusListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getMediaPreviewEnabled() {
|
|
||||||
return statusDisplayOptions.mediaPreviewEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
|
|
||||||
this.statusDisplayOptions = statusDisplayOptions.copy(
|
|
||||||
statusDisplayOptions.animateAvatars(),
|
|
||||||
mediaPreviewEnabled,
|
|
||||||
statusDisplayOptions.useAbsoluteTime(),
|
|
||||||
statusDisplayOptions.showBotOverlay(),
|
|
||||||
statusDisplayOptions.useBlurhash(),
|
|
||||||
statusDisplayOptions.cardViewMode(),
|
|
||||||
statusDisplayOptions.confirmReblogs(),
|
|
||||||
statusDisplayOptions.hideStats(),
|
|
||||||
statusDisplayOptions.animateEmojis()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
|
||||||
switch (viewType) {
|
|
||||||
default:
|
|
||||||
case VIEW_TYPE_STATUS: {
|
|
||||||
View view = LayoutInflater.from(viewGroup.getContext())
|
|
||||||
.inflate(R.layout.item_status, viewGroup, false);
|
|
||||||
return new StatusViewHolder(view);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_PLACEHOLDER: {
|
|
||||||
View view = LayoutInflater.from(viewGroup.getContext())
|
|
||||||
.inflate(R.layout.item_status_placeholder, viewGroup, false);
|
|
||||||
return new PlaceholderViewHolder(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
|
||||||
bindViewHolder(viewHolder, position, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) {
|
|
||||||
bindViewHolder(viewHolder, position, payloads);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) {
|
|
||||||
StatusViewData status = dataSource.getItemAt(position);
|
|
||||||
if (status instanceof StatusViewData.Placeholder) {
|
|
||||||
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
|
|
||||||
holder.setup(statusListener, ((StatusViewData.Placeholder) status).isLoading());
|
|
||||||
} else if (status instanceof StatusViewData.Concrete) {
|
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
|
||||||
holder.setupWithStatus((StatusViewData.Concrete) status,
|
|
||||||
statusListener,
|
|
||||||
statusDisplayOptions,
|
|
||||||
payloads != null && !payloads.isEmpty() ? payloads.get(0) : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return dataSource.getItemCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
if (dataSource.getItemAt(position) instanceof StatusViewData.Placeholder) {
|
|
||||||
return VIEW_TYPE_PLACEHOLDER;
|
|
||||||
} else {
|
|
||||||
return VIEW_TYPE_STATUS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
return dataSource.getItemAt(position).getViewDataId();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,14 +3,14 @@ package com.keylesspalace.tusky.appstore
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import io.reactivex.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CacheUpdater @Inject constructor(
|
class CacheUpdater @Inject constructor(
|
||||||
eventHub: EventHub,
|
eventHub: EventHub,
|
||||||
accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val appDatabase: AppDatabase,
|
private val appDatabase: AppDatabase,
|
||||||
gson: Gson
|
gson: Gson
|
||||||
) {
|
) {
|
||||||
|
@ -19,6 +19,7 @@ class CacheUpdater @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val timelineDao = appDatabase.timelineDao()
|
val timelineDao = appDatabase.timelineDao()
|
||||||
|
|
||||||
disposable = eventHub.events.subscribe { event ->
|
disposable = eventHub.events.subscribe { event ->
|
||||||
val accountId = accountManager.activeAccount?.id ?: return@subscribe
|
val accountId = accountManager.activeAccount?.id ?: return@subscribe
|
||||||
when (event) {
|
when (event) {
|
||||||
|
@ -36,6 +37,8 @@ class CacheUpdater @Inject constructor(
|
||||||
val pollString = gson.toJson(event.poll)
|
val pollString = gson.toJson(event.poll)
|
||||||
timelineDao.setVoted(accountId, event.statusId, pollString)
|
timelineDao.setVoted(accountId, event.statusId, pollString)
|
||||||
}
|
}
|
||||||
|
is PinEvent ->
|
||||||
|
timelineDao.setPinned(accountId, event.statusId, event.pinned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,3 +21,4 @@ data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||||
data class DomainMuteEvent(val instance: String) : Dispatchable
|
data class DomainMuteEvent(val instance: String) : Dispatchable
|
||||||
data class AnnouncementReadEvent(val announcementId: String) : Dispatchable
|
data class AnnouncementReadEvent(val announcementId: String) : Dispatchable
|
||||||
|
data class PinEvent(val statusId: String, val pinned: Boolean) : Dispatchable
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
package com.keylesspalace.tusky.appstore
|
package com.keylesspalace.tusky.appstore
|
||||||
|
|
||||||
import io.reactivex.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
interface Event
|
interface Event
|
||||||
interface Dispatchable : Event
|
interface Dispatchable : Event
|
||||||
|
|
||||||
interface EventHub {
|
@Singleton
|
||||||
val events: Observable<Event>
|
class EventHub @Inject constructor() {
|
||||||
fun dispatch(event: Dispatchable)
|
|
||||||
}
|
|
||||||
|
|
||||||
object EventHubImpl : EventHub {
|
|
||||||
|
|
||||||
private val eventsSubject = PublishSubject.create<Event>()
|
private val eventsSubject = PublishSubject.create<Event>()
|
||||||
override val events: Observable<Event> = eventsSubject
|
val events: Observable<Event> = eventsSubject
|
||||||
|
|
||||||
override fun dispatch(event: Dispatchable) {
|
fun dispatch(event: Dispatchable) {
|
||||||
eventsSubject.onNext(event)
|
eventsSubject.onNext(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky.components.account
|
||||||
|
|
||||||
import android.animation.ArgbEvaluator
|
import android.animation.ArgbEvaluator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -34,20 +34,29 @@ import androidx.annotation.Px
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.viewpager2.widget.MarginPageTransformer
|
import androidx.viewpager2.widget.MarginPageTransformer
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
import com.google.android.material.shape.ShapeAppearanceModel
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.keylesspalace.tusky.adapter.AccountFieldAdapter
|
import com.keylesspalace.tusky.AccountListActivity
|
||||||
|
import com.keylesspalace.tusky.BottomSheetActivity
|
||||||
|
import com.keylesspalace.tusky.EditProfileActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
|
import com.keylesspalace.tusky.ViewTagActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
||||||
|
@ -57,11 +66,20 @@ import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.pager.AccountPagerAdapter
|
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.DefaultTextWatcher
|
||||||
|
import com.keylesspalace.tusky.util.Error
|
||||||
|
import com.keylesspalace.tusky.util.LinkHelper
|
||||||
|
import com.keylesspalace.tusky.util.Loading
|
||||||
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
|
@ -129,6 +147,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
hideFab = sharedPrefs.getBoolean("fabHide", false)
|
hideFab = sharedPrefs.getBoolean("fabHide", false)
|
||||||
|
|
||||||
|
handleWindowInsets()
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
setupTabs()
|
setupTabs()
|
||||||
setupAccountViews()
|
setupAccountViews()
|
||||||
|
@ -170,7 +189,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
binding.accountFieldList.layoutManager = LinearLayoutManager(this)
|
binding.accountFieldList.layoutManager = LinearLayoutManager(this)
|
||||||
binding.accountFieldList.adapter = accountFieldAdapter
|
binding.accountFieldList.adapter = accountFieldAdapter
|
||||||
|
|
||||||
|
|
||||||
val accountListClickListener = { v: View ->
|
val accountListClickListener = { v: View ->
|
||||||
val type = when (v.id) {
|
val type = when (v.id) {
|
||||||
R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS
|
R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS
|
||||||
|
@ -231,21 +249,25 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
|
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {}
|
override fun onTabSelected(tab: TabLayout.Tab?) {}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupToolbar() {
|
private fun handleWindowInsets() {
|
||||||
// set toolbar top margin according to system window insets
|
ViewCompat.setOnApplyWindowInsetsListener(binding.accountCoordinatorLayout) { _, insets ->
|
||||||
binding.accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets ->
|
val top = insets.getInsets(systemBars()).top
|
||||||
val top = insets.systemWindowInsetTop
|
val toolbarParams = binding.accountToolbar.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
|
||||||
val toolbarParams = binding.accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams
|
|
||||||
toolbarParams.topMargin = top
|
toolbarParams.topMargin = top
|
||||||
|
|
||||||
insets.consumeSystemWindowInsets()
|
val right = insets.getInsets(systemBars()).right
|
||||||
|
val bottom = insets.getInsets(systemBars()).bottom
|
||||||
|
val left = insets.getInsets(systemBars()).left
|
||||||
|
binding.accountCoordinatorLayout.updatePadding(right = right, bottom = bottom, left = left)
|
||||||
|
|
||||||
|
WindowInsetsCompat.CONSUMED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupToolbar() {
|
||||||
// Setup the toolbar.
|
// Setup the toolbar.
|
||||||
setSupportActionBar(binding.accountToolbar)
|
setSupportActionBar(binding.accountToolbar)
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
|
@ -314,12 +336,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0
|
binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeNotificationBarTransparent() {
|
private fun makeNotificationBarTransparent() {
|
||||||
val decorView = window.decorView
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
window.statusBarColor = statusBarColorTransparent
|
window.statusBarColor = statusBarColorTransparent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,6 +355,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
is Loading -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.relationshipData.observe(this) {
|
viewModel.relationshipData.observe(this) {
|
||||||
|
@ -348,12 +369,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
viewModel.accountFieldData.observe(this, {
|
viewModel.accountFieldData.observe(
|
||||||
|
this,
|
||||||
|
{
|
||||||
accountFieldAdapter.fields = it
|
accountFieldAdapter.fields = it
|
||||||
accountFieldAdapter.notifyDataSetChanged()
|
accountFieldAdapter.notifyDataSetChanged()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
viewModel.noteSaved.observe(this) {
|
viewModel.noteSaved.observe(this) {
|
||||||
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
||||||
}
|
}
|
||||||
|
@ -367,9 +390,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
adapter.refreshContent()
|
adapter.refreshContent()
|
||||||
}
|
}
|
||||||
viewModel.isRefreshing.observe(this, { isRefreshing ->
|
viewModel.isRefreshing.observe(
|
||||||
|
this,
|
||||||
|
{ isRefreshing ->
|
||||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||||
})
|
}
|
||||||
|
)
|
||||||
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,9 +448,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(binding.accountHeaderImageView)
|
.into(binding.accountHeaderImageView)
|
||||||
|
|
||||||
|
|
||||||
binding.accountAvatarImageView.setOnClickListener { avatarView ->
|
binding.accountAvatarImageView.setOnClickListener { avatarView ->
|
||||||
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
val intent =
|
||||||
|
ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||||
|
|
||||||
avatarView.transitionName = account.avatar
|
avatarView.transitionName = account.avatar
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar)
|
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar)
|
||||||
|
@ -479,7 +505,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
|
|
||||||
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -555,8 +580,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
|
|
||||||
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
|
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
|
||||||
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
|
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
|
||||||
if(!viewModel.isSelf && followState == FollowState.FOLLOWING
|
if (!viewModel.isSelf && followState == FollowState.FOLLOWING &&
|
||||||
&& (relation.subscribing != null || relation.notifying != null)) {
|
(relation.subscribing != null || relation.notifying != null)
|
||||||
|
) {
|
||||||
binding.accountSubscribeButton.show()
|
binding.accountSubscribeButton.show()
|
||||||
binding.accountSubscribeButton.setOnClickListener {
|
binding.accountSubscribeButton.setOnClickListener {
|
||||||
viewModel.changeSubscribingState()
|
viewModel.changeSubscribingState()
|
||||||
|
@ -649,7 +675,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
binding.accountMuteButton.hide()
|
binding.accountMuteButton.hide()
|
||||||
updateMuteButton()
|
updateMuteButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
binding.accountFloatingActionButton.hide()
|
binding.accountFloatingActionButton.hide()
|
||||||
binding.accountFollowButton.hide()
|
binding.accountFollowButton.hide()
|
||||||
|
@ -699,11 +724,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.action_show_reblogs)
|
getString(R.string.action_show_reblogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
menu.removeItem(R.id.action_show_reblogs)
|
menu.removeItem(R.id.action_show_reblogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// It shouldn't be possible to block, mute or report yourself.
|
// It shouldn't be possible to block, mute or report yourself.
|
||||||
menu.removeItem(R.id.action_block)
|
menu.removeItem(R.id.action_block)
|
||||||
|
@ -773,8 +796,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
|
|
||||||
private fun mention() {
|
private fun mention() {
|
||||||
loadedAccount?.let {
|
loadedAccount?.let {
|
||||||
val intent = ComposeActivity.startIntent(this,
|
val intent = ComposeActivity.startIntent(
|
||||||
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username)))
|
this,
|
||||||
|
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))
|
||||||
|
)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -850,5 +875,4 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -13,19 +13,22 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter
|
package com.keylesspalace.tusky.components.account
|
||||||
|
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
|
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.Field
|
import com.keylesspalace.tusky.entity.Field
|
||||||
import com.keylesspalace.tusky.entity.IdentityProof
|
import com.keylesspalace.tusky.entity.IdentityProof
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
import com.keylesspalace.tusky.util.Either
|
||||||
|
import com.keylesspalace.tusky.util.LinkHelper
|
||||||
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
|
||||||
class AccountFieldAdapter(
|
class AccountFieldAdapter(
|
||||||
private val linkListener: LinkListener,
|
private val linkListener: LinkListener,
|
||||||
|
@ -70,6 +73,5 @@ class AccountFieldAdapter(
|
||||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,14 +13,14 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.pager
|
package com.keylesspalace.tusky.components.account
|
||||||
|
|
||||||
import androidx.fragment.app.*
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.keylesspalace.tusky.fragment.AccountMediaFragment
|
import com.keylesspalace.tusky.components.account.media.AccountMediaFragment
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||||
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||||
|
|
||||||
import com.keylesspalace.tusky.util.CustomFragmentStateAdapter
|
import com.keylesspalace.tusky.util.CustomFragmentStateAdapter
|
||||||
|
|
||||||
class AccountPagerAdapter(
|
class AccountPagerAdapter(
|
||||||
|
@ -32,9 +32,9 @@ class AccountPagerAdapter(
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId, false)
|
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false)
|
||||||
1 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId, false)
|
1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false)
|
||||||
2 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId, false)
|
2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
|
||||||
3 -> AccountMediaFragment.newInstance(accountId, false)
|
3 -> AccountMediaFragment.newInstance(accountId, false)
|
||||||
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
|
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
|
||||||
}
|
}
|
|
@ -1,17 +1,28 @@
|
||||||
package com.keylesspalace.tusky.viewmodel
|
package com.keylesspalace.tusky.components.account
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.keylesspalace.tusky.appstore.*
|
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.DomainMuteEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Field
|
import com.keylesspalace.tusky.entity.Field
|
||||||
import com.keylesspalace.tusky.entity.IdentityProof
|
import com.keylesspalace.tusky.entity.IdentityProof
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.Either
|
||||||
import io.reactivex.Single
|
import com.keylesspalace.tusky.util.Error
|
||||||
import io.reactivex.disposables.Disposable
|
import com.keylesspalace.tusky.util.Loading
|
||||||
|
import com.keylesspalace.tusky.util.Resource
|
||||||
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import com.keylesspalace.tusky.util.combineOptionalLiveData
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
@ -59,16 +70,19 @@ class AccountViewModel @Inject constructor(
|
||||||
accountData.postValue(Loading())
|
accountData.postValue(Loading())
|
||||||
|
|
||||||
mastodonApi.account(accountId)
|
mastodonApi.account(accountId)
|
||||||
.subscribe({ account ->
|
.subscribe(
|
||||||
|
{ account ->
|
||||||
accountData.postValue(Success(account))
|
accountData.postValue(Success(account))
|
||||||
isDataLoading = false
|
isDataLoading = false
|
||||||
isRefreshing.postValue(false)
|
isRefreshing.postValue(false)
|
||||||
}, {t ->
|
},
|
||||||
|
{ t ->
|
||||||
Log.w(TAG, "failed obtaining account", t)
|
Log.w(TAG, "failed obtaining account", t)
|
||||||
accountData.postValue(Error())
|
accountData.postValue(Error())
|
||||||
isDataLoading = false
|
isDataLoading = false
|
||||||
isRefreshing.postValue(false)
|
isRefreshing.postValue(false)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,12 +93,15 @@ class AccountViewModel @Inject constructor(
|
||||||
relationshipData.postValue(Loading())
|
relationshipData.postValue(Loading())
|
||||||
|
|
||||||
mastodonApi.relationships(listOf(accountId))
|
mastodonApi.relationships(listOf(accountId))
|
||||||
.subscribe({ relationships ->
|
.subscribe(
|
||||||
|
{ relationships ->
|
||||||
relationshipData.postValue(Success(relationships[0]))
|
relationshipData.postValue(Success(relationships[0]))
|
||||||
}, { t ->
|
},
|
||||||
|
{ t ->
|
||||||
Log.w(TAG, "failed obtaining relationships", t)
|
Log.w(TAG, "failed obtaining relationships", t)
|
||||||
relationshipData.postValue(Error())
|
relationshipData.postValue(Error())
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,11 +110,14 @@ class AccountViewModel @Inject constructor(
|
||||||
if (identityProofData.value == null || reload) {
|
if (identityProofData.value == null || reload) {
|
||||||
|
|
||||||
mastodonApi.identityProofs(accountId)
|
mastodonApi.identityProofs(accountId)
|
||||||
.subscribe({ proofs ->
|
.subscribe(
|
||||||
|
{ proofs ->
|
||||||
identityProofData.postValue(proofs)
|
identityProofData.postValue(proofs)
|
||||||
}, { t ->
|
},
|
||||||
|
{ t ->
|
||||||
Log.w(TAG, "failed obtaining identity proofs", t)
|
Log.w(TAG, "failed obtaining identity proofs", t)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,8 +149,9 @@ class AccountViewModel @Inject constructor(
|
||||||
|
|
||||||
fun changeSubscribingState() {
|
fun changeSubscribingState() {
|
||||||
val relationship = relationshipData.value?.data
|
val relationship = relationshipData.value?.data
|
||||||
if(relationship?.notifying == true /* Mastodon 3.3.0rc1 */
|
if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */
|
||||||
|| relationship?.subscribing == true /* Pleroma */ ) {
|
relationship?.subscribing == true /* Pleroma */
|
||||||
|
) {
|
||||||
changeRelationship(RelationShipAction.UNSUBSCRIBE)
|
changeRelationship(RelationShipAction.UNSUBSCRIBE)
|
||||||
} else {
|
} else {
|
||||||
changeRelationship(RelationShipAction.SUBSCRIBE)
|
changeRelationship(RelationShipAction.SUBSCRIBE)
|
||||||
|
@ -269,11 +290,14 @@ class AccountViewModel @Inject constructor(
|
||||||
noteSaved.postValue(true)
|
noteSaved.postValue(true)
|
||||||
}
|
}
|
||||||
.delay(4, TimeUnit.SECONDS)
|
.delay(4, TimeUnit.SECONDS)
|
||||||
.subscribe({
|
.subscribe(
|
||||||
|
{
|
||||||
noteSaved.postValue(false)
|
noteSaved.postValue(false)
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
Log.e(TAG, "Error updating note", it)
|
Log.e(TAG, "Error updating note", it)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment
|
package com.keylesspalace.tusky.components.account.media
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -27,6 +27,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import autodispose2.androidx.lifecycle.autoDispose
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
|
@ -43,13 +44,12 @@ import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.view.SquareImageView
|
import com.keylesspalace.tusky.view.SquareImageView
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.SingleObserver
|
import io.reactivex.rxjava3.core.SingleObserver
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.disposables.Disposable
|
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.Random
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -163,7 +163,6 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
|
||||||
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
||||||
val layoutManager = GridLayoutManager(view.context, columnCount)
|
val layoutManager = GridLayoutManager(view.context, columnCount)
|
||||||
|
|
||||||
|
@ -188,7 +187,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
||||||
val lastItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
val lastItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||||
if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) {
|
if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) {
|
||||||
statuses.lastOrNull()?.let { (id) ->
|
statuses.lastOrNull()?.let { (id) ->
|
||||||
Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)")
|
Log.d(TAG, "Requesting statuses with max_id: $id, (bottom)")
|
||||||
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
|
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
|
||||||
api.accountStatuses(accountId, id, null, null, null, true, null)
|
api.accountStatuses(accountId, id, null, null, null, true, null)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -230,8 +229,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
|
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
|
||||||
.subscribe(callback)
|
.subscribe(callback)
|
||||||
}
|
} else if (needToRefresh)
|
||||||
else if (needToRefresh)
|
|
||||||
refresh()
|
refresh()
|
||||||
needToRefresh = false
|
needToRefresh = false
|
||||||
}
|
}
|
||||||
|
@ -310,9 +308,8 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
||||||
.into(holder.imageView)
|
.into(holder.imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class MediaViewHolder(val imageView: ImageView) :
|
||||||
inner class MediaViewHolder(val imageView: ImageView)
|
RecyclerView.ViewHolder(imageView),
|
||||||
: RecyclerView.ViewHolder(imageView),
|
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener(this)
|
itemView.setOnClickListener(this)
|
|
@ -15,21 +15,25 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.announcements
|
package com.keylesspalace.tusky.components.announcements
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.size
|
import androidx.core.view.size
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding
|
import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding
|
||||||
import com.keylesspalace.tusky.entity.Announcement
|
import com.keylesspalace.tusky.entity.Announcement
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
import com.keylesspalace.tusky.util.EmojiSpan
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
import com.keylesspalace.tusky.util.LinkHelper
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
interface AnnouncementActionListener : LinkListener {
|
interface AnnouncementActionListener : LinkListener {
|
||||||
fun openReactionPicker(announcementId: String, target: View)
|
fun openReactionPicker(announcementId: String, target: View)
|
||||||
|
@ -56,7 +60,9 @@ class AnnouncementAdapter(
|
||||||
val chips = holder.binding.chipGroup
|
val chips = holder.binding.chipGroup
|
||||||
val addReactionChip = holder.binding.addReactionChip
|
val addReactionChip = holder.binding.addReactionChip
|
||||||
|
|
||||||
LinkHelper.setClickableText(text, item.content, null, listener)
|
val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis)
|
||||||
|
|
||||||
|
LinkHelper.setClickableText(text, emojifiedText, item.mentions, listener)
|
||||||
|
|
||||||
// If wellbeing mode is enabled, announcement badge counts should not be shown.
|
// If wellbeing mode is enabled, announcement badge counts should not be shown.
|
||||||
if (wellbeingEnabled) {
|
if (wellbeingEnabled) {
|
||||||
|
@ -67,29 +73,32 @@ class AnnouncementAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
item.reactions.forEachIndexed { i, reaction ->
|
item.reactions.forEachIndexed { i, reaction ->
|
||||||
(chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
(
|
||||||
|
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
||||||
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
|
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
|
||||||
isCheckable = true
|
isCheckable = true
|
||||||
checkedIcon = null
|
checkedIcon = null
|
||||||
chips.addView(this, i)
|
chips.addView(this, i)
|
||||||
})
|
|
||||||
.apply {
|
|
||||||
val emojiText = if (reaction.url == null) {
|
|
||||||
reaction.name
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.emoji_shortcode_format, reaction.name)
|
|
||||||
}
|
}
|
||||||
this.text = ("$emojiText ${reaction.count}")
|
|
||||||
.emojify(
|
|
||||||
listOf(Emoji(
|
|
||||||
reaction.name,
|
|
||||||
reaction.url ?: "",
|
|
||||||
reaction.staticUrl ?: "",
|
|
||||||
null
|
|
||||||
)),
|
|
||||||
this,
|
|
||||||
animateEmojis
|
|
||||||
)
|
)
|
||||||
|
.apply {
|
||||||
|
if (reaction.url == null) {
|
||||||
|
this.text = "${reaction.name} ${reaction.count}"
|
||||||
|
} else {
|
||||||
|
// we set the EmojiSpan on a space, because otherwise the Chip won't have the right size
|
||||||
|
// https://github.com/tuskyapp/Tusky/issues/2308
|
||||||
|
val spanBuilder = SpannableStringBuilder(" ${reaction.count}")
|
||||||
|
val span = EmojiSpan(WeakReference(this))
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
span.contentDescription = reaction.name
|
||||||
|
}
|
||||||
|
spanBuilder.setSpan(span, 0, 1, 0)
|
||||||
|
Glide.with(this)
|
||||||
|
.asDrawable()
|
||||||
|
.load(if (animateEmojis) { reaction.url } else { reaction.staticUrl })
|
||||||
|
.into(span.getTarget(animateEmojis))
|
||||||
|
this.text = spanBuilder
|
||||||
|
}
|
||||||
|
|
||||||
isChecked = reaction.me
|
isChecked = reaction.me
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,12 @@ import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.Error
|
||||||
|
import com.keylesspalace.tusky.util.Loading
|
||||||
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.view.EmojiPicker
|
import com.keylesspalace.tusky.view.EmojiPicker
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,13 @@ import com.keylesspalace.tusky.entity.Announcement
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.Instance
|
import com.keylesspalace.tusky.entity.Instance
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.Either
|
||||||
import io.reactivex.rxkotlin.Singles
|
import com.keylesspalace.tusky.util.Error
|
||||||
|
import com.keylesspalace.tusky.util.Loading
|
||||||
|
import com.keylesspalace.tusky.util.Resource
|
||||||
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AnnouncementsViewModel @Inject constructor(
|
class AnnouncementsViewModel @Inject constructor(
|
||||||
|
@ -45,15 +50,15 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
val emojis: LiveData<List<Emoji>> = emojisMutable
|
val emojis: LiveData<List<Emoji>> = emojisMutable
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Singles.zip(
|
Single.zip(
|
||||||
mastodonApi.getCustomEmojis(),
|
mastodonApi.getCustomEmojis(),
|
||||||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||||
.onErrorResumeNext(
|
.onErrorResumeNext {
|
||||||
mastodonApi.getInstance()
|
mastodonApi.getInstance()
|
||||||
.map { Either.Right(it) }
|
.map { Either.Right(it) }
|
||||||
)
|
},
|
||||||
) { emojis, either ->
|
{ emojis, either ->
|
||||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||||
?: InstanceEntity(
|
?: InstanceEntity(
|
||||||
accountManager.activeAccount?.domain!!,
|
accountManager.activeAccount?.domain!!,
|
||||||
|
@ -64,21 +69,26 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
either.asRight().version
|
either.asRight().version
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
.doOnSuccess {
|
.doOnSuccess {
|
||||||
appDatabase.instanceDao().insertOrReplace(it)
|
appDatabase.instanceDao().insertOrReplace(it)
|
||||||
}
|
}
|
||||||
.subscribe({
|
.subscribe(
|
||||||
emojisMutable.postValue(it.emojiList)
|
{
|
||||||
}, {
|
emojisMutable.postValue(it.emojiList.orEmpty())
|
||||||
|
},
|
||||||
|
{
|
||||||
Log.w(TAG, "Failed to get custom emojis.", it)
|
Log.w(TAG, "Failed to get custom emojis.", it)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
announcementsMutable.postValue(Loading())
|
announcementsMutable.postValue(Loading())
|
||||||
mastodonApi.listAnnouncements()
|
mastodonApi.listAnnouncements()
|
||||||
.subscribe({
|
.subscribe(
|
||||||
|
{
|
||||||
announcementsMutable.postValue(Success(it))
|
announcementsMutable.postValue(Success(it))
|
||||||
it.filter { announcement -> !announcement.read }
|
it.filter { announcement -> !announcement.read }
|
||||||
.forEach { announcement ->
|
.forEach { announcement ->
|
||||||
|
@ -93,15 +103,18 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
announcementsMutable.postValue(Error(cause = it))
|
announcementsMutable.postValue(Error(cause = it))
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addReaction(announcementId: String, name: String) {
|
fun addReaction(announcementId: String, name: String) {
|
||||||
mastodonApi.addAnnouncementReaction(announcementId, name)
|
mastodonApi.addAnnouncementReaction(announcementId, name)
|
||||||
.subscribe({
|
.subscribe(
|
||||||
|
{
|
||||||
announcementsMutable.postValue(
|
announcementsMutable.postValue(
|
||||||
Success(
|
Success(
|
||||||
announcements.value!!.data!!.map { announcement ->
|
announcements.value!!.data!!.map { announcement ->
|
||||||
|
@ -140,15 +153,18 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeReaction(announcementId: String, name: String) {
|
fun removeReaction(announcementId: String, name: String) {
|
||||||
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
||||||
.subscribe({
|
.subscribe(
|
||||||
|
{
|
||||||
announcementsMutable.postValue(
|
announcementsMutable.postValue(
|
||||||
Success(
|
Success(
|
||||||
announcements.value!!.data!!.map { announcement ->
|
announcements.value!!.data!!.map { announcement ->
|
||||||
|
@ -175,9 +191,11 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
package com.keylesspalace.tusky.components.compose
|
package com.keylesspalace.tusky.components.compose
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity
|
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -28,13 +27,16 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
@ -71,7 +73,20 @@ import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.ComposeTokenizer
|
||||||
|
import com.keylesspalace.tusky.util.PickMediaFiles
|
||||||
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
|
import com.keylesspalace.tusky.util.afterTextChanged
|
||||||
|
import com.keylesspalace.tusky.util.combineLiveData
|
||||||
|
import com.keylesspalace.tusky.util.combineOptionalLiveData
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.highlightSpans
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
import com.keylesspalace.tusky.util.onTextChanged
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import com.keylesspalace.tusky.util.withLifecycleContext
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
|
@ -84,7 +99,8 @@ import javax.inject.Inject
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class ComposeActivity : BaseActivity(),
|
class ComposeActivity :
|
||||||
|
BaseActivity(),
|
||||||
ComposeOptionsListener,
|
ComposeOptionsListener,
|
||||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
|
@ -114,6 +130,21 @@ class ComposeActivity : BaseActivity(),
|
||||||
private val maxUploadMediaNumber = 4
|
private val maxUploadMediaNumber = 4
|
||||||
private var mediaCount = 0
|
private var mediaCount = 0
|
||||||
|
|
||||||
|
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||||
|
if (success) {
|
||||||
|
pickMedia(photoUploadUri!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
|
||||||
|
if (mediaCount + uris.size > maxUploadMediaNumber) {
|
||||||
|
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
uris.forEach { uri ->
|
||||||
|
pickMedia(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -274,8 +305,9 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O ||
|
||||||
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1
|
||||||
|
) {
|
||||||
binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,9 +348,9 @@ class ComposeActivity : BaseActivity(),
|
||||||
updateScheduleButton()
|
updateScheduleButton()
|
||||||
}
|
}
|
||||||
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
|
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
|
||||||
val active = poll == null
|
val active = poll == null &&
|
||||||
&& media!!.size != 4
|
media!!.size != 4 &&
|
||||||
&& (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
||||||
enableButton(binding.composeAddMediaButton, active, active)
|
enableButton(binding.composeAddMediaButton, active, active)
|
||||||
enablePollButton(media.isNullOrEmpty())
|
enablePollButton(media.isNullOrEmpty())
|
||||||
}.subscribe()
|
}.subscribe()
|
||||||
|
@ -379,7 +411,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
setHomeAsUpIndicator(R.drawable.ic_close_24dp)
|
setHomeAsUpIndicator(R.drawable.ic_close_24dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) {
|
private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) {
|
||||||
|
@ -395,8 +426,10 @@ class ComposeActivity : BaseActivity(),
|
||||||
avatarSize / 8,
|
avatarSize / 8,
|
||||||
animateAvatars
|
animateAvatars
|
||||||
)
|
)
|
||||||
binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description,
|
binding.composeAvatar.contentDescription = getString(
|
||||||
activeAccount.fullName)
|
R.string.compose_active_account_description,
|
||||||
|
activeAccount.fullName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun replaceTextAtCaret(text: CharSequence) {
|
private fun replaceTextAtCaret(text: CharSequence) {
|
||||||
|
@ -454,7 +487,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun atButtonClicked() {
|
private fun atButtonClicked() {
|
||||||
prependSelectedWordsWith("@")
|
prependSelectedWordsWith("@")
|
||||||
}
|
}
|
||||||
|
@ -488,7 +520,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
||||||
binding.composeHideMediaButton.isClickable = false
|
binding.composeHideMediaButton.isClickable = false
|
||||||
ContextCompat.getColor(this, R.color.transparent_chinwag_green)
|
ContextCompat.getColor(this, R.color.transparent_chinwag_green)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
binding.composeHideMediaButton.isClickable = true
|
binding.composeHideMediaButton.isClickable = true
|
||||||
if (markMediaSensitive) {
|
if (markMediaSensitive) {
|
||||||
|
@ -601,11 +632,13 @@ class ComposeActivity : BaseActivity(),
|
||||||
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
addMediaBehavior.removeBottomSheetCallback(this)
|
addMediaBehavior.removeBottomSheetCallback(this)
|
||||||
if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||||
ActivityCompat.requestPermissions(this@ComposeActivity,
|
ActivityCompat.requestPermissions(
|
||||||
|
this@ComposeActivity,
|
||||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
|
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
initiateMediaPicking()
|
pickMediaFile.launch(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -619,8 +652,10 @@ class ComposeActivity : BaseActivity(),
|
||||||
private fun openPollDialog() {
|
private fun openPollDialog() {
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
val instanceParams = viewModel.instanceParams.value!!
|
val instanceParams = viewModel.instanceParams.value!!
|
||||||
showAddPollDialog(this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
showAddPollDialog(
|
||||||
instanceParams.pollMaxLength, viewModel::updatePoll)
|
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
||||||
|
instanceParams.pollMaxLength, viewModel::updatePoll
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPollView() {
|
private fun setupPollView() {
|
||||||
|
@ -741,34 +776,39 @@ class ComposeActivity : BaseActivity(),
|
||||||
if (viewModel.media.value!!.isNotEmpty()) {
|
if (viewModel.media.value!!.isNotEmpty()) {
|
||||||
finishingUploadDialog = ProgressDialog.show(
|
finishingUploadDialog = ProgressDialog.show(
|
||||||
this, getString(R.string.dialog_title_finishing_media_upload),
|
this, getString(R.string.dialog_title_finishing_media_upload),
|
||||||
getString(R.string.dialog_message_uploading_media), true, true)
|
getString(R.string.dialog_message_uploading_media), true, true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.sendStatus(contentText, spoilerText).observe(this, {
|
viewModel.sendStatus(contentText, spoilerText).observe(
|
||||||
|
this,
|
||||||
|
{
|
||||||
finishingUploadDialog?.dismiss()
|
finishingUploadDialog?.dismiss()
|
||||||
deleteDraftAndFinish()
|
deleteDraftAndFinish()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||||
enableButtons(true)
|
enableButtons(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
grantResults: IntArray) {
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
|
||||||
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
|
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
initiateMediaPicking()
|
pickMediaFile.launch(true)
|
||||||
} else {
|
} else {
|
||||||
val bar = Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission,
|
Snackbar.make(
|
||||||
Snackbar.LENGTH_SHORT).apply {
|
binding.activityCompose, R.string.error_media_upload_permission,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
}
|
).apply {
|
||||||
bar.setAction(R.string.action_retry) { onMediaPick() }
|
setAction(R.string.action_retry) { onMediaPick() }
|
||||||
// necessary so snackbar is shown over everything
|
// necessary so snackbar is shown over everything
|
||||||
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||||
bar.show()
|
show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -776,11 +816,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
private fun initiateCameraApp() {
|
private fun initiateCameraApp() {
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
|
||||||
// We don't need to ask for permission in this case, because the used calls require
|
|
||||||
// android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was
|
|
||||||
// way before permission dialogues have been introduced.
|
|
||||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
|
||||||
val photoFile: File = try {
|
val photoFile: File = try {
|
||||||
createNewImageFile(this)
|
createNewImageFile(this)
|
||||||
} catch (ex: IOException) {
|
} catch (ex: IOException) {
|
||||||
|
@ -789,37 +824,30 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue only if the File was successfully created
|
// Continue only if the File was successfully created
|
||||||
photoUploadUri = FileProvider.getUriForFile(this,
|
photoUploadUri = FileProvider.getUriForFile(
|
||||||
|
this,
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
photoFile)
|
photoFile
|
||||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri)
|
)
|
||||||
startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT)
|
takePicture.launch(photoUploadUri)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initiateMediaPicking() {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
|
|
||||||
val mimeTypes = arrayOf("image/*", "video/*", "audio/*")
|
|
||||||
intent.type = "*/*"
|
|
||||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
|
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
||||||
startActivityForResult(intent, MEDIA_PICK_RESULT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
||||||
button.isEnabled = clickable
|
button.isEnabled = clickable
|
||||||
ThemeUtils.setDrawableTint(this, button.drawable,
|
ThemeUtils.setDrawableTint(
|
||||||
|
this, button.drawable,
|
||||||
if (colorActive) android.R.attr.textColorTertiary
|
if (colorActive) android.R.attr.textColorTertiary
|
||||||
else R.attr.textColorDisabled)
|
else R.attr.textColorDisabled
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enablePollButton(enable: Boolean) {
|
private fun enablePollButton(enable: Boolean) {
|
||||||
binding.addPollTextActionTextView.isEnabled = enable
|
binding.addPollTextActionTextView.isEnabled = enable
|
||||||
val textColor = ThemeUtils.getColor(this,
|
val textColor = ThemeUtils.getColor(
|
||||||
|
this,
|
||||||
if (enable) android.R.attr.textColorTertiary
|
if (enable) android.R.attr.textColorTertiary
|
||||||
else R.attr.textColorDisabled)
|
else R.attr.textColorDisabled
|
||||||
|
)
|
||||||
binding.addPollTextActionTextView.setTextColor(textColor)
|
binding.addPollTextActionTextView.setTextColor(textColor)
|
||||||
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||||
}
|
}
|
||||||
|
@ -828,31 +856,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
viewModel.removeMediaFromQueue(item)
|
viewModel.removeMediaFromQueue(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, intent)
|
|
||||||
if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
|
|
||||||
if (intent.data != null) {
|
|
||||||
// Single media, upload it and done.
|
|
||||||
pickMedia(intent.data!!)
|
|
||||||
} else if (intent.clipData != null) {
|
|
||||||
val clipData = intent.clipData!!
|
|
||||||
val count = clipData.itemCount
|
|
||||||
if (mediaCount + count > maxUploadMediaNumber) {
|
|
||||||
// check if exist media + upcoming media > 4, then prob error message.
|
|
||||||
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
// if not grater then 4, upload all multiple media.
|
|
||||||
for (i in 0 until count) {
|
|
||||||
val imageUri = clipData.getItemAt(i).getUri()
|
|
||||||
pickMedia(imageUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
|
|
||||||
pickMedia(photoUploadUri!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) {
|
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) {
|
||||||
withLifecycleContext {
|
withLifecycleContext {
|
||||||
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
||||||
|
@ -876,7 +879,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
displayTransientError(errorId)
|
displayTransientError(errorId)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -910,7 +912,8 @@ class ComposeActivity : BaseActivity(),
|
||||||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
|
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
) {
|
||||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
@ -1013,7 +1016,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
data class ComposeOptions(
|
data class ComposeOptions(
|
||||||
// Let's keep fields var until all consumers are Kotlin
|
// Let's keep fields var until all consumers are Kotlin
|
||||||
var scheduledTootId: String? = null,
|
var scheduledTootId: String? = null,
|
||||||
var savedTootUid: Int? = null,
|
|
||||||
var draftId: Int? = null,
|
var draftId: Int? = null,
|
||||||
var tootText: String? = null,
|
var tootText: String? = null,
|
||||||
var mediaUrls: List<String>? = null,
|
var mediaUrls: List<String>? = null,
|
||||||
|
@ -1035,8 +1037,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ComposeActivity" // logging tag
|
private const val TAG = "ComposeActivity" // logging tag
|
||||||
private const val MEDIA_PICK_RESULT = 1
|
|
||||||
private const val MEDIA_TAKE_PHOTO_RESULT = 2
|
|
||||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||||
|
|
||||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||||
|
|
|
@ -21,22 +21,34 @@ import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
import com.keylesspalace.tusky.components.search.SearchType
|
import com.keylesspalace.tusky.components.search.SearchType
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.InstanceEntity
|
import com.keylesspalace.tusky.db.InstanceEntity
|
||||||
import com.keylesspalace.tusky.entity.*
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.TootToSend
|
import com.keylesspalace.tusky.service.TootToSend
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.Either
|
||||||
import io.reactivex.Observable.just
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
import io.reactivex.disposables.Disposable
|
import com.keylesspalace.tusky.util.VersionUtils
|
||||||
import io.reactivex.rxkotlin.Singles
|
import com.keylesspalace.tusky.util.combineLiveData
|
||||||
import java.util.*
|
import com.keylesspalace.tusky.util.filter
|
||||||
|
import com.keylesspalace.tusky.util.map
|
||||||
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
|
import com.keylesspalace.tusky.util.toLiveData
|
||||||
|
import com.keylesspalace.tusky.util.withoutFirstWhich
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ComposeViewModel @Inject constructor(
|
class ComposeViewModel @Inject constructor(
|
||||||
|
@ -45,14 +57,12 @@ class ComposeViewModel @Inject constructor(
|
||||||
private val mediaUploader: MediaUploader,
|
private val mediaUploader: MediaUploader,
|
||||||
private val serviceClient: ServiceClient,
|
private val serviceClient: ServiceClient,
|
||||||
private val draftHelper: DraftHelper,
|
private val draftHelper: DraftHelper,
|
||||||
private val saveTootHelper: SaveTootHelper,
|
|
||||||
private val db: AppDatabase
|
private val db: AppDatabase
|
||||||
) : RxAwareViewModel() {
|
) : RxAwareViewModel() {
|
||||||
|
|
||||||
private var replyingStatusAuthor: String? = null
|
private var replyingStatusAuthor: String? = null
|
||||||
private var replyingStatusContent: String? = null
|
private var replyingStatusContent: String? = null
|
||||||
internal var startingText: String? = null
|
internal var startingText: String? = null
|
||||||
private var savedTootUid: Int = 0
|
|
||||||
private var draftId: Int = 0
|
private var draftId: Int = 0
|
||||||
private var scheduledTootId: String? = null
|
private var scheduledTootId: String? = null
|
||||||
private var startingContentWarning: String = ""
|
private var startingContentWarning: String = ""
|
||||||
|
@ -91,7 +101,9 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance ->
|
Single.zip(
|
||||||
|
api.getCustomEmojis(), api.getInstance(),
|
||||||
|
{ emojis, instance ->
|
||||||
InstanceEntity(
|
InstanceEntity(
|
||||||
instance = accountManager.activeAccount?.domain!!,
|
instance = accountManager.activeAccount?.domain!!,
|
||||||
emojiList = emojis,
|
emojiList = emojis,
|
||||||
|
@ -101,19 +113,23 @@ class ComposeViewModel @Inject constructor(
|
||||||
version = instance.version
|
version = instance.version
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
.doOnSuccess {
|
.doOnSuccess {
|
||||||
db.instanceDao().insertOrReplace(it)
|
db.instanceDao().insertOrReplace(it)
|
||||||
}
|
}
|
||||||
.onErrorResumeNext(
|
.onErrorResumeNext {
|
||||||
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||||
)
|
}
|
||||||
.subscribe({ instanceEntity ->
|
.subscribe(
|
||||||
|
{ instanceEntity ->
|
||||||
emoji.postValue(instanceEntity.emojiList)
|
emoji.postValue(instanceEntity.emojiList)
|
||||||
instance.postValue(instanceEntity)
|
instance.postValue(instanceEntity)
|
||||||
}, { throwable ->
|
},
|
||||||
|
{ throwable ->
|
||||||
// this can happen on network error when no cached data is available
|
// this can happen on network error when no cached data is available
|
||||||
Log.w(TAG, "error loading instance data", throwable)
|
Log.w(TAG, "error loading instance data", throwable)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,19 +140,23 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaUploader.prepareMedia(uri)
|
mediaUploader.prepareMedia(uri)
|
||||||
.map { (type, uri, size) ->
|
.map { (type, uri, size) ->
|
||||||
val mediaItems = media.value!!
|
val mediaItems = media.value!!
|
||||||
if (type != QueuedMedia.Type.IMAGE
|
if (type != QueuedMedia.Type.IMAGE &&
|
||||||
&& mediaItems.isNotEmpty()
|
mediaItems.isNotEmpty() &&
|
||||||
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
|
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
||||||
|
) {
|
||||||
throw VideoOrImageException()
|
throw VideoOrImageException()
|
||||||
} else {
|
} else {
|
||||||
addMediaToQueue(type, uri, size, description)
|
addMediaToQueue(type, uri, size, description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribe({ queuedMedia ->
|
.subscribe(
|
||||||
|
{ queuedMedia ->
|
||||||
liveData.postValue(Either.Right(queuedMedia))
|
liveData.postValue(Either.Right(queuedMedia))
|
||||||
}, { error ->
|
},
|
||||||
|
{ error ->
|
||||||
liveData.postValue(Either.Left(error))
|
liveData.postValue(Either.Left(error))
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
return liveData
|
return liveData
|
||||||
}
|
}
|
||||||
|
@ -157,7 +177,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
media.value = media.value!! + mediaItem
|
media.value = media.value!! + mediaItem
|
||||||
mediaToDisposable[mediaItem.localId] = mediaUploader
|
mediaToDisposable[mediaItem.localId] = mediaUploader
|
||||||
.uploadMedia(mediaItem)
|
.uploadMedia(mediaItem)
|
||||||
.subscribe({ event ->
|
.subscribe(
|
||||||
|
{ event ->
|
||||||
val item = media.value?.find { it.localId == mediaItem.localId }
|
val item = media.value?.find { it.localId == mediaItem.localId }
|
||||||
?: return@subscribe
|
?: return@subscribe
|
||||||
val newMediaItem = when (event) {
|
val newMediaItem = when (event) {
|
||||||
|
@ -169,16 +190,20 @@ class ComposeViewModel @Inject constructor(
|
||||||
synchronized(media) {
|
synchronized(media) {
|
||||||
val mediaValue = media.value!!
|
val mediaValue = media.value!!
|
||||||
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
|
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
|
||||||
media.postValue(if (index == -1) {
|
media.postValue(
|
||||||
|
if (index == -1) {
|
||||||
mediaValue + newMediaItem
|
mediaValue + newMediaItem
|
||||||
} else {
|
} else {
|
||||||
mediaValue.toMutableList().also { it[index] = newMediaItem }
|
mediaValue.toMutableList().also { it[index] = newMediaItem }
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, { error ->
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
|
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
|
||||||
uploadError.postValue(error)
|
uploadError.postValue(error)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return mediaItem
|
return mediaItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,12 +223,14 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
fun didChange(content: String?, contentWarning: String?): Boolean {
|
fun didChange(content: String?, contentWarning: String?): Boolean {
|
||||||
|
|
||||||
val textChanged = !(content.isNullOrEmpty()
|
val textChanged = !(
|
||||||
|| startingText?.startsWith(content.toString()) ?: false)
|
content.isNullOrEmpty() ||
|
||||||
|
startingText?.startsWith(content.toString()) ?: false
|
||||||
|
)
|
||||||
|
|
||||||
val contentWarningChanged = showContentWarning.value!!
|
val contentWarningChanged = showContentWarning.value!! &&
|
||||||
&& !contentWarning.isNullOrEmpty()
|
!contentWarning.isNullOrEmpty() &&
|
||||||
&& !startingContentWarning.startsWith(contentWarning.toString())
|
!startingContentWarning.startsWith(contentWarning.toString())
|
||||||
val mediaChanged = !media.value.isNullOrEmpty()
|
val mediaChanged = !media.value.isNullOrEmpty()
|
||||||
val pollChanged = poll.value != null
|
val pollChanged = poll.value != null
|
||||||
|
|
||||||
|
@ -216,17 +243,15 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDraft() {
|
fun deleteDraft() {
|
||||||
if (savedTootUid != 0) {
|
viewModelScope.launch {
|
||||||
saveTootHelper.deleteDraft(savedTootUid)
|
|
||||||
}
|
|
||||||
if (draftId != 0) {
|
if (draftId != 0) {
|
||||||
draftHelper.deleteDraftAndAttachments(draftId)
|
draftHelper.deleteDraftAndAttachments(draftId)
|
||||||
.subscribe()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDraft(content: String, contentWarning: String) {
|
fun saveDraft(content: String, contentWarning: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
val mediaUris: MutableList<String> = mutableListOf()
|
val mediaUris: MutableList<String> = mutableListOf()
|
||||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||||
media.value?.forEach { item ->
|
media.value?.forEach { item ->
|
||||||
|
@ -246,7 +271,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaDescriptions = mediaDescriptions,
|
mediaDescriptions = mediaDescriptions,
|
||||||
poll = poll.value,
|
poll = poll.value,
|
||||||
failedToSend = false
|
failedToSend = false
|
||||||
).subscribe()
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -262,7 +288,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
val deletionObservable = if (isEditingScheduledToot) {
|
val deletionObservable = if (isEditingScheduledToot) {
|
||||||
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
||||||
} else {
|
} else {
|
||||||
just(Unit)
|
Observable.just(Unit)
|
||||||
}.toLiveData()
|
}.toLiveData()
|
||||||
|
|
||||||
val sendObservable = media
|
val sendObservable = media
|
||||||
|
@ -291,7 +317,6 @@ class ComposeViewModel @Inject constructor(
|
||||||
replyingStatusContent = null,
|
replyingStatusContent = null,
|
||||||
replyingStatusAuthorUsername = null,
|
replyingStatusAuthorUsername = null,
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
savedTootUid = savedTootUid,
|
|
||||||
draftId = draftId,
|
draftId = draftId,
|
||||||
idempotencyKey = randomAlphanumericString(16),
|
idempotencyKey = randomAlphanumericString(16),
|
||||||
retries = 0
|
retries = 0
|
||||||
|
@ -318,11 +343,14 @@ class ComposeViewModel @Inject constructor(
|
||||||
media.removeObserver(this)
|
media.removeObserver(this)
|
||||||
} else if (updatedItem.id != null) {
|
} else if (updatedItem.id != null) {
|
||||||
api.updateMedia(updatedItem.id, description)
|
api.updateMedia(updatedItem.id, description)
|
||||||
.subscribe({
|
.subscribe(
|
||||||
|
{
|
||||||
completedCaptioningLiveData.postValue(true)
|
completedCaptioningLiveData.postValue(true)
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
completedCaptioningLiveData.postValue(false)
|
completedCaptioningLiveData.postValue(false)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
media.removeObserver(this)
|
media.removeObserver(this)
|
||||||
}
|
}
|
||||||
|
@ -357,11 +385,11 @@ class ComposeViewModel @Inject constructor(
|
||||||
':' -> {
|
':' -> {
|
||||||
val emojiList = emoji.value ?: return emptyList()
|
val emojiList = emoji.value ?: return emptyList()
|
||||||
|
|
||||||
val incomplete = token.substring(1).toLowerCase(Locale.ROOT)
|
val incomplete = token.substring(1).lowercase(Locale.ROOT)
|
||||||
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||||
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||||
for (emoji in emojiList) {
|
for (emoji in emojiList) {
|
||||||
val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT)
|
val shortcode = emoji.shortcode.lowercase(Locale.ROOT)
|
||||||
if (shortcode.startsWith(incomplete)) {
|
if (shortcode.startsWith(incomplete)) {
|
||||||
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
||||||
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
||||||
|
@ -391,7 +419,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
||||||
startingVisibility = Status.Visibility.byNum(
|
startingVisibility = Status.Visibility.byNum(
|
||||||
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
|
preferredVisibility.num.coerceAtLeast(replyVisibility.num)
|
||||||
|
)
|
||||||
|
|
||||||
inReplyToId = composeOptions?.inReplyToId
|
inReplyToId = composeOptions?.inReplyToId
|
||||||
|
|
||||||
|
@ -406,20 +435,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// recreate media list
|
// recreate media list
|
||||||
val loadedDraftMediaUris = composeOptions?.mediaUrls
|
|
||||||
val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
|
|
||||||
val draftAttachments = composeOptions?.draftAttachments
|
val draftAttachments = composeOptions?.draftAttachments
|
||||||
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
|
if (draftAttachments != null) {
|
||||||
// when coming from SavedTootActivity
|
|
||||||
loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
|
|
||||||
.forEach { (uri, description) ->
|
|
||||||
pickMedia(uri.toUri()).observeForever { errorOrItem ->
|
|
||||||
if (errorOrItem.isRight() && description != null) {
|
|
||||||
updateDescription(errorOrItem.asRight().localId, description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (draftAttachments != null) {
|
|
||||||
// when coming from DraftActivity
|
// when coming from DraftActivity
|
||||||
draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
|
draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
|
||||||
} else composeOptions?.mediaAttachments?.forEach { a ->
|
} else composeOptions?.mediaAttachments?.forEach { a ->
|
||||||
|
@ -432,7 +449,6 @@ class ComposeViewModel @Inject constructor(
|
||||||
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
savedTootUid = composeOptions?.savedTootUid ?: 0
|
|
||||||
draftId = composeOptions?.draftId ?: 0
|
draftId = composeOptions?.draftId ?: 0
|
||||||
scheduledTootId = composeOptions?.scheduledTootId
|
scheduledTootId = composeOptions?.scheduledTootId
|
||||||
startingText = composeOptions?.tootText
|
startingText = composeOptions?.tootText
|
||||||
|
@ -483,14 +499,13 @@ class ComposeViewModel @Inject constructor(
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "ComposeViewModel"
|
const val TAG = "ComposeViewModel"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
|
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
|
||||||
|
|
||||||
const val DEFAULT_CHARACTER_LIMIT = 500
|
const val DEFAULT_CHARACTER_LIMIT = 500
|
||||||
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
||||||
private const val DEFAULT_MAX_OPTION_LENGTH = 25
|
private const val DEFAULT_MAX_OPTION_LENGTH = 50
|
||||||
|
|
||||||
data class ComposeInstanceParams(
|
data class ComposeInstanceParams(
|
||||||
val maxChars: Int,
|
val maxChars: Int,
|
||||||
|
|
|
@ -81,7 +81,9 @@ class MediaPreviewAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
private val differ = AsyncListDiffer(
|
||||||
|
this,
|
||||||
|
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
||||||
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||||
return oldItem.localId == newItem.localId
|
return oldItem.localId == newItem.localId
|
||||||
}
|
}
|
||||||
|
@ -89,10 +91,11 @@ class MediaPreviewAdapter(
|
||||||
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
inner class PreviewViewHolder(val progressImageView: ProgressImageView)
|
inner class PreviewViewHolder(val progressImageView: ProgressImageView) :
|
||||||
: RecyclerView.ViewHolder(progressImageView) {
|
RecyclerView.ViewHolder(progressImageView) {
|
||||||
init {
|
init {
|
||||||
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
||||||
val margin = itemView.context.resources
|
val margin = itemView.context.resources
|
||||||
|
|
|
@ -28,16 +28,20 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||||
import io.reactivex.Observable
|
import com.keylesspalace.tusky.util.getImageSquarePixels
|
||||||
import io.reactivex.Single
|
import com.keylesspalace.tusky.util.getMediaSize
|
||||||
import io.reactivex.schedulers.Schedulers
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
sealed class UploadEvent {
|
sealed class UploadEvent {
|
||||||
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
||||||
|
@ -58,21 +62,16 @@ fun createNewImageFile(context: Context): File {
|
||||||
|
|
||||||
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
||||||
|
|
||||||
interface MediaUploader {
|
|
||||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia>
|
|
||||||
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent>
|
|
||||||
}
|
|
||||||
|
|
||||||
class AudioSizeException : Exception()
|
class AudioSizeException : Exception()
|
||||||
class VideoSizeException : Exception()
|
class VideoSizeException : Exception()
|
||||||
class MediaTypeException : Exception()
|
class MediaTypeException : Exception()
|
||||||
class CouldNotOpenFileException : Exception()
|
class CouldNotOpenFileException : Exception()
|
||||||
|
|
||||||
class MediaUploaderImpl(
|
class MediaUploader @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val mastodonApi: MastodonApi
|
private val mastodonApi: MastodonApi
|
||||||
) : MediaUploader {
|
) {
|
||||||
override fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
|
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
|
||||||
return Observable
|
return Observable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
if (shouldResizeMedia(media)) {
|
if (shouldResizeMedia(media)) {
|
||||||
|
@ -83,7 +82,7 @@ class MediaUploaderImpl(
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||||
return Single.fromCallable {
|
return Single.fromCallable {
|
||||||
var mediaSize = getMediaSize(contentResolver, inUri)
|
var mediaSize = getMediaSize(contentResolver, inUri)
|
||||||
var uri = inUri
|
var uri = inUri
|
||||||
|
@ -101,12 +100,13 @@ class MediaUploaderImpl(
|
||||||
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
||||||
FileOutputStream(file.absoluteFile).use { out ->
|
FileOutputStream(file.absoluteFile).use { out ->
|
||||||
input.copyTo(out)
|
input.copyTo(out)
|
||||||
uri = FileProvider.getUriForFile(context,
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
file)
|
file
|
||||||
|
)
|
||||||
mediaSize = getMediaSize(contentResolver, uri)
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, e)
|
Log.w(TAG, e)
|
||||||
|
@ -151,20 +151,22 @@ class MediaUploaderImpl(
|
||||||
var mimeType = contentResolver.getType(media.uri)
|
var mimeType = contentResolver.getType(media.uri)
|
||||||
val map = MimeTypeMap.getSingleton()
|
val map = MimeTypeMap.getSingleton()
|
||||||
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
||||||
val filename = String.format("%s_%s_%s.%s",
|
val filename = "%s_%s_%s.%s".format(
|
||||||
context.getString(R.string.app_name),
|
context.getString(R.string.app_name),
|
||||||
Date().time.toString(),
|
Date().time.toString(),
|
||||||
randomAlphanumericString(10),
|
randomAlphanumericString(10),
|
||||||
fileExtension)
|
fileExtension
|
||||||
|
)
|
||||||
|
|
||||||
val stream = contentResolver.openInputStream(media.uri)
|
val stream = contentResolver.openInputStream(media.uri)
|
||||||
|
|
||||||
if (mimeType == null) mimeType = "multipart/form-data"
|
if (mimeType == null) mimeType = "multipart/form-data"
|
||||||
|
|
||||||
|
|
||||||
var lastProgress = -1
|
var lastProgress = -1
|
||||||
val fileBody = ProgressRequestBody(stream, media.mediaSize,
|
val fileBody = ProgressRequestBody(
|
||||||
mimeType.toMediaTypeOrNull()) { percentage ->
|
stream, media.mediaSize,
|
||||||
|
mimeType.toMediaTypeOrNull()
|
||||||
|
) { percentage ->
|
||||||
if (percentage != lastProgress) {
|
if (percentage != lastProgress) {
|
||||||
emitter.onNext(UploadEvent.ProgressEvent(percentage))
|
emitter.onNext(UploadEvent.ProgressEvent(percentage))
|
||||||
}
|
}
|
||||||
|
@ -180,12 +182,15 @@ class MediaUploaderImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
||||||
.subscribe({ attachment ->
|
.subscribe(
|
||||||
|
{ attachment ->
|
||||||
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
||||||
emitter.onComplete()
|
emitter.onComplete()
|
||||||
}, { e ->
|
},
|
||||||
|
{ e ->
|
||||||
emitter.onError(e)
|
emitter.onError(e)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Cancel the request when our observable is cancelled
|
// Cancel the request when our observable is cancelled
|
||||||
emitter.setDisposable(uploadDisposable)
|
emitter.setDisposable(uploadDisposable)
|
||||||
|
@ -194,15 +199,16 @@ class MediaUploaderImpl(
|
||||||
|
|
||||||
private fun downsize(media: QueuedMedia): QueuedMedia {
|
private fun downsize(media: QueuedMedia): QueuedMedia {
|
||||||
val file = createNewImageFile(context)
|
val file = createNewImageFile(context)
|
||||||
DownsizeImageTask.resize(arrayOf(media.uri),
|
DownsizeImageTask.resize(
|
||||||
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file)
|
arrayOf(media.uri),
|
||||||
|
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file
|
||||||
|
)
|
||||||
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldResizeMedia(media: QueuedMedia): Boolean {
|
private fun shouldResizeMedia(media: QueuedMedia): Boolean {
|
||||||
return media.type == QueuedMedia.Type.IMAGE
|
return media.type == QueuedMedia.Type.IMAGE &&
|
||||||
&& (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT
|
(media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
||||||
|| getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
@ -211,6 +217,5 @@ class MediaUploaderImpl(
|
||||||
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
|
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
|
||||||
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
|
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
|
||||||
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
|
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,11 +82,13 @@ fun showAddPollDialog(
|
||||||
val pollDuration = context.resources
|
val pollDuration = context.resources
|
||||||
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
||||||
|
|
||||||
onUpdatePoll(NewPoll(
|
onUpdatePoll(
|
||||||
|
NewPoll(
|
||||||
options = adapter.pollOptions,
|
options = adapter.pollOptions,
|
||||||
expiresIn = pollDuration,
|
expiresIn = pollDuration,
|
||||||
multiple = binding.multipleChoicesCheckBox.isChecked
|
multiple = binding.multipleChoicesCheckBox.isChecked
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ class AddPollOptionsAdapter(
|
||||||
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
|
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
|
||||||
|
|
||||||
binding.optionEditText.onTextChanged { s, _, _, _ ->
|
binding.optionEditText.onTextChanged { s, _, _, _ ->
|
||||||
val pos = holder.adapterPosition
|
val pos = holder.bindingAdapterPosition
|
||||||
if (pos != RecyclerView.NO_POSITION) {
|
if (pos != RecyclerView.NO_POSITION) {
|
||||||
options[pos] = s.toString()
|
options[pos] = s.toString()
|
||||||
onOptionChanged(validateInput())
|
onOptionChanged(validateInput())
|
||||||
|
@ -68,8 +68,8 @@ class AddPollOptionsAdapter(
|
||||||
|
|
||||||
holder.binding.deleteButton.setOnClickListener {
|
holder.binding.deleteButton.setOnClickListener {
|
||||||
holder.binding.optionEditText.clearFocus()
|
holder.binding.optionEditText.clearFocus()
|
||||||
options.removeAt(holder.adapterPosition)
|
options.removeAt(holder.bindingAdapterPosition)
|
||||||
notifyItemRemoved(holder.adapterPosition)
|
notifyItemRemoved(holder.bindingAdapterPosition)
|
||||||
onOptionRemoved(validateInput())
|
onOptionRemoved(validateInput())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
@ -41,7 +40,8 @@ import com.keylesspalace.tusky.util.withLifecycleContext
|
||||||
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
|
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
|
||||||
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
||||||
|
|
||||||
fun <T> T.makeCaptionDialog(existingDescription: String?,
|
fun <T> T.makeCaptionDialog(
|
||||||
|
existingDescription: String?,
|
||||||
previewUri: Uri,
|
previewUri: Uri,
|
||||||
onUpdateDescription: (String) -> LiveData<Boolean>
|
onUpdateDescription: (String) -> LiveData<Boolean>
|
||||||
) where T : Activity, T : LifecycleOwner {
|
) where T : Activity, T : LifecycleOwner {
|
||||||
|
@ -54,9 +54,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||||
maximumScale = 6f
|
maximumScale = 6f
|
||||||
}
|
}
|
||||||
|
|
||||||
val displayMetrics = DisplayMetrics()
|
|
||||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
|
||||||
|
|
||||||
val margin = Utils.dpToPx(this, 4)
|
val margin = Utils.dpToPx(this, 4)
|
||||||
dialogLayout.addView(imageView)
|
dialogLayout.addView(imageView)
|
||||||
(imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f
|
(imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f
|
||||||
|
@ -64,14 +61,18 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||||
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
|
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
|
||||||
|
|
||||||
val input = EditText(this)
|
val input = EditText(this)
|
||||||
input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired,
|
input.hint = resources.getQuantityString(
|
||||||
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT)
|
R.plurals.hint_describe_for_visually_impaired,
|
||||||
|
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT
|
||||||
|
)
|
||||||
dialogLayout.addView(input)
|
dialogLayout.addView(input)
|
||||||
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
|
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
|
||||||
input.setLines(2)
|
input.setLines(2)
|
||||||
input.inputType = (InputType.TYPE_CLASS_TEXT
|
input.inputType = (
|
||||||
|
InputType.TYPE_CLASS_TEXT
|
||||||
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||||
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
|
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||||
|
)
|
||||||
input.setText(existingDescription)
|
input.setText(existingDescription)
|
||||||
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
||||||
|
|
||||||
|
@ -80,7 +81,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||||
withLifecycleContext {
|
withLifecycleContext {
|
||||||
onUpdateDescription(input.text.toString())
|
onUpdateDescription(input.text.toString())
|
||||||
.observe { success -> if (!success) showFailedCaptionMessage() }
|
.observe { success -> if (!success) showFailedCaptionMessage() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
@ -94,7 +94,8 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||||
|
|
||||||
val window = dialog.window
|
val window = dialog.window
|
||||||
window?.setSoftInputMode(
|
window?.setSoftInputMode(
|
||||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||||
|
)
|
||||||
|
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
|
@ -113,7 +114,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun Activity.showFailedCaptionMessage() {
|
private fun Activity.showFailedCaptionMessage() {
|
||||||
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,12 +57,10 @@ class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: Attr
|
||||||
R.id.directRadioButton
|
R.id.directRadioButton
|
||||||
else ->
|
else ->
|
||||||
R.id.directRadioButton
|
R.id.directRadioButton
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check(selectedButton)
|
check(selectedButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComposeOptionsListener {
|
interface ComposeOptionsListener {
|
||||||
|
|
|
@ -16,19 +16,21 @@
|
||||||
package com.keylesspalace.tusky.components.compose.view
|
package com.keylesspalace.tusky.components.compose.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.emoji.widget.EmojiEditTextHelper
|
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
|
||||||
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.text.method.KeyListener
|
import android.text.method.KeyListener
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputConnection
|
import android.view.inputmethod.InputConnection
|
||||||
|
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
||||||
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
|
import androidx.emoji.widget.EmojiEditTextHelper
|
||||||
|
|
||||||
class EditTextTyped @JvmOverloads constructor(context: Context,
|
class EditTextTyped @JvmOverloads constructor(
|
||||||
attributeSet: AttributeSet? = null)
|
context: Context,
|
||||||
: AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
attributeSet: AttributeSet? = null
|
||||||
|
) :
|
||||||
|
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||||
|
|
||||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
||||||
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
||||||
|
@ -52,8 +54,13 @@ class EditTextTyped @JvmOverloads constructor(context: Context,
|
||||||
val connection = super.onCreateInputConnection(editorInfo)
|
val connection = super.onCreateInputConnection(editorInfo)
|
||||||
return if (onCommitContentListener != null) {
|
return if (onCommitContentListener != null) {
|
||||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||||
getEmojiEditTextHelper().onCreateInputConnection(InputConnectionCompat.createWrapper(connection, editorInfo,
|
getEmojiEditTextHelper().onCreateInputConnection(
|
||||||
onCommitContentListener!!), editorInfo)!!
|
InputConnectionCompat.createWrapper(
|
||||||
|
connection, editorInfo,
|
||||||
|
onCommitContentListener!!
|
||||||
|
),
|
||||||
|
editorInfo
|
||||||
|
)!!
|
||||||
} else {
|
} else {
|
||||||
connection
|
connection
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,9 @@ import com.keylesspalace.tusky.entity.NewPoll
|
||||||
class PollPreviewView @JvmOverloads constructor(
|
class PollPreviewView @JvmOverloads constructor(
|
||||||
context: Context?,
|
context: Context?,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0)
|
defStyleAttr: Int = 0
|
||||||
: LinearLayout(context, attrs, defStyleAttr) {
|
) :
|
||||||
|
LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private val adapter = PreviewPollOptionsAdapter()
|
private val adapter = PreviewPollOptionsAdapter()
|
||||||
|
|
||||||
|
|
|
@ -68,8 +68,5 @@ class TootButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,114 +17,39 @@ package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.paging.AsyncPagedListDiffer
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.paging.PagedList
|
|
||||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
import com.keylesspalace.tusky.util.NetworkState
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
|
|
||||||
class ConversationAdapter(
|
class ConversationAdapter(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val listener: StatusActionListener,
|
private val listener: StatusActionListener
|
||||||
private val topLoadedCallback: () -> Unit,
|
) : PagingDataAdapter<ConversationEntity, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
||||||
private val retryCallback: () -> Unit
|
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|
||||||
|
|
||||||
private var networkState: NetworkState? = null
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
||||||
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object : ListUpdateCallback {
|
return ConversationViewHolder(view, statusDisplayOptions, listener)
|
||||||
override fun onInserted(position: Int, count: Int) {
|
|
||||||
notifyItemRangeInserted(position, count)
|
|
||||||
if (position == 0) {
|
|
||||||
topLoadedCallback()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRemoved(position: Int, count: Int) {
|
override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) {
|
||||||
notifyItemRangeRemoved(position, count)
|
holder.setupWithConversation(getItem(position))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
fun item(position: Int): ConversationEntity? {
|
||||||
notifyItemMoved(fromPosition, toPosition)
|
return getItem(position)
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
|
||||||
notifyItemRangeChanged(position, count, payload)
|
|
||||||
}
|
|
||||||
}, AsyncDifferConfig.Builder(CONVERSATION_COMPARATOR).build())
|
|
||||||
|
|
||||||
fun submitList(list: PagedList<ConversationEntity>) {
|
|
||||||
differ.submitList(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
return when (viewType) {
|
|
||||||
R.layout.item_network_state -> {
|
|
||||||
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
NetworkStateViewHolder(binding, retryCallback)
|
|
||||||
}
|
|
||||||
R.layout.item_conversation -> {
|
|
||||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
|
||||||
ConversationViewHolder(view, statusDisplayOptions, listener)
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException("unknown view type $viewType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
when (getItemViewType(position)) {
|
|
||||||
R.layout.item_network_state -> (holder as NetworkStateViewHolder).setUpWithNetworkState(networkState, differ.itemCount == 0)
|
|
||||||
R.layout.item_conversation -> (holder as ConversationViewHolder).setupWithConversation(differ.getItem(position))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return if (hasExtraRow() && position == itemCount - 1) {
|
|
||||||
R.layout.item_network_state
|
|
||||||
} else {
|
|
||||||
R.layout.item_conversation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return differ.itemCount + if (hasExtraRow()) 1 else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNetworkState(newNetworkState: NetworkState?) {
|
|
||||||
val previousState = this.networkState
|
|
||||||
val hadExtraRow = hasExtraRow()
|
|
||||||
this.networkState = newNetworkState
|
|
||||||
val hasExtraRow = hasExtraRow()
|
|
||||||
if (hadExtraRow != hasExtraRow) {
|
|
||||||
if (hadExtraRow) {
|
|
||||||
notifyItemRemoved(differ.itemCount)
|
|
||||||
} else {
|
|
||||||
notifyItemInserted(differ.itemCount)
|
|
||||||
}
|
|
||||||
} else if (hasExtraRow && previousState != newNetworkState) {
|
|
||||||
notifyItemChanged(itemCount - 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationEntity>() {
|
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationEntity>() {
|
||||||
override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean =
|
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
||||||
oldItem == newItem
|
return oldItem.id == newItem.id
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean =
|
|
||||||
oldItem.id == newItem.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright 2019 Conny Duck
|
/* Copyright 2021 Tusky Contributors
|
||||||
*
|
*
|
||||||
* This file is a part of Tusky.
|
* This file is a part of Tusky.
|
||||||
*
|
*
|
||||||
|
@ -21,9 +21,14 @@ import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.keylesspalace.tusky.db.Converters
|
import com.keylesspalace.tusky.db.Converters
|
||||||
import com.keylesspalace.tusky.entity.*
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.entity.Conversation
|
||||||
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
@Entity(primaryKeys = ["id", "accountId"])
|
@Entity(primaryKeys = ["id", "accountId"])
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
@ -73,13 +78,13 @@ data class ConversationStatusEntity(
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
val spoilerText: String,
|
val spoilerText: String,
|
||||||
val attachments: ArrayList<Attachment>,
|
val attachments: ArrayList<Attachment>,
|
||||||
val mentions: Array<Status.Mention>,
|
val mentions: List<Status.Mention>,
|
||||||
val showingHiddenContent: Boolean,
|
val showingHiddenContent: Boolean,
|
||||||
val expanded: Boolean,
|
val expanded: Boolean,
|
||||||
val collapsible: Boolean,
|
val collapsible: Boolean,
|
||||||
val collapsed: Boolean,
|
val collapsed: Boolean,
|
||||||
|
val muted: Boolean,
|
||||||
val poll: Poll?
|
val poll: Poll?
|
||||||
|
|
||||||
) {
|
) {
|
||||||
/** its necessary to override this because Spanned.equals does not work as expected */
|
/** its necessary to override this because Spanned.equals does not work as expected */
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
@ -101,11 +106,12 @@ data class ConversationStatusEntity(
|
||||||
if (sensitive != other.sensitive) return false
|
if (sensitive != other.sensitive) return false
|
||||||
if (spoilerText != other.spoilerText) return false
|
if (spoilerText != other.spoilerText) return false
|
||||||
if (attachments != other.attachments) return false
|
if (attachments != other.attachments) return false
|
||||||
if (!mentions.contentEquals(other.mentions)) return false
|
if (mentions != other.mentions) return false
|
||||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
if (showingHiddenContent != other.showingHiddenContent) return false
|
||||||
if (expanded != other.expanded) return false
|
if (expanded != other.expanded) return false
|
||||||
if (collapsible != other.collapsible) return false
|
if (collapsible != other.collapsible) return false
|
||||||
if (collapsed != other.collapsed) return false
|
if (collapsed != other.collapsed) return false
|
||||||
|
if (muted != other.muted) return false
|
||||||
if (poll != other.poll) return false
|
if (poll != other.poll) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -125,11 +131,12 @@ data class ConversationStatusEntity(
|
||||||
result = 31 * result + sensitive.hashCode()
|
result = 31 * result + sensitive.hashCode()
|
||||||
result = 31 * result + spoilerText.hashCode()
|
result = 31 * result + spoilerText.hashCode()
|
||||||
result = 31 * result + attachments.hashCode()
|
result = 31 * result + attachments.hashCode()
|
||||||
result = 31 * result + mentions.contentHashCode()
|
result = 31 * result + mentions.hashCode()
|
||||||
result = 31 * result + showingHiddenContent.hashCode()
|
result = 31 * result + showingHiddenContent.hashCode()
|
||||||
result = 31 * result + expanded.hashCode()
|
result = 31 * result + expanded.hashCode()
|
||||||
result = 31 * result + collapsible.hashCode()
|
result = 31 * result + collapsible.hashCode()
|
||||||
result = 31 * result + collapsed.hashCode()
|
result = 31 * result + collapsed.hashCode()
|
||||||
|
result = 31 * result + muted.hashCode()
|
||||||
result = 31 * result + poll.hashCode()
|
result = 31 * result + poll.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -157,39 +164,52 @@ data class ConversationStatusEntity(
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
application = null,
|
application = null,
|
||||||
pinned = false,
|
pinned = false,
|
||||||
muted = false,
|
muted = muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = null)
|
card = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Account.toEntity() =
|
fun Account.toEntity() =
|
||||||
ConversationAccountEntity(
|
ConversationAccountEntity(
|
||||||
id,
|
id = id,
|
||||||
username,
|
username = username,
|
||||||
name,
|
displayName = name,
|
||||||
avatar,
|
avatar = avatar,
|
||||||
emojis ?: emptyList()
|
emojis = emojis ?: emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Status.toEntity() =
|
fun Status.toEntity() =
|
||||||
ConversationStatusEntity(
|
ConversationStatusEntity(
|
||||||
id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content,
|
id = id,
|
||||||
createdAt, emojis, favouritesCount, favourited, bookmarked, sensitive,
|
url = url,
|
||||||
spoilerText, attachments, mentions,
|
inReplyToId = inReplyToId,
|
||||||
false,
|
inReplyToAccountId = inReplyToAccountId,
|
||||||
false,
|
account = account.toEntity(),
|
||||||
shouldTrimStatus(content),
|
content = content,
|
||||||
true,
|
createdAt = createdAt,
|
||||||
poll
|
emojis = emojis,
|
||||||
|
favouritesCount = favouritesCount,
|
||||||
|
favourited = favourited,
|
||||||
|
bookmarked = bookmarked,
|
||||||
|
sensitive = sensitive,
|
||||||
|
spoilerText = spoilerText,
|
||||||
|
attachments = attachments,
|
||||||
|
mentions = mentions,
|
||||||
|
showingHiddenContent = false,
|
||||||
|
expanded = false,
|
||||||
|
collapsible = shouldTrimStatus(content),
|
||||||
|
collapsed = true,
|
||||||
|
muted = muted ?: false,
|
||||||
|
poll = poll
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
fun Conversation.toEntity(accountId: Long) =
|
fun Conversation.toEntity(accountId: Long) =
|
||||||
ConversationEntity(
|
ConversationEntity(
|
||||||
accountId,
|
accountId = accountId,
|
||||||
id,
|
id = id,
|
||||||
accounts.map { it.toEntity() },
|
accounts = accounts.map { it.toEntity() },
|
||||||
unread,
|
unread = unread,
|
||||||
lastStatus!!.toEntity()
|
lastStatus = lastStatus!!.toEntity()
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/* Copyright 2021 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.components.conversation
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.LoadStateAdapter
|
||||||
|
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
|
||||||
|
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
||||||
|
|
||||||
|
class ConversationLoadStateAdapter(
|
||||||
|
private val retryCallback: () -> Unit
|
||||||
|
) : LoadStateAdapter<NetworkStateViewHolder>() {
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: NetworkStateViewHolder, loadState: LoadState) {
|
||||||
|
holder.setUpWithNetworkState(loadState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
loadState: LoadState
|
||||||
|
): NetworkStateViewHolder {
|
||||||
|
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return NetworkStateViewHolder(binding, retryCallback)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,98 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* 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.components.conversation
|
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import com.keylesspalace.tusky.entity.Conversation
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
|
||||||
import com.keylesspalace.tusky.util.PagingRequestHelper
|
|
||||||
import com.keylesspalace.tusky.util.createStatusLiveData
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This boundary callback gets notified when user reaches to the edges of the list such that the
|
|
||||||
* database cannot provide any more data.
|
|
||||||
* <p>
|
|
||||||
* The boundary callback might be called multiple times for the same direction so it does its own
|
|
||||||
* rate limiting using the PagingRequestHelper class.
|
|
||||||
*/
|
|
||||||
class ConversationsBoundaryCallback(
|
|
||||||
private val accountId: Long,
|
|
||||||
private val mastodonApi: MastodonApi,
|
|
||||||
private val handleResponse: (Long, List<Conversation>?) -> Unit,
|
|
||||||
private val ioExecutor: Executor,
|
|
||||||
private val networkPageSize: Int)
|
|
||||||
: PagedList.BoundaryCallback<ConversationEntity>() {
|
|
||||||
|
|
||||||
val helper = PagingRequestHelper(ioExecutor)
|
|
||||||
val networkState = helper.createStatusLiveData()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database returned 0 items. We should query the backend for more items.
|
|
||||||
*/
|
|
||||||
@MainThread
|
|
||||||
override fun onZeroItemsLoaded() {
|
|
||||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) {
|
|
||||||
mastodonApi.getConversations(null, networkPageSize)
|
|
||||||
.enqueue(createWebserviceCallback(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User reached to the end of the list.
|
|
||||||
*/
|
|
||||||
@MainThread
|
|
||||||
override fun onItemAtEndLoaded(itemAtEnd: ConversationEntity) {
|
|
||||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
|
|
||||||
mastodonApi.getConversations(itemAtEnd.lastStatus.id, networkPageSize)
|
|
||||||
.enqueue(createWebserviceCallback(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* every time it gets new items, boundary callback simply inserts them into the database and
|
|
||||||
* paging library takes care of refreshing the list if necessary.
|
|
||||||
*/
|
|
||||||
private fun insertItemsIntoDb(
|
|
||||||
response: Response<List<Conversation>>,
|
|
||||||
it: PagingRequestHelper.Request.Callback) {
|
|
||||||
ioExecutor.execute {
|
|
||||||
handleResponse(accountId, response.body())
|
|
||||||
it.recordSuccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemAtFrontLoaded(itemAtFront: ConversationEntity) {
|
|
||||||
// ignored, since we only ever append to what's in the DB
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback): Callback<List<Conversation>> {
|
|
||||||
return object : Callback<List<Conversation>> {
|
|
||||||
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
|
|
||||||
it.recordFailure(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<List<Conversation>>, response: Response<List<Conversation>>) {
|
|
||||||
insertItemsIntoDb(response, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright 2019 Conny Duck
|
/* Copyright 2021 Tusky Contributors
|
||||||
*
|
*
|
||||||
* This file is a part of Tusky.
|
* This file is a part of Tusky.
|
||||||
*
|
*
|
||||||
|
@ -20,14 +20,19 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.keylesspalace.tusky.AccountActivity
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.ViewTagActivity
|
import com.keylesspalace.tusky.ViewTagActivity
|
||||||
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
@ -36,12 +41,18 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.CardViewMode
|
import com.keylesspalace.tusky.util.CardViewMode
|
||||||
import com.keylesspalace.tusky.util.NetworkState
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
|
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -52,9 +63,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
||||||
|
|
||||||
private lateinit var adapter: ConversationAdapter
|
private lateinit var adapter: ConversationAdapter
|
||||||
|
private lateinit var loadStateAdapter: ConversationLoadStateAdapter
|
||||||
|
|
||||||
private var layoutManager: LinearLayoutManager? = null
|
private var layoutManager: LinearLayoutManager? = null
|
||||||
|
|
||||||
|
private var initialRefreshDone: Boolean = false
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
||||||
}
|
}
|
||||||
|
@ -70,16 +84,18 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
useBlurhash = preferences.getBoolean("useBlurhash", true),
|
useBlurhash = preferences.getBoolean("useBlurhash", true),
|
||||||
cardViewMode = CardViewMode.NONE,
|
cardViewMode = CardViewMode.NONE,
|
||||||
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
|
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
|
||||||
|
confirmFavourites = preferences.getBoolean("confirmFavourites", false),
|
||||||
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
||||||
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)
|
adapter = ConversationAdapter(statusDisplayOptions, this)
|
||||||
|
loadStateAdapter = ConversationLoadStateAdapter(adapter::retry)
|
||||||
|
|
||||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||||
layoutManager = LinearLayoutManager(view.context)
|
layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter.withLoadStateFooter(loadStateAdapter)
|
||||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
|
||||||
binding.progressBar.hide()
|
binding.progressBar.hide()
|
||||||
|
@ -87,58 +103,101 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
|
|
||||||
initSwipeToRefresh()
|
initSwipeToRefresh()
|
||||||
|
|
||||||
viewModel.conversations.observe(viewLifecycleOwner) {
|
lifecycleScope.launch {
|
||||||
adapter.submitList(it)
|
viewModel.conversationFlow.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
}
|
}
|
||||||
viewModel.networkState.observe(viewLifecycleOwner) {
|
|
||||||
adapter.setNetworkState(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.load()
|
adapter.addLoadStateListener { loadStates ->
|
||||||
|
|
||||||
|
loadStates.refresh.let { refreshState ->
|
||||||
|
if (refreshState is LoadState.Error) {
|
||||||
|
binding.statusView.show()
|
||||||
|
if (refreshState.error is IOException) {
|
||||||
|
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
||||||
|
adapter.refresh()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||||
|
adapter.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.statusView.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.progressBar.visible(refreshState == LoadState.Loading && adapter.itemCount == 0)
|
||||||
|
|
||||||
|
if (refreshState is LoadState.NotLoading && !initialRefreshDone) {
|
||||||
|
// jump to top after the initial refresh finished
|
||||||
|
binding.recyclerView.scrollToPosition(0)
|
||||||
|
initialRefreshDone = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshState != LoadState.Loading) {
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSwipeToRefresh() {
|
private fun initSwipeToRefresh() {
|
||||||
viewModel.refreshState.observe(viewLifecycleOwner) {
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
|
|
||||||
}
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||||
viewModel.refresh()
|
adapter.refresh()
|
||||||
}
|
}
|
||||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTopLoaded() {
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReblog(reblog: Boolean, position: Int) {
|
override fun onReblog(reblog: Boolean, position: Int) {
|
||||||
// its impossible to reblog private messages
|
// its impossible to reblog private messages
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||||
viewModel.favourite(favourite, position)
|
adapter.item(position)?.let { conversation ->
|
||||||
|
viewModel.favourite(favourite, conversation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBookmark(favourite: Boolean, position: Int) {
|
override fun onBookmark(favourite: Boolean, position: Int) {
|
||||||
viewModel.bookmark(favourite, position)
|
adapter.item(position)?.let { conversation ->
|
||||||
|
viewModel.bookmark(favourite, conversation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMore(view: View, position: Int) {
|
override fun onMore(view: View, position: Int) {
|
||||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
adapter.item(position)?.let { conversation ->
|
||||||
more(it.toStatus(), view, position)
|
|
||||||
|
val popup = PopupMenu(requireContext(), view)
|
||||||
|
popup.inflate(R.menu.conversation_more)
|
||||||
|
|
||||||
|
if (conversation.lastStatus.muted) {
|
||||||
|
popup.menu.removeItem(R.id.status_mute_conversation)
|
||||||
|
} else {
|
||||||
|
popup.menu.removeItem(R.id.status_unmute_conversation)
|
||||||
|
}
|
||||||
|
|
||||||
|
popup.setOnMenuItemClickListener { item ->
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.status_mute_conversation -> viewModel.muteConversation(conversation)
|
||||||
|
R.id.status_unmute_conversation -> viewModel.muteConversation(conversation)
|
||||||
|
R.id.conversation_delete -> deleteConversation(conversation)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
popup.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
adapter.item(position)?.let { conversation ->
|
||||||
viewMedia(attachmentIndex, it.toStatus(), view)
|
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewThread(position: Int) {
|
override fun onViewThread(position: Int) {
|
||||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
adapter.item(position)?.let { conversation ->
|
||||||
viewThread(it.toStatus())
|
viewThread(conversation.lastStatus.id, conversation.lastStatus.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,11 +206,15 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||||
viewModel.expandHiddenStatus(expanded, position)
|
adapter.item(position)?.let { conversation ->
|
||||||
|
viewModel.expandHiddenStatus(expanded, conversation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||||
viewModel.showContent(isShowing, position)
|
adapter.item(position)?.let { conversation ->
|
||||||
|
viewModel.showContent(isShowing, conversation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadMore(position: Int) {
|
override fun onLoadMore(position: Int) {
|
||||||
|
@ -159,7 +222,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||||
viewModel.collapseLongStatus(isCollapsed, position)
|
adapter.item(position)?.let { conversation ->
|
||||||
|
viewModel.collapseLongStatus(isCollapsed, conversation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
override fun onViewAccount(id: String) {
|
||||||
|
@ -174,15 +239,25 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeItem(position: Int) {
|
override fun removeItem(position: Int) {
|
||||||
viewModel.remove(position)
|
// not needed
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReply(position: Int) {
|
override fun onReply(position: Int) {
|
||||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
adapter.item(position)?.let { conversation ->
|
||||||
reply(it.toStatus())
|
reply(conversation.lastStatus.toStatus())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun deleteConversation(conversation: ConversationEntity) {
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setMessage(R.string.dialog_delete_conversation_warning)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
viewModel.remove(conversation)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun jumpToTop() {
|
private fun jumpToTop() {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
layoutManager?.scrollToPosition(0)
|
layoutManager?.scrollToPosition(0)
|
||||||
|
@ -195,7 +270,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
viewModel.voteInPoll(position, choices)
|
adapter.item(position)?.let { conversation ->
|
||||||
|
viewModel.voteInPoll(choices, conversation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.LoadType
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.paging.RemoteMediator
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
class ConversationsRemoteMediator(
|
||||||
|
private val accountId: Long,
|
||||||
|
private val api: MastodonApi,
|
||||||
|
private val db: AppDatabase
|
||||||
|
) : RemoteMediator<Int, ConversationEntity>() {
|
||||||
|
|
||||||
|
override suspend fun load(
|
||||||
|
loadType: LoadType,
|
||||||
|
state: PagingState<Int, ConversationEntity>
|
||||||
|
): MediatorResult {
|
||||||
|
|
||||||
|
try {
|
||||||
|
val conversationsResult = when (loadType) {
|
||||||
|
LoadType.REFRESH -> {
|
||||||
|
api.getConversations(limit = state.config.initialLoadSize)
|
||||||
|
}
|
||||||
|
LoadType.PREPEND -> {
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
}
|
||||||
|
LoadType.APPEND -> {
|
||||||
|
val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.lastStatus?.id
|
||||||
|
api.getConversations(maxId = maxId, limit = state.config.pageSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadType == LoadType.REFRESH) {
|
||||||
|
db.conversationDao().deleteForAccount(accountId)
|
||||||
|
}
|
||||||
|
db.conversationDao().insert(
|
||||||
|
conversationsResult
|
||||||
|
.filterNot { it.lastStatus == null }
|
||||||
|
.map { it.toEntity(accountId) }
|
||||||
|
)
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = conversationsResult.isEmpty())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return MediatorResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun initialize() = InitializeAction.LAUNCH_INITIAL_REFRESH
|
||||||
|
}
|
|
@ -1,99 +1,32 @@
|
||||||
|
/* Copyright 2021 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.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import androidx.paging.Config
|
|
||||||
import androidx.paging.toLiveData
|
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.entity.Conversation
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.Listing
|
import io.reactivex.rxjava3.core.Single
|
||||||
import com.keylesspalace.tusky.util.NetworkState
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import io.reactivex.Single
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, val db: AppDatabase) {
|
class ConversationsRepository @Inject constructor(
|
||||||
|
val mastodonApi: MastodonApi,
|
||||||
private val ioExecutor = Executors.newSingleThreadExecutor()
|
val db: AppDatabase
|
||||||
|
) {
|
||||||
companion object {
|
|
||||||
private const val DEFAULT_PAGE_SIZE = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun refresh(accountId: Long, showLoadingIndicator: Boolean): LiveData<NetworkState> {
|
|
||||||
val networkState = MutableLiveData<NetworkState>()
|
|
||||||
if(showLoadingIndicator) {
|
|
||||||
networkState.value = NetworkState.LOADING
|
|
||||||
}
|
|
||||||
|
|
||||||
mastodonApi.getConversations(limit = DEFAULT_PAGE_SIZE).enqueue(
|
|
||||||
object : Callback<List<Conversation>> {
|
|
||||||
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
|
|
||||||
// retrofit calls this on main thread so safe to call set value
|
|
||||||
networkState.value = NetworkState.error(t.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<List<Conversation>>, response: Response<List<Conversation>>) {
|
|
||||||
ioExecutor.execute {
|
|
||||||
db.runInTransaction {
|
|
||||||
db.conversationDao().deleteForAccount(accountId)
|
|
||||||
insertResultIntoDb(accountId, response.body())
|
|
||||||
}
|
|
||||||
// since we are in bg thread now, post the result.
|
|
||||||
networkState.postValue(NetworkState.LOADED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return networkState
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun conversations(accountId: Long): Listing<ConversationEntity> {
|
|
||||||
// create a boundary callback which will observe when the user reaches to the edges of
|
|
||||||
// the list and update the database with extra data.
|
|
||||||
val boundaryCallback = ConversationsBoundaryCallback(
|
|
||||||
accountId = accountId,
|
|
||||||
mastodonApi = mastodonApi,
|
|
||||||
handleResponse = this::insertResultIntoDb,
|
|
||||||
ioExecutor = ioExecutor,
|
|
||||||
networkPageSize = DEFAULT_PAGE_SIZE)
|
|
||||||
// we are using a mutable live data to trigger refresh requests which eventually calls
|
|
||||||
// refresh method and gets a new live data. Each refresh request by the user becomes a newly
|
|
||||||
// dispatched data in refreshTrigger
|
|
||||||
val refreshTrigger = MutableLiveData<Unit>()
|
|
||||||
val refreshState = Transformations.switchMap(refreshTrigger) {
|
|
||||||
refresh(accountId, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use toLiveData Kotlin extension function here, you could also use LivePagedListBuilder
|
|
||||||
val livePagedList = db.conversationDao().conversationsForAccount(accountId).toLiveData(
|
|
||||||
config = Config(pageSize = DEFAULT_PAGE_SIZE, prefetchDistance = DEFAULT_PAGE_SIZE / 2, enablePlaceholders = false),
|
|
||||||
boundaryCallback = boundaryCallback
|
|
||||||
)
|
|
||||||
|
|
||||||
return Listing(
|
|
||||||
pagedList = livePagedList,
|
|
||||||
networkState = boundaryCallback.networkState,
|
|
||||||
retry = {
|
|
||||||
boundaryCallback.helper.retryAllFailed()
|
|
||||||
},
|
|
||||||
refresh = {
|
|
||||||
refreshTrigger.value = null
|
|
||||||
},
|
|
||||||
refreshState = refreshState
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteCacheForAccount(accountId: Long) {
|
fun deleteCacheForAccount(accountId: Long) {
|
||||||
Single.fromCallable {
|
Single.fromCallable {
|
||||||
|
@ -101,11 +34,4 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi,
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertResultIntoDb(accountId: Long, result: List<Conversation>?) {
|
|
||||||
result?.filter { it.lastStatus != null }
|
|
||||||
?.map{ it.toEntity(accountId) }
|
|
||||||
?.let { db.conversationDao().insert(it) }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,107 +1,100 @@
|
||||||
|
/* Copyright 2021 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.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.Listing
|
|
||||||
import com.keylesspalace.tusky.util.NetworkState
|
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
import io.reactivex.schedulers.Schedulers
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.rx3.await
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ConversationsViewModel @Inject constructor(
|
class ConversationsViewModel @Inject constructor(
|
||||||
private val repository: ConversationsRepository,
|
|
||||||
private val timelineCases: TimelineCases,
|
private val timelineCases: TimelineCases,
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val accountManager: AccountManager
|
private val accountManager: AccountManager,
|
||||||
|
private val api: MastodonApi
|
||||||
) : RxAwareViewModel() {
|
) : RxAwareViewModel() {
|
||||||
|
|
||||||
private val repoResult = MutableLiveData<Listing<ConversationEntity>>()
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
val conversationFlow = Pager(
|
||||||
|
config = PagingConfig(pageSize = 10, enablePlaceholders = false, initialLoadSize = 20),
|
||||||
|
remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database),
|
||||||
|
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
||||||
|
)
|
||||||
|
.flow
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
val conversations: LiveData<PagedList<ConversationEntity>> = Transformations.switchMap(repoResult) { it.pagedList }
|
fun favourite(favourite: Boolean, conversation: ConversationEntity) {
|
||||||
val networkState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkState }
|
viewModelScope.launch {
|
||||||
val refreshState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
|
try {
|
||||||
|
timelineCases.favourite(conversation.lastStatus.id, favourite).await()
|
||||||
|
|
||||||
fun load() {
|
|
||||||
val accountId = accountManager.activeAccount?.id ?: return
|
|
||||||
if (repoResult.value == null) {
|
|
||||||
repository.refresh(accountId, false)
|
|
||||||
}
|
|
||||||
repoResult.value = repository.conversations(accountId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
repoResult.value?.refresh?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retry() {
|
|
||||||
repoResult.value?.retry?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun favourite(favourite: Boolean, position: Int) {
|
|
||||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
|
||||||
timelineCases.favourite(conversation.lastStatus.toStatus(), favourite)
|
|
||||||
.flatMap {
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.copy(
|
||||||
lastStatus = conversation.lastStatus.copy(favourited = favourite)
|
lastStatus = conversation.lastStatus.copy(favourited = favourite)
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
database.conversationDao().insert(newConversation)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "failed to favourite status", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
|
|
||||||
.onErrorReturnItem(0)
|
|
||||||
.subscribe()
|
|
||||||
.autoDispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fun bookmark(bookmark: Boolean, conversation: ConversationEntity) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
timelineCases.bookmark(conversation.lastStatus.id, bookmark).await()
|
||||||
|
|
||||||
fun bookmark(bookmark: Boolean, position: Int) {
|
|
||||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
|
||||||
timelineCases.bookmark(conversation.lastStatus.toStatus(), bookmark)
|
|
||||||
.flatMap {
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.copy(
|
||||||
lastStatus = conversation.lastStatus.copy(bookmarked = bookmark)
|
lastStatus = conversation.lastStatus.copy(bookmarked = bookmark)
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
database.conversationDao().insert(newConversation)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "failed to bookmark status", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.doOnError { t -> Log.w("ConversationViewModel", "Failed to bookmark conversation", t) }
|
|
||||||
.onErrorReturnItem(0)
|
|
||||||
.subscribe()
|
|
||||||
.autoDispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fun voteInPoll(choices: List<Int>, conversation: ConversationEntity) {
|
||||||
|
viewModelScope.launch {
|
||||||
fun voteInPoll(position: Int, choices: MutableList<Int>) {
|
try {
|
||||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await()
|
||||||
timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices)
|
|
||||||
.flatMap { poll ->
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.copy(
|
||||||
lastStatus = conversation.lastStatus.copy(poll = poll)
|
lastStatus = conversation.lastStatus.copy(poll = poll)
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
database.conversationDao().insert(newConversation)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "failed to vote in poll", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
|
|
||||||
.onErrorReturnItem(0)
|
|
||||||
.subscribe()
|
|
||||||
.autoDispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) {
|
||||||
|
viewModelScope.launch {
|
||||||
fun expandHiddenStatus(expanded: Boolean, position: Int) {
|
|
||||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.copy(
|
||||||
lastStatus = conversation.lastStatus.copy(expanded = expanded)
|
lastStatus = conversation.lastStatus.copy(expanded = expanded)
|
||||||
)
|
)
|
||||||
|
@ -109,8 +102,8 @@ class ConversationsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun collapseLongStatus(collapsed: Boolean, position: Int) {
|
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) {
|
||||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.copy(
|
||||||
lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
|
lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
|
||||||
)
|
)
|
||||||
|
@ -118,8 +111,8 @@ class ConversationsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showContent(showing: Boolean, position: Int) {
|
fun showContent(showing: Boolean, conversation: ConversationEntity) {
|
||||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.copy(
|
||||||
lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
|
lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
|
||||||
)
|
)
|
||||||
|
@ -127,16 +120,42 @@ class ConversationsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(position: Int) {
|
fun remove(conversation: ConversationEntity) {
|
||||||
conversations.value?.getOrNull(position)?.let {
|
viewModelScope.launch {
|
||||||
refresh()
|
try {
|
||||||
|
api.deleteConversation(conversationId = conversation.id)
|
||||||
|
|
||||||
|
database.conversationDao().delete(conversation)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "failed to delete conversation", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveConversationToDb(conversation: ConversationEntity) {
|
fun muteConversation(conversation: ConversationEntity) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val newStatus = timelineCases.muteConversation(
|
||||||
|
conversation.lastStatus.id,
|
||||||
|
!conversation.lastStatus.muted
|
||||||
|
).await()
|
||||||
|
|
||||||
|
val newConversation = conversation.copy(
|
||||||
|
lastStatus = newStatus.toEntity()
|
||||||
|
)
|
||||||
|
|
||||||
|
database.conversationDao().insert(newConversation)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "failed to mute conversation", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveConversationToDb(conversation: ConversationEntity) {
|
||||||
database.conversationDao().insert(conversation)
|
database.conversationDao().insert(conversation)
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ConversationsViewModel"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,13 +28,12 @@ import com.keylesspalace.tusky.db.DraftEntity
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.util.IOUtils
|
import com.keylesspalace.tusky.util.IOUtils
|
||||||
import io.reactivex.Completable
|
import kotlinx.coroutines.Dispatchers
|
||||||
import io.reactivex.Observable
|
import kotlinx.coroutines.withContext
|
||||||
import io.reactivex.Single
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DraftHelper @Inject constructor(
|
class DraftHelper @Inject constructor(
|
||||||
|
@ -44,7 +43,7 @@ class DraftHelper @Inject constructor(
|
||||||
|
|
||||||
private val draftDao = db.draftDao()
|
private val draftDao = db.draftDao()
|
||||||
|
|
||||||
fun saveDraft(
|
suspend fun saveDraft(
|
||||||
draftId: Int,
|
draftId: Int,
|
||||||
accountId: Long,
|
accountId: Long,
|
||||||
inReplyToId: String?,
|
inReplyToId: String?,
|
||||||
|
@ -56,9 +55,7 @@ class DraftHelper @Inject constructor(
|
||||||
mediaDescriptions: List<String?>,
|
mediaDescriptions: List<String?>,
|
||||||
poll: NewPoll?,
|
poll: NewPoll?,
|
||||||
failedToSend: Boolean
|
failedToSend: Boolean
|
||||||
): Completable {
|
) = withContext(Dispatchers.IO) {
|
||||||
return Single.fromCallable {
|
|
||||||
|
|
||||||
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
||||||
|
|
||||||
if (externalFilesDir == null || !(externalFilesDir.exists())) {
|
if (externalFilesDir == null || !(externalFilesDir.exists())) {
|
||||||
|
@ -103,7 +100,7 @@ class DraftHelper @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DraftEntity(
|
val draft = DraftEntity(
|
||||||
id = draftId,
|
id = draftId,
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
|
@ -116,40 +113,34 @@ class DraftHelper @Inject constructor(
|
||||||
failedToSend = failedToSend
|
failedToSend = failedToSend
|
||||||
)
|
)
|
||||||
|
|
||||||
}.flatMapCompletable { draft ->
|
|
||||||
draftDao.insertOrReplace(draft)
|
draftDao.insertOrReplace(draft)
|
||||||
}.subscribeOn(Schedulers.io())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDraftAndAttachments(draftId: Int): Completable {
|
suspend fun deleteDraftAndAttachments(draftId: Int) {
|
||||||
return draftDao.find(draftId)
|
draftDao.find(draftId)?.let { draft ->
|
||||||
.flatMapCompletable { draft ->
|
|
||||||
deleteDraftAndAttachments(draft)
|
deleteDraftAndAttachments(draft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDraftAndAttachments(draft: DraftEntity): Completable {
|
suspend fun deleteDraftAndAttachments(draft: DraftEntity) {
|
||||||
return deleteAttachments(draft)
|
deleteAttachments(draft)
|
||||||
.andThen(draftDao.delete(draft.id))
|
draftDao.delete(draft.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
|
suspend fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
|
||||||
draftDao.loadDraftsSingle(accountId)
|
draftDao.loadDrafts(accountId).forEach { draft ->
|
||||||
.flatMapObservable { Observable.fromIterable(it) }
|
|
||||||
.flatMapCompletable { draft ->
|
|
||||||
deleteDraftAndAttachments(draft)
|
deleteDraftAndAttachments(draft)
|
||||||
}.subscribeOn(Schedulers.io())
|
}
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAttachments(draft: DraftEntity): Completable {
|
suspend fun deleteAttachments(draft: DraftEntity) {
|
||||||
return Completable.fromCallable {
|
withContext(Dispatchers.IO) {
|
||||||
draft.attachments.forEach { attachment ->
|
draft.attachments.forEach { attachment ->
|
||||||
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
|
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
|
||||||
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
|
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.subscribeOn(Schedulers.io())
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Uri.isNotInFolder(folder: File): Boolean {
|
private fun Uri.isNotInFolder(folder: File): Boolean {
|
||||||
|
@ -171,5 +162,4 @@ class DraftHelper @Inject constructor(
|
||||||
IOUtils.copyToFile(contentResolver, this, file)
|
IOUtils.copyToFile(contentResolver, this, file)
|
||||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -38,7 +38,6 @@ class DraftMediaAdapter(
|
||||||
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -60,8 +59,8 @@ class DraftMediaAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class DraftMediaViewHolder(val imageView: ImageView)
|
inner class DraftMediaViewHolder(val imageView: ImageView) :
|
||||||
: RecyclerView.ViewHolder(imageView) {
|
RecyclerView.ViewHolder(imageView) {
|
||||||
init {
|
init {
|
||||||
val thumbnailViewSize =
|
val thumbnailViewSize =
|
||||||
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||||
|
|
|
@ -19,28 +19,26 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
|
import autodispose2.autoDispose
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.SavedTootActivity
|
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
||||||
import com.keylesspalace.tusky.db.DraftEntity
|
import com.keylesspalace.tusky.db.DraftEntity
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.keylesspalace.tusky.util.show
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.launch
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -54,10 +52,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
private lateinit var binding: ActivityDraftsBinding
|
private lateinit var binding: ActivityDraftsBinding
|
||||||
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
||||||
|
|
||||||
private var oldDraftsButton: MenuItem? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
binding = ActivityDraftsBinding.inflate(layoutInflater)
|
binding = ActivityDraftsBinding.inflate(layoutInflater)
|
||||||
|
@ -70,7 +65,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status)
|
binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_drafts)
|
||||||
|
|
||||||
val adapter = DraftsAdapter(this)
|
val adapter = DraftsAdapter(this)
|
||||||
|
|
||||||
|
@ -80,44 +75,15 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
|
|
||||||
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
||||||
|
|
||||||
viewModel.drafts.observe(this) { draftList ->
|
lifecycleScope.launch {
|
||||||
if (draftList.isEmpty()) {
|
viewModel.drafts.collectLatest { draftData ->
|
||||||
binding.draftsRecyclerView.hide()
|
adapter.submitData(draftData)
|
||||||
binding.draftsErrorMessageView.show()
|
|
||||||
} else {
|
|
||||||
binding.draftsRecyclerView.show()
|
|
||||||
binding.draftsErrorMessageView.hide()
|
|
||||||
adapter.submitList(draftList)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
adapter.addLoadStateListener {
|
||||||
menuInflater.inflate(R.menu.drafts, menu)
|
binding.draftsErrorMessageView.visible(adapter.itemCount == 0)
|
||||||
oldDraftsButton = menu.findItem(R.id.action_old_drafts)
|
|
||||||
viewModel.showOldDraftsButton()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
|
||||||
.subscribe { showOldDraftsButton ->
|
|
||||||
oldDraftsButton?.isVisible = showOldDraftsButton
|
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
android.R.id.home -> {
|
|
||||||
onBackPressed()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.action_old_drafts -> {
|
|
||||||
val intent = Intent(this, SavedTootActivity::class.java)
|
|
||||||
startActivityWithSlideInAnimation(intent)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenDraft(draft: DraftEntity) {
|
override fun onOpenDraft(draft: DraftEntity) {
|
||||||
|
@ -126,8 +92,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
viewModel.getToot(draft.inReplyToId)
|
viewModel.getToot(draft.inReplyToId)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(this)
|
.autoDispose(from(this))
|
||||||
.subscribe({ status ->
|
.subscribe(
|
||||||
|
{ status ->
|
||||||
val composeOptions = ComposeActivity.ComposeOptions(
|
val composeOptions = ComposeActivity.ComposeOptions(
|
||||||
draftId = draft.id,
|
draftId = draft.id,
|
||||||
tootText = draft.content,
|
tootText = draft.content,
|
||||||
|
@ -144,8 +111,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
|
||||||
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
||||||
|
},
|
||||||
}, { throwable ->
|
{ throwable ->
|
||||||
|
|
||||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
|
||||||
|
@ -160,7 +127,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
openDraftWithoutReply(draft)
|
openDraftWithoutReply(draft)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.drafts
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.paging.PagedListAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -35,7 +35,7 @@ interface DraftActionListener {
|
||||||
|
|
||||||
class DraftsAdapter(
|
class DraftsAdapter(
|
||||||
private val listener: DraftActionListener
|
private val listener: DraftActionListener
|
||||||
) : PagedListAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
|
) : PagingDataAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
|
||||||
object : DiffUtil.ItemCallback<DraftEntity>() {
|
object : DiffUtil.ItemCallback<DraftEntity>() {
|
||||||
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
|
@ -87,6 +87,5 @@ class DraftsAdapter(
|
||||||
holder.binding.draftPoll.hide()
|
holder.binding.draftPoll.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,14 +16,17 @@
|
||||||
package com.keylesspalace.tusky.components.drafts
|
package com.keylesspalace.tusky.components.drafts
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.paging.toLiveData
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.DraftEntity
|
import com.keylesspalace.tusky.db.DraftEntity
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import io.reactivex.Observable
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.Single
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DraftsViewModel @Inject constructor(
|
class DraftsViewModel @Inject constructor(
|
||||||
|
@ -33,37 +36,39 @@ class DraftsViewModel @Inject constructor(
|
||||||
val draftHelper: DraftHelper
|
val draftHelper: DraftHelper
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20)
|
val drafts = Pager(
|
||||||
|
config = PagingConfig(pageSize = 20),
|
||||||
|
pagingSourceFactory = { database.draftDao().draftsPagingSource(accountManager.activeAccount?.id!!) }
|
||||||
|
).flow
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
private val deletedDrafts: MutableList<DraftEntity> = mutableListOf()
|
private val deletedDrafts: MutableList<DraftEntity> = mutableListOf()
|
||||||
|
|
||||||
fun showOldDraftsButton(): Observable<Boolean> {
|
|
||||||
return database.tootDao().savedTootCount()
|
|
||||||
.map { count -> count > 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteDraft(draft: DraftEntity) {
|
fun deleteDraft(draft: DraftEntity) {
|
||||||
// this does not immediately delete media files to avoid unnecessary file operations
|
// this does not immediately delete media files to avoid unnecessary file operations
|
||||||
// in case the user decides to restore the draft
|
// in case the user decides to restore the draft
|
||||||
|
viewModelScope.launch {
|
||||||
database.draftDao().delete(draft.id)
|
database.draftDao().delete(draft.id)
|
||||||
.subscribe()
|
|
||||||
deletedDrafts.add(draft)
|
deletedDrafts.add(draft)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun restoreDraft(draft: DraftEntity) {
|
fun restoreDraft(draft: DraftEntity) {
|
||||||
|
viewModelScope.launch {
|
||||||
database.draftDao().insertOrReplace(draft)
|
database.draftDao().insertOrReplace(draft)
|
||||||
.subscribe()
|
|
||||||
deletedDrafts.remove(draft)
|
deletedDrafts.remove(draft)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getToot(tootId: String): Single<Status> {
|
fun getToot(tootId: String): Single<Status> {
|
||||||
return api.status(tootId)
|
return api.status(tootId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
viewModelScope.launch {
|
||||||
deletedDrafts.forEach {
|
deletedDrafts.forEach {
|
||||||
draftHelper.deleteAttachments(it).subscribe()
|
draftHelper.deleteAttachments(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val binding = ActivityAccountListBinding.inflate(layoutInflater)
|
val binding = ActivityAccountListBinding.inflate(layoutInflater)
|
||||||
setContentView(R.layout.activity_account_list)
|
setContentView(binding.root)
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
supportActionBar?.apply {
|
supportActionBar?.apply {
|
||||||
|
@ -33,5 +33,4 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
override fun androidInjector() = androidInjector
|
||||||
|
|
||||||
}
|
}
|
|
@ -8,6 +8,8 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
|
import autodispose2.autoDispose
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
|
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
|
||||||
|
@ -20,9 +22,7 @@ import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
||||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import com.uber.autodispose.autoDispose
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
@ -114,7 +114,8 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
||||||
api.domainBlocks(id, bottomId)
|
api.domainBlocks(id, bottomId)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||||
.subscribe({ response ->
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
val instances = response.body()
|
val instances = response.body()
|
||||||
|
|
||||||
if (response.isSuccessful && instances != null) {
|
if (response.isSuccessful && instances != null) {
|
||||||
|
@ -122,9 +123,11 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
||||||
} else {
|
} else {
|
||||||
onFetchInstancesFailure(Exception(response.message()))
|
onFetchInstancesFailure(Exception(response.message()))
|
||||||
}
|
}
|
||||||
}, {throwable ->
|
},
|
||||||
|
{ throwable ->
|
||||||
onFetchInstancesFailure(throwable)
|
onFetchInstancesFailure(throwable)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
|
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.keylesspalace.tusky.components.notifications
|
package com.keylesspalace.tusky.components.notifications
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
@ -12,7 +13,7 @@ import javax.inject.Inject
|
||||||
class NotificationFetcher @Inject constructor(
|
class NotificationFetcher @Inject constructor(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val notifier: Notifier
|
private val context: Context
|
||||||
) {
|
) {
|
||||||
fun fetchAndShow() {
|
fun fetchAndShow() {
|
||||||
for (account in accountManager.getAllAccountsOrderedByActive()) {
|
for (account in accountManager.getAllAccountsOrderedByActive()) {
|
||||||
|
@ -20,7 +21,7 @@ class NotificationFetcher @Inject constructor(
|
||||||
try {
|
try {
|
||||||
val notifications = fetchNotifications(account)
|
val notifications = fetchNotifications(account)
|
||||||
notifications.forEachIndexed { index, notification ->
|
notifications.forEachIndexed { index, notification ->
|
||||||
notifier.show(notification, account, index == 0)
|
NotificationHelper.make(context, notification, account, index == 0)
|
||||||
}
|
}
|
||||||
accountManager.saveAccount(account)
|
accountManager.saveAccount(account)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -70,8 +70,8 @@ import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.reactivex.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
||||||
|
|
||||||
|
@ -316,7 +316,7 @@ public class NotificationHelper {
|
||||||
Status actionableStatus = status.getActionableStatus();
|
Status actionableStatus = status.getActionableStatus();
|
||||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||||
String contentWarning = actionableStatus.getSpoilerText();
|
String contentWarning = actionableStatus.getSpoilerText();
|
||||||
Status.Mention[] mentions = actionableStatus.getMentions();
|
List<Status.Mention> mentions = actionableStatus.getMentions();
|
||||||
List<String> mentionedUsernames = new ArrayList<>();
|
List<String> mentionedUsernames = new ArrayList<>();
|
||||||
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
||||||
for (Status.Mention mention : mentions) {
|
for (Status.Mention mention : mentions) {
|
||||||
|
@ -381,7 +381,6 @@ public class NotificationHelper {
|
||||||
|
|
||||||
NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName());
|
NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName());
|
||||||
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
notificationManager.createNotificationChannelGroup(channelGroup);
|
notificationManager.createNotificationChannelGroup(channelGroup);
|
||||||
|
|
||||||
for (int i = 0; i < channelIds.length; i++) {
|
for (int i = 0; i < channelIds.length; i++) {
|
||||||
|
@ -660,9 +659,12 @@ public class NotificationHelper {
|
||||||
StringBuilder builder = new StringBuilder(notification.getStatus().getContent());
|
StringBuilder builder = new StringBuilder(notification.getStatus().getContent());
|
||||||
builder.append('\n');
|
builder.append('\n');
|
||||||
Poll poll = notification.getStatus().getPoll();
|
Poll poll = notification.getStatus().getPoll();
|
||||||
for(PollOption option: poll.getOptions()) {
|
List<PollOption> options = poll.getOptions();
|
||||||
|
for(int i = 0; i < options.size(); ++i) {
|
||||||
|
PollOption option = options.get(i);
|
||||||
builder.append(buildDescription(option.getTitle(),
|
builder.append(buildDescription(option.getTitle(),
|
||||||
PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()),
|
PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()),
|
||||||
|
poll.getOwnVotes() != null && poll.getOwnVotes().contains(i),
|
||||||
context));
|
context));
|
||||||
builder.append('\n');
|
builder.append('\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
package com.keylesspalace.tusky.components.notifications
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
|
||||||
import com.keylesspalace.tusky.entity.Notification
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows notifications.
|
|
||||||
*/
|
|
||||||
interface Notifier {
|
|
||||||
fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SystemNotifier(
|
|
||||||
private val context: Context
|
|
||||||
) : Notifier {
|
|
||||||
override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) {
|
|
||||||
NotificationHelper.make(context, notification, account, isFirstInBatch)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,7 +22,11 @@ import android.util.Log
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.*
|
import com.keylesspalace.tusky.AccountListActivity
|
||||||
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
|
import com.keylesspalace.tusky.FiltersActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
|
@ -33,7 +37,12 @@ import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.settings.*
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
import com.keylesspalace.tusky.settings.listPreference
|
||||||
|
import com.keylesspalace.tusky.settings.makePreferenceScreen
|
||||||
|
import com.keylesspalace.tusky.settings.preference
|
||||||
|
import com.keylesspalace.tusky.settings.preferenceCategory
|
||||||
|
import com.keylesspalace.tusky.settings.switchPreference
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
|
@ -75,8 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = Intent(context, TabPreferenceActivity::class.java)
|
val intent = Intent(context, TabPreferenceActivity::class.java)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivity(intent)
|
||||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
activity?.overridePendingTransition(
|
||||||
R.anim.slide_to_left)
|
R.anim.slide_from_right,
|
||||||
|
R.anim.slide_to_left
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,8 +99,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
val intent = Intent(context, AccountListActivity::class.java)
|
val intent = Intent(context, AccountListActivity::class.java)
|
||||||
intent.putExtra("type", AccountListActivity.Type.MUTES)
|
intent.putExtra("type", AccountListActivity.Type.MUTES)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivity(intent)
|
||||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
activity?.overridePendingTransition(
|
||||||
R.anim.slide_to_left)
|
R.anim.slide_from_right,
|
||||||
|
R.anim.slide_to_left
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,8 +117,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
val intent = Intent(context, AccountListActivity::class.java)
|
val intent = Intent(context, AccountListActivity::class.java)
|
||||||
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
|
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivity(intent)
|
||||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
activity?.overridePendingTransition(
|
||||||
R.anim.slide_to_left)
|
R.anim.slide_from_right,
|
||||||
|
R.anim.slide_to_left
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,8 +131,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = Intent(context, InstanceListActivity::class.java)
|
val intent = Intent(context, InstanceListActivity::class.java)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivity(intent)
|
||||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
activity?.overridePendingTransition(
|
||||||
R.anim.slide_to_left)
|
R.anim.slide_from_right,
|
||||||
|
R.anim.slide_to_left
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,8 +218,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
preference {
|
preference {
|
||||||
setTitle(R.string.pref_title_public_filter_keywords)
|
setTitle(R.string.pref_title_public_filter_keywords)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
launchFilterActivity(Filter.PUBLIC,
|
launchFilterActivity(
|
||||||
R.string.pref_title_public_filter_keywords)
|
Filter.PUBLIC,
|
||||||
|
R.string.pref_title_public_filter_keywords
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,8 +245,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
preference {
|
preference {
|
||||||
setTitle(R.string.pref_title_thread_filter_keywords)
|
setTitle(R.string.pref_title_thread_filter_keywords)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
launchFilterActivity(Filter.THREAD,
|
launchFilterActivity(
|
||||||
R.string.pref_title_thread_filter_keywords)
|
Filter.THREAD,
|
||||||
|
R.string.pref_title_thread_filter_keywords
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,7 +276,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
it.startActivity(intent)
|
it.startActivity(intent)
|
||||||
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,7 +309,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
Log.e("AccountPreferences", "failed updating settings on server", t)
|
Log.e("AccountPreferences", "failed updating settings on server", t)
|
||||||
showErrorSnackbar(visibility, sensitive)
|
showErrorSnackbar(visibility, sensitive)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,8 @@ import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI
|
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
@ -124,8 +124,6 @@ class EmojiPreference(
|
||||||
finishDownload(font, binding)
|
finishDownload(font, binding)
|
||||||
}
|
}
|
||||||
).also { downloadDisposables[font.id] = it }
|
).also { downloadDisposables[font.id] = it }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
||||||
|
@ -222,12 +220,14 @@ class EmojiPreference(
|
||||||
context,
|
context,
|
||||||
0x1f973, // This is the codepoint of the party face emoji :D
|
0x1f973, // This is the codepoint of the party face emoji :D
|
||||||
launchIntent,
|
launchIntent,
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
)
|
||||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
mgr.set(
|
mgr.set(
|
||||||
AlarmManager.RTC,
|
AlarmManager.RTC,
|
||||||
System.currentTimeMillis() + 100,
|
System.currentTimeMillis() + 100,
|
||||||
mPendingIntent)
|
mPendingIntent
|
||||||
|
)
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue