Merge remote-tracking branch 'tuskyapp/develop'
This commit is contained in:
commit
2005b32dfa
|
@ -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```.
|
||||
|
||||
### Translation
|
||||
Translations are done through 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.
|
||||
Translations are done through our [Weblate](https://weblate.tusky.app/projects/tusky/tusky/).
|
||||
To add a new language, click on the 'Start a new translation' button on at the bottom of the page.
|
||||
|
||||
### 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
|
||||
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 Kotlin.
|
||||
|
||||
### 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
|
||||
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
|
||||
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:
|
||||
|
|
|
@ -15,7 +15,7 @@ def getGitSha = {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
defaultConfig {
|
||||
applicationId 'net.accelf.yuito'
|
||||
minSdkVersion 21
|
||||
|
@ -34,7 +34,6 @@ android {
|
|||
kapt {
|
||||
arguments {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.incremental", "true")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,10 +66,6 @@ android {
|
|||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
@ -97,19 +92,13 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
ext.lifecycleVersion = "2.2.0"
|
||||
ext.lifecycleVersion = "2.3.1"
|
||||
ext.roomVersion = '2.3.0'
|
||||
ext.retrofitVersion = '2.9.0'
|
||||
ext.okhttpVersion = '4.9.0'
|
||||
ext.glideVersion = '4.11.0'
|
||||
ext.daggerVersion = '2.30.1'
|
||||
ext.materialdrawerVersion = '8.2.0'
|
||||
ext.okhttpVersion = '4.9.1'
|
||||
ext.glideVersion = '4.12.0'
|
||||
ext.daggerVersion = '2.37'
|
||||
ext.materialdrawerVersion = '8.4.1'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
|
@ -121,16 +110,19 @@ repositories {
|
|||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.2.5"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.5.0'
|
||||
|
||||
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.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.cardview:cardview:1.0.0"
|
||||
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-appcompat:1.1.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||
|
@ -138,33 +130,33 @@ dependencies {
|
|||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
||||
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.work:work-runtime:2.4.0"
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
implementation "androidx.room:room-rxjava2:$roomVersion"
|
||||
implementation "androidx.work:work-runtime:2.5.0"
|
||||
implementation "androidx.room:room-ktx:$roomVersion"
|
||||
implementation "androidx.room:room-rxjava3:$roomVersion"
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
|
||||
implementation "com.google.android.material:material:1.3.0"
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:$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: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:okhttp3-integration:$glideVersion"
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.20"
|
||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||
implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.12"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
|
||||
|
||||
implementation "com.uber.autodispose:autodispose-android-archcomponents:1.4.0"
|
||||
implementation "com.uber.autodispose:autodispose:1.4.0"
|
||||
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.0.0"
|
||||
implementation "com.uber.autodispose2:autodispose:2.0.0"
|
||||
|
||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||
|
@ -180,9 +172,9 @@ dependencies {
|
|||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-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 "org.robolectric:robolectric:4.4"
|
||||
|
@ -192,6 +184,7 @@ dependencies {
|
|||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
|
||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
|
||||
implementation 'net.accelf:easter:1.0.2'
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
|
|
@ -65,7 +65,14 @@
|
|||
|
||||
# remove some kotlin overhead
|
||||
-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 checkNotNullParameter(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);
|
||||
}
|
||||
|
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ package com.keylesspalace.tusky
|
|||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
|
@ -33,12 +33,15 @@ class MigrationsTest {
|
|||
val active = true
|
||||
val accountId = "accountId"
|
||||
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,
|
||||
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`," +
|
||||
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
|
||||
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
|
||||
|
@ -46,7 +49,8 @@ class MigrationsTest {
|
|||
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
|
||||
"`mediaPreviewEnabled`) " +
|
||||
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
values)
|
||||
values
|
||||
)
|
||||
|
||||
db.close()
|
||||
|
||||
|
|
|
@ -3,9 +3,13 @@ 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.components.timeline.TimelineRepository
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
||||
import com.keylesspalace.tusky.db.TimelineDao
|
||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
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
|
||||
|
@ -41,8 +45,10 @@ class TimelineDAOTest {
|
|||
timelineDao.insertInTransaction(status, author, reblogger)
|
||||
}
|
||||
|
||||
val resultsFromDb = timelineDao.getStatusesForAccount(setOne.first.timelineUserId,
|
||||
maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10)
|
||||
val resultsFromDb = timelineDao.getStatusesForAccount(
|
||||
setOne.first.timelineUserId,
|
||||
maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10
|
||||
)
|
||||
.blockingGet()
|
||||
|
||||
assertEquals(2, resultsFromDb.size)
|
||||
|
@ -71,7 +77,6 @@ class TimelineDAOTest {
|
|||
assertEquals(author, result.account)
|
||||
assertEquals(status, result.status)
|
||||
assertNull(result.reblogAccount)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -143,7 +148,7 @@ class TimelineDAOTest {
|
|||
timelineDao.insertInTransaction(status, author, reblogAuthor)
|
||||
}
|
||||
|
||||
//make sure status 2 is no longer in db
|
||||
// make sure status 2 is no longer in db
|
||||
|
||||
assertEquals(
|
||||
newStatuses,
|
||||
|
@ -185,7 +190,6 @@ class TimelineDAOTest {
|
|||
)
|
||||
} else null
|
||||
|
||||
|
||||
val even = accountId % 2 == 0L
|
||||
val status = TimelineStatusEntity(
|
||||
serverId = statusId.toString(),
|
||||
|
|
|
@ -35,9 +35,6 @@
|
|||
android:resource="@xml/share_shortcuts" />
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SavedTootActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden" />
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
@ -124,7 +121,7 @@
|
|||
<activity android:name=".AboutActivity" />
|
||||
<activity android:name=".TabPreferenceActivity" />
|
||||
<activity
|
||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||
android:name="com.canhub.cropper.CropImageActivity"
|
||||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
<activity
|
||||
android:name=".components.search.SearchActivity"
|
||||
|
|
|
@ -11,7 +11,7 @@ import android.widget.TextView
|
|||
import androidx.annotation.StringRes
|
||||
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
|
||||
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 net.accelf.yuito.AccessTokenLoginActivity
|
||||
|
||||
|
@ -37,7 +37,7 @@ class AboutActivity : BottomSheetActivity(), Injectable {
|
|||
|
||||
binding.versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME)
|
||||
|
||||
if(BuildConfig.CUSTOM_INSTANCE.isBlank()) {
|
||||
if (BuildConfig.CUSTOM_INSTANCE.isBlank()) {
|
||||
binding.aboutPoweredByTusky.hide()
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
|
|||
val end = builder.getSpanEnd(span)
|
||||
val flags = builder.getSpanFlags(span)
|
||||
|
||||
val customSpan = object : CustomURLSpan(span.url) {}
|
||||
val customSpan = NoUnderlineURLSpan(span.url)
|
||||
|
||||
builder.removeSpan(span)
|
||||
builder.setSpan(customSpan, start, end, flags)
|
||||
|
|
|
@ -34,13 +34,16 @@ import androidx.annotation.Px
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
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.emoji.text.EmojiCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import com.bumptech.glide.Glide
|
||||
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.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
@ -59,7 +62,16 @@ import com.keylesspalace.tusky.interfaces.LinkListener
|
|||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.pager.AccountPagerAdapter
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.DefaultTextWatcher
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
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.viewmodel.AccountViewModel
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
|
@ -175,7 +187,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
binding.accountFieldList.layoutManager = LinearLayoutManager(this)
|
||||
binding.accountFieldList.adapter = accountFieldAdapter
|
||||
|
||||
|
||||
val accountListClickListener = { v: View ->
|
||||
val type = when (v.id) {
|
||||
R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS
|
||||
|
@ -236,19 +247,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
// set toolbar top margin according to system window insets
|
||||
binding.accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets ->
|
||||
val top = insets.systemWindowInsetTop
|
||||
|
||||
val toolbarParams = binding.accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.accountCoordinatorLayout) { _, insets ->
|
||||
val top = insets.getInsets(systemBars()).top
|
||||
val toolbarParams = binding.accountToolbar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
toolbarParams.topMargin = top
|
||||
|
||||
insets.consumeSystemWindowInsets()
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
// Setup the toolbar.
|
||||
|
@ -319,12 +327,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun makeNotificationBarTransparent() {
|
||||
val decorView = window.decorView
|
||||
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.statusBarColor = statusBarColorTransparent
|
||||
}
|
||||
|
||||
|
@ -353,12 +359,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
viewModel.accountFieldData.observe(this, {
|
||||
viewModel.accountFieldData.observe(
|
||||
this,
|
||||
{
|
||||
accountFieldAdapter.fields = it
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
})
|
||||
}
|
||||
)
|
||||
viewModel.noteSaved.observe(this) {
|
||||
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
||||
}
|
||||
|
@ -372,9 +380,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
viewModel.refresh()
|
||||
adapter.refreshContent()
|
||||
}
|
||||
viewModel.isRefreshing.observe(this, { isRefreshing ->
|
||||
viewModel.isRefreshing.observe(
|
||||
this,
|
||||
{ isRefreshing ->
|
||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||
})
|
||||
}
|
||||
)
|
||||
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
}
|
||||
|
||||
|
@ -427,7 +438,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
.centerCrop()
|
||||
.into(binding.accountHeaderImageView)
|
||||
|
||||
|
||||
binding.accountAvatarImageView.setOnClickListener { avatarView ->
|
||||
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||
|
||||
|
@ -484,7 +494,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -560,8 +569,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
// 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
|
||||
if (!viewModel.isSelf && followState == FollowState.FOLLOWING
|
||||
&& (relation.subscribing != null || relation.notifying != null)) {
|
||||
if (!viewModel.isSelf && followState == FollowState.FOLLOWING &&
|
||||
(relation.subscribing != null || relation.notifying != null)
|
||||
) {
|
||||
binding.accountSubscribeButton.show()
|
||||
binding.accountSubscribeButton.setOnClickListener {
|
||||
viewModel.changeSubscribingState()
|
||||
|
@ -695,11 +705,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
} else {
|
||||
getString(R.string.action_show_reblogs)
|
||||
}
|
||||
|
||||
} else {
|
||||
menu.removeItem(R.id.action_show_reblogs)
|
||||
}
|
||||
|
||||
} else {
|
||||
// It shouldn't be possible to block, mute or report yourself.
|
||||
menu.removeItem(R.id.action_block)
|
||||
|
@ -769,8 +777,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
private fun mention() {
|
||||
loadedAccount?.let {
|
||||
val intent = ComposeActivity.startIntent(this,
|
||||
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username)))
|
||||
val intent = ComposeActivity.startIntent(
|
||||
this,
|
||||
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
@ -846,5 +856,4 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,18 +28,24 @@ import androidx.preference.PreferenceManager
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
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.State
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -146,11 +152,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
viewModel.load(listId)
|
||||
}
|
||||
if (error is IOException) {
|
||||
binding.messageView.setup(R.drawable.elephant_offline,
|
||||
R.string.error_network, retryAction)
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_offline,
|
||||
R.string.error_network, retryAction
|
||||
)
|
||||
} else {
|
||||
binding.messageView.setup(R.drawable.elephant_error,
|
||||
R.string.error_generic, retryAction)
|
||||
binding.messageView.setup(
|
||||
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 {
|
||||
return oldItem.second == newItem.second
|
||||
&& oldItem.first.deepEquals(newItem.first)
|
||||
return oldItem.second == newItem.second &&
|
||||
oldItem.first.deepEquals(newItem.first)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -199,6 +199,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
|||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requesters.containsKey(requestCode)) {
|
||||
PermissionRequester requester = requesters.remove(requestCode);
|
||||
requester.onRequestPermissionsResult(permissions, grantResults);
|
||||
|
|
|
@ -22,12 +22,12 @@ import android.widget.LinearLayout
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import javax.inject.Inject
|
||||
|
@ -62,7 +62,6 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER, text: String = "") {
|
||||
|
@ -81,7 +80,8 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
resolve = true
|
||||
).observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({ (accounts, statuses) ->
|
||||
.subscribe(
|
||||
{ (accounts, statuses) ->
|
||||
if (getCancelSearchRequested(url)) {
|
||||
return@subscribe
|
||||
}
|
||||
|
@ -97,12 +97,14 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
if (!getCancelSearchRequested(url)) {
|
||||
onEndSearch(url)
|
||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
onBeginSearch(url)
|
||||
}
|
||||
|
@ -195,7 +197,8 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
|
|||
|
||||
if (uri.query != null ||
|
||||
uri.fragment != null ||
|
||||
uri.path == null) {
|
||||
uri.path == null
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -36,18 +36,24 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.canhub.cropper.CropImage
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
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.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import com.theartofdev.edmodo.cropper.CropImage
|
||||
import javax.inject.Inject
|
||||
|
||||
class EditProfileActivity : BaseActivity(), Injectable {
|
||||
|
@ -110,11 +116,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
binding.addFieldButton.setOnClickListener {
|
||||
accountFieldEditAdapter.addField()
|
||||
if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
|
||||
if (accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
|
||||
it.isVisible = false
|
||||
}
|
||||
|
||||
binding.scrollView.post{
|
||||
binding.scrollView.post {
|
||||
binding.scrollView.smoothScrollTo(0, it.bottom)
|
||||
}
|
||||
}
|
||||
|
@ -134,7 +140,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList())
|
||||
binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS
|
||||
|
||||
if(viewModel.avatarData.value == null) {
|
||||
if (viewModel.avatarData.value == null) {
|
||||
Glide.with(this)
|
||||
.load(me.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
|
@ -145,12 +151,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
.into(binding.avatarPreview)
|
||||
}
|
||||
|
||||
if(viewModel.headerData.value == null) {
|
||||
if (viewModel.headerData.value == null) {
|
||||
Glide.with(this)
|
||||
.load(me.header)
|
||||
.into(binding.headerPreview)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
is Error -> {
|
||||
|
@ -159,7 +164,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
viewModel.obtainProfile()
|
||||
}
|
||||
snackbar.show()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -179,8 +183,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
|
||||
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
|
||||
|
||||
viewModel.saveData.observe(this, {
|
||||
when(it) {
|
||||
viewModel.saveData.observe(
|
||||
this,
|
||||
{
|
||||
when (it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
}
|
||||
|
@ -191,8 +197,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
onSaveFailure(it.errorMessage)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
@ -202,19 +208,25 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if(!isFinishing) {
|
||||
viewModel.updateProfile(binding.displayNameEditText.text.toString(),
|
||||
if (!isFinishing) {
|
||||
viewModel.updateProfile(
|
||||
binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData())
|
||||
accountFieldEditAdapter.getFieldData()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeImage(liveData: LiveData<Resource<Bitmap>>,
|
||||
private fun observeImage(
|
||||
liveData: LiveData<Resource<Bitmap>>,
|
||||
imageView: ImageView,
|
||||
progressBar: View,
|
||||
roundedCorners: Boolean) {
|
||||
liveData.observe(this, {
|
||||
roundedCorners: Boolean
|
||||
) {
|
||||
liveData.observe(
|
||||
this,
|
||||
{
|
||||
|
||||
when (it) {
|
||||
is Success -> {
|
||||
|
@ -238,14 +250,14 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
is Error -> {
|
||||
progressBar.hide()
|
||||
if(!it.consumed) {
|
||||
if (!it.consumed) {
|
||||
onResizeFailure()
|
||||
it.consumed = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onMediaPick(pickType: PickType) {
|
||||
|
@ -261,8 +273,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
|
@ -310,11 +325,13 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
return
|
||||
}
|
||||
|
||||
viewModel.save(binding.displayNameEditText.text.toString(),
|
||||
viewModel.save(
|
||||
binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData(),
|
||||
this)
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSaveFailure(msg: String?) {
|
||||
|
@ -374,7 +391,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> {
|
||||
val result = CropImage.getActivityResult(data)
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> beginResize(result.uri)
|
||||
Activity.RESULT_OK -> beginResize(result?.uriContent)
|
||||
CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure()
|
||||
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()
|
||||
|
||||
when (currentlyPicking) {
|
||||
|
@ -398,12 +420,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
currentlyPicking = PickType.NOTHING
|
||||
|
||||
}
|
||||
|
||||
private fun onResizeFailure() {
|
||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||
endMediaPicking()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.widget.AdapterView
|
|||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
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.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
|
@ -21,7 +24,7 @@ import retrofit2.Response
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class FiltersActivity: BaseActivity() {
|
||||
class FiltersActivity : BaseActivity() {
|
||||
@Inject
|
||||
lateinit var api: MastodonApi
|
||||
|
||||
|
@ -30,7 +33,7 @@ class FiltersActivity: BaseActivity() {
|
|||
|
||||
private val binding by viewBinding(ActivityFiltersBinding::inflate)
|
||||
|
||||
private lateinit var context : String
|
||||
private lateinit var context: String
|
||||
private lateinit var filters: MutableList<Filter>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -54,7 +57,7 @@ class FiltersActivity: BaseActivity() {
|
|||
|
||||
private fun updateFilter(filter: Filter, itemIndex: Int) {
|
||||
api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt)
|
||||
.enqueue(object: Callback<Filter>{
|
||||
.enqueue(object : Callback<Filter> {
|
||||
override fun onFailure(call: Call<Filter>, t: Throwable) {
|
||||
Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
@ -76,7 +79,7 @@ class FiltersActivity: BaseActivity() {
|
|||
val filter = filters[itemIndex]
|
||||
if (filter.context.size == 1) {
|
||||
// This is the only context for this filter; delete it
|
||||
api.deleteFilter(filters[itemIndex].id).enqueue(object: Callback<ResponseBody> {
|
||||
api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback<ResponseBody> {
|
||||
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
|
||||
Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
@ -90,17 +93,19 @@ class FiltersActivity: BaseActivity() {
|
|||
} else {
|
||||
// Keep the filter, but remove it from this context
|
||||
val oldFilter = filters[itemIndex]
|
||||
val newFilter = Filter(oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
||||
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord)
|
||||
val newFilter = Filter(
|
||||
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
||||
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
|
||||
)
|
||||
updateFilter(newFilter, itemIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFilter(phrase: String, wholeWord: Boolean) {
|
||||
api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object: Callback<Filter> {
|
||||
api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object : Callback<Filter> {
|
||||
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
|
||||
val filterResponse = response.body()
|
||||
if(response.isSuccessful && filterResponse != null) {
|
||||
if (response.isSuccessful && filterResponse != null) {
|
||||
filters.add(filterResponse)
|
||||
refreshFilterDisplay()
|
||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||
|
@ -121,7 +126,7 @@ class FiltersActivity: BaseActivity() {
|
|||
AlertDialog.Builder(this@FiltersActivity)
|
||||
.setTitle(R.string.filter_addition_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked)
|
||||
}
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
|
@ -139,8 +144,10 @@ class FiltersActivity: BaseActivity() {
|
|||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
|
||||
val oldFilter = filters[itemIndex]
|
||||
val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
|
||||
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked)
|
||||
val newFilter = Filter(
|
||||
oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
|
||||
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked
|
||||
)
|
||||
updateFilter(newFilter, itemIndex)
|
||||
}
|
||||
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
||||
|
@ -162,39 +169,35 @@ class FiltersActivity: BaseActivity() {
|
|||
binding.addFilterButton.hide()
|
||||
binding.filterProgressBar.show()
|
||||
|
||||
api.getFilters().enqueue(object : Callback<List<Filter>> {
|
||||
override fun onResponse(call: Call<List<Filter>>, response: Response<List<Filter>>) {
|
||||
val filterResponse = response.body()
|
||||
if(response.isSuccessful && filterResponse != null) {
|
||||
lifecycleScope.launch {
|
||||
val newFilters = try {
|
||||
api.getFilters().await()
|
||||
} 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()
|
||||
|
||||
binding.filtersView.show()
|
||||
binding.addFilterButton.show()
|
||||
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 {
|
||||
const val FILTERS_CONTEXT = "filters_context"
|
||||
const val FILTERS_TITLE = "filters_title"
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RawRes
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RawRes
|
||||
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
||||
import com.keylesspalace.tusky.util.IOUtils
|
||||
import java.io.BufferedReader
|
||||
|
@ -42,7 +42,6 @@ class LicenseActivity : BaseActivity() {
|
|||
|
||||
loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView)
|
||||
loadFileIntoTextView(R.raw.mit, binding.licenseMitTextView)
|
||||
|
||||
}
|
||||
|
||||
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
|
||||
|
|
|
@ -23,32 +23,48 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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.annotation.StringRes
|
||||
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.RecyclerView
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
|
||||
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
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.Event.*
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event
|
||||
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.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
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.HasAndroidInjector
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -84,7 +100,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
binding.listsRecycler.adapter = adapter
|
||||
binding.listsRecycler.layoutManager = LinearLayoutManager(this)
|
||||
binding.listsRecycler.addItemDecoration(
|
||||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
|
||||
viewModel.state
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -101,9 +118,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
.subscribe { event ->
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
when (event) {
|
||||
CREATE_ERROR -> showMessage(R.string.error_create_list)
|
||||
RENAME_ERROR -> showMessage(R.string.error_rename_list)
|
||||
DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
||||
Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
|
||||
Event.RENAME_ERROR -> showMessage(R.string.error_rename_list)
|
||||
Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +138,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
.setView(layout)
|
||||
.setPositiveButton(
|
||||
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)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
@ -138,14 +156,13 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
private fun showListDeleteDialog(list: MastoList) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.dialog_delete_list_warning, list.title))
|
||||
.setPositiveButton(R.string.action_delete){ _, _ ->
|
||||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||
viewModel.deleteList(list.id)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
private fun update(state: ListsViewModel.State) {
|
||||
adapter.submitList(state.lists)
|
||||
binding.progressBar.visible(state.loadingState == LOADING)
|
||||
|
@ -166,8 +183,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
LOADED ->
|
||||
if (state.lists.isEmpty()) {
|
||||
binding.messageView.show()
|
||||
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
|
||||
null)
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_friend_empty, R.string.message_empty,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
binding.messageView.hide()
|
||||
}
|
||||
|
@ -182,7 +201,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
|
||||
private fun onListSelected(listId: String) {
|
||||
startActivityWithSlideInAnimation(
|
||||
ModalTimelineActivity.newIntent(this, TimelineFragment.Kind.LIST, listId))
|
||||
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
|
||||
)
|
||||
}
|
||||
|
||||
private fun openListSettings(list: MastoList) {
|
||||
|
@ -219,8 +239,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
}
|
||||
}
|
||||
|
||||
private inner class ListsAdapter
|
||||
: ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
|
||||
private inner class ListsAdapter :
|
||||
ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
|
||||
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
|
||||
}
|
||||
|
||||
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view),
|
||||
private inner class ListViewHolder(view: View) :
|
||||
RecyclerView.ViewHolder(view),
|
||||
View.OnClickListener {
|
||||
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
|
||||
val moreButton: ImageButton = view.findViewById(R.id.editListButton)
|
||||
|
|
|
@ -34,7 +34,9 @@ import com.keylesspalace.tusky.di.Injectable
|
|||
import com.keylesspalace.tusky.entity.AccessToken
|
||||
import com.keylesspalace.tusky.entity.AppCredentials
|
||||
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.viewBinding
|
||||
import okhttp3.HttpUrl
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
|
@ -62,12 +64,12 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
setContentView(binding.root)
|
||||
|
||||
if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
|
||||
if (savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
|
||||
binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
|
||||
binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
|
||||
}
|
||||
|
||||
if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
|
||||
if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
|
||||
Glide.with(binding.loginLogo)
|
||||
.load(BuildConfig.CUSTOM_LOGO_URL)
|
||||
.placeholder(null)
|
||||
|
@ -75,7 +77,8 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
binding.loginButton.setOnClickListener { onButtonClick() }
|
||||
|
||||
|
@ -95,7 +98,6 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
} else {
|
||||
binding.toolbar.visibility = View.GONE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun requiresLogin(): Boolean {
|
||||
|
@ -104,7 +106,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
if(isAdditionalLogin()) {
|
||||
if (isAdditionalLogin()) {
|
||||
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
}
|
||||
}
|
||||
|
@ -129,8 +131,10 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
val callback = object : Callback<AppCredentials> {
|
||||
override fun onResponse(call: Call<AppCredentials>,
|
||||
response: Response<AppCredentials>) {
|
||||
override fun onResponse(
|
||||
call: Call<AppCredentials>,
|
||||
response: Response<AppCredentials>
|
||||
) {
|
||||
if (!response.isSuccessful) {
|
||||
binding.loginButton.isEnabled = true
|
||||
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
|
||||
|
@ -160,11 +164,12 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
mastodonApi
|
||||
.authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website))
|
||||
.authenticateApp(
|
||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||
)
|
||||
.enqueue(callback)
|
||||
setLoading(true)
|
||||
|
||||
}
|
||||
|
||||
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
|
||||
|
@ -219,31 +224,27 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
} else {
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
||||
Log.e(TAG, String.format("%s %s",
|
||||
getString(R.string.error_retrieving_oauth_token),
|
||||
response.message()))
|
||||
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
||||
Log.e(TAG, String.format("%s %s",
|
||||
getString(R.string.error_retrieving_oauth_token),
|
||||
t.message))
|
||||
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message))
|
||||
}
|
||||
}
|
||||
|
||||
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
|
||||
"authorization_code").enqueue(callback)
|
||||
mastodonApi.fetchOAuthToken(
|
||||
domain, clientId, clientSecret, redirectUri, code,
|
||||
"authorization_code"
|
||||
).enqueue(callback)
|
||||
} else if (error != null) {
|
||||
/* Authorization failed. Put the error response where the user can read it and they
|
||||
* can try again. */
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
|
||||
Log.e(TAG, String.format("%s %s",
|
||||
getString(R.string.error_authorization_denied),
|
||||
error))
|
||||
Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
|
||||
} else {
|
||||
// This case means a junk response was received somehow.
|
||||
setLoading(false)
|
||||
|
|
|
@ -40,20 +40,20 @@ import androidx.appcompat.view.menu.MenuBuilder
|
|||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import autodispose2.androidx.lifecycle.autoDispose
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.target.FixedSizeDrawable
|
||||
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.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
@ -68,20 +68,25 @@ import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
|||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.fragment.NotificationsFragment
|
||||
import com.keylesspalace.tusky.fragment.SFragment
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
||||
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.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
|
@ -90,18 +95,27 @@ import com.mikepenz.materialdrawer.holder.BadgeStyle
|
|||
import com.mikepenz.materialdrawer.holder.ColorHolder
|
||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
||||
import com.mikepenz.materialdrawer.model.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
||||
import com.mikepenz.materialdrawer.model.AbstractDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.DividerDrawerItem
|
||||
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.*
|
||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
||||
import com.uber.autodispose.autoDispose
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.accelf.yuito.FooterDrawerItem
|
||||
import net.accelf.yuito.QuickTootViewModel
|
||||
import javax.inject.Inject
|
||||
|
@ -119,9 +133,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
@Inject
|
||||
lateinit var conversationRepository: ConversationsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var appDb: AppDatabase
|
||||
|
||||
@Inject
|
||||
lateinit var draftHelper: DraftHelper
|
||||
|
||||
|
@ -158,10 +169,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
super.onCreate(savedInstanceState)
|
||||
|
||||
val activeAccount = accountManager.activeAccount
|
||||
if (activeAccount == null) {
|
||||
// will be redirected to LoginActivity by BaseActivity
|
||||
return
|
||||
}
|
||||
?: return // will be redirected to LoginActivity by BaseActivity
|
||||
|
||||
var showNotificationTab = false
|
||||
if (intent != null) {
|
||||
/** there are two possibilities the accountId can be passed to MainActivity:
|
||||
|
@ -186,7 +195,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
forwardShare(intent)
|
||||
} else {
|
||||
// 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) {
|
||||
val requestedId = account.id
|
||||
if (requestedId == activeAccount.id) {
|
||||
|
@ -198,7 +209,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
changeAccount(requestedId, intent)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (accountRequested && savedInstanceState == null) {
|
||||
// user clicked a notification, show notification tab
|
||||
|
@ -276,7 +288,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
// Flush old media that was cached for sharing
|
||||
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
||||
}
|
||||
draftWarning()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -382,12 +393,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
||||
currentHiddenInList = true
|
||||
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) }
|
||||
addProfile(ProfileSettingDrawerItem().apply {
|
||||
addProfile(
|
||||
ProfileSettingDrawerItem().apply {
|
||||
identifier = DRAWER_ITEM_ADD_ACCOUNT
|
||||
nameRes = R.string.add_account_name
|
||||
descriptionRes = R.string.add_account_description
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_add
|
||||
}, 0)
|
||||
},
|
||||
0
|
||||
)
|
||||
attachToSliderView(binding.mainDrawer)
|
||||
dividerBelowHeader = false
|
||||
closeDrawerOnProfileListClick = true
|
||||
|
@ -468,7 +482,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_saved_toot
|
||||
nameRes = R.string.action_access_drafts
|
||||
iconRes = R.drawable.ic_notebook
|
||||
onClick = {
|
||||
val intent = DraftsActivity.newIntent(context)
|
||||
|
@ -536,14 +550,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
)
|
||||
|
||||
if (addSearchButton) {
|
||||
binding.mainDrawer.addItemsAtPosition(4,
|
||||
binding.mainDrawer.addItemsAtPosition(
|
||||
4,
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_search
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setSavedInstance(savedInstanceState)
|
||||
|
@ -663,11 +679,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
(fragment as ReselectableFragment).onReselect()
|
||||
}
|
||||
}
|
||||
R.id.tabReset -> {
|
||||
if (fragment is ReselectableFragment) {
|
||||
(fragment as ReselectableFragment).onReset()
|
||||
}
|
||||
}
|
||||
R.id.tabEditList -> {
|
||||
AccountsInListFragment.newInstance(
|
||||
tabs[i].arguments.getOrNull(0).orEmpty(),
|
||||
|
@ -676,25 +687,25 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
R.id.tabToggleStreaming -> {
|
||||
if (fragment is TimelineFragment) {
|
||||
fragment.isStreamingEnabled = !fragment.isStreamingEnabled
|
||||
item.isChecked = fragment.isStreamingEnabled
|
||||
val current = fragment.toggleStreaming()
|
||||
item.isChecked = current
|
||||
tintCheckIcon(item)
|
||||
|
||||
if (fragment.isStreamingEnabled) {
|
||||
if (current) {
|
||||
streamingTabsCount++
|
||||
} else {
|
||||
streamingTabsCount--
|
||||
}
|
||||
keepScreenOn()
|
||||
|
||||
tabs[i] = tabs[i].copy(enableStreaming = fragment.isStreamingEnabled)
|
||||
tabs[i] = tabs[i].copy(enableStreaming = current)
|
||||
accountManager.activeAccount?.let {
|
||||
Single.fromCallable {
|
||||
it.tabPreferences = tabs
|
||||
accountManager.saveAccount(it)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
@ -767,25 +778,24 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
|
||||
val activeAccount = accountManager.activeAccount
|
||||
|
||||
//open profile when active image was clicked
|
||||
// open profile when active image was clicked
|
||||
if (current && activeAccount != null) {
|
||||
val intent = AccountActivity.getIntent(this, activeAccount.accountId)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
return false
|
||||
}
|
||||
//open LoginActivity to add new account
|
||||
// open LoginActivity to add new account
|
||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true))
|
||||
return false
|
||||
}
|
||||
//change Account
|
||||
// change Account
|
||||
changeAccount(profile.identifier, null)
|
||||
return false
|
||||
}
|
||||
|
||||
private fun changeAccount(newSelectedId: Long, forward: Intent?) {
|
||||
cacheUpdater.stop()
|
||||
SFragment.flushFilters()
|
||||
accountManager.setActiveAccount(newSelectedId)
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
@ -805,25 +815,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.action_logout)
|
||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
|
||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
lifecycleScope.launch {
|
||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
|
||||
cacheUpdater.clearForUser(activeAccount.id)
|
||||
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
||||
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
|
||||
removeShortcut(this, activeAccount)
|
||||
removeShortcut(this@MainActivity, activeAccount)
|
||||
val newAccount = accountManager.logActiveAccountOut()
|
||||
if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||
NotificationHelper.disablePullNotifications(this)
|
||||
if (!NotificationHelper.areNotificationsEnabled(
|
||||
this@MainActivity,
|
||||
accountManager
|
||||
)
|
||||
) {
|
||||
NotificationHelper.disablePullNotifications(this@MainActivity)
|
||||
}
|
||||
val intent = if (newAccount == null) {
|
||||
LoginActivity.getIntent(this, false)
|
||||
LoginActivity.getIntent(this@MainActivity, false)
|
||||
} else {
|
||||
Intent(this, MainActivity::class.java)
|
||||
Intent(this@MainActivity, MainActivity::class.java)
|
||||
}
|
||||
startActivity(intent)
|
||||
finishWithoutSlideOutAnimation()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
@ -937,30 +953,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
header.setActiveProfile(accountManager.activeAccount!!.id)
|
||||
}
|
||||
|
||||
private fun draftWarning() {
|
||||
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 getActionButton() = binding.composeButton
|
||||
|
||||
override fun androidInjector() = androidInjector
|
||||
|
||||
|
|
|
@ -5,17 +5,17 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import autodispose2.androidx.lifecycle.autoDispose
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
|
||||
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.uber.autodispose.AutoDispose.autoDisposable
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import net.accelf.yuito.QuickTootViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -43,8 +43,8 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
|
|||
}
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
|
||||
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind
|
||||
?: TimelineFragment.Kind.HOME
|
||||
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind
|
||||
?: TimelineViewModel.Kind.HOME
|
||||
val argument = intent?.getStringExtra(ARG_ARG)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
|
||||
|
@ -55,7 +55,7 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
|
|||
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.`as`(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(binding.viewQuickToot::handleEvent)
|
||||
binding.floatingBtn.setOnClickListener(binding.viewQuickToot::onFABClicked)
|
||||
}
|
||||
|
@ -69,13 +69,15 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
|
|||
private const val ARG_ARG = "arg"
|
||||
|
||||
@JvmStatic
|
||||
fun newIntent(context: Context, kind: TimelineFragment.Kind,
|
||||
argument: String?): Intent {
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
kind: TimelineViewModel.Kind,
|
||||
argument: String?
|
||||
): Intent {
|
||||
val intent = Intent(context, ModalTimelineActivity::class.java)
|
||||
intent.putExtra(ARG_KIND, kind)
|
||||
intent.putExtra(ARG_ARG, argument)
|
||||
return intent
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,213 +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(),
|
||||
/*quoteId*/null,
|
||||
/*quoteStatusAuthor*/null,
|
||||
/*quoteStatusContent*/null,
|
||||
/*replyVisibility*/null,
|
||||
item.getVisibility(),
|
||||
item.getContentWarning(),
|
||||
item.getInReplyToUsername(),
|
||||
item.getInReplyToText(),
|
||||
/*mediaAttachments*/null,
|
||||
/*draftAttachments*/null,
|
||||
/*scheduledAt*/null,
|
||||
/*sensitive*/null,
|
||||
/*poll*/null,
|
||||
/* modifiedInitialState */ true,
|
||||
false
|
||||
);
|
||||
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.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import net.accelf.yuito.CustomUncaughtExceptionHandler
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -50,5 +49,4 @@ class SplashActivity : AppCompatActivity(), Injectable {
|
|||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,17 +21,15 @@ import android.os.Bundle
|
|||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import autodispose2.androidx.lifecycle.autoDispose
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineViewModel.Kind
|
||||
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment.Kind
|
||||
import com.uber.autodispose.AutoDispose
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import net.accelf.yuito.QuickTootViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -56,7 +54,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
||||
val title = if(kind == Kind.FAVOURITES) {
|
||||
val title = if (kind == Kind.FAVOURITES) {
|
||||
R.string.title_favourites
|
||||
} else {
|
||||
R.string.title_bookmarks
|
||||
|
@ -77,7 +75,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.`as`(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(binding.viewQuickToot::handleEvent)
|
||||
binding.floatingBtn.setOnClickListener(binding.viewQuickToot::onFABClicked)
|
||||
}
|
||||
|
@ -100,5 +98,4 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,8 +20,9 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
|
||||
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 */
|
||||
|
||||
|
@ -35,13 +36,15 @@ const val LIST = "List"
|
|||
|
||||
const val STREAMING = "STR"
|
||||
|
||||
data class TabData(val id: String,
|
||||
data class TabData(
|
||||
val id: String,
|
||||
@StringRes val text: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
val fragment: (List<String>) -> Fragment,
|
||||
val arguments: List<String> = emptyList(),
|
||||
val title: (Context) -> String = { context -> context.getString(text)},
|
||||
val enableStreaming: Boolean = false)
|
||||
val title: (Context) -> String = { context -> context.getString(text) },
|
||||
val enableStreaming: Boolean = false,
|
||||
)
|
||||
|
||||
fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabData {
|
||||
val enableStreaming = id.endsWith(STREAMING)
|
||||
|
@ -50,7 +53,7 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
|||
HOME,
|
||||
R.string.title_home,
|
||||
R.drawable.ic_home_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.HOME, enableStreaming = enableStreaming) },
|
||||
{ TimelineFragment.newInstance(TimelineViewModel.Kind.HOME, enableStreaming = enableStreaming) },
|
||||
enableStreaming = enableStreaming
|
||||
)
|
||||
NOTIFICATIONS -> TabData(
|
||||
|
@ -63,14 +66,14 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
|||
LOCAL,
|
||||
R.string.title_public_local,
|
||||
R.drawable.ic_local_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL, enableStreaming = enableStreaming) },
|
||||
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL, enableStreaming = enableStreaming) },
|
||||
enableStreaming = enableStreaming
|
||||
)
|
||||
FEDERATED -> TabData(
|
||||
FEDERATED,
|
||||
R.string.title_public_federated,
|
||||
R.drawable.ic_public_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED, enableStreaming = enableStreaming) },
|
||||
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED, enableStreaming = enableStreaming) },
|
||||
enableStreaming = enableStreaming
|
||||
)
|
||||
DIRECT -> TabData(
|
||||
|
@ -85,13 +88,13 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
|||
R.drawable.ic_hashtag,
|
||||
{ args -> TimelineFragment.newHashtagInstance(args) },
|
||||
arguments,
|
||||
{ context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) }}
|
||||
{ context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) } }
|
||||
)
|
||||
LIST -> TabData(
|
||||
LIST,
|
||||
R.string.list,
|
||||
R.drawable.ic_list,
|
||||
{ args -> TimelineFragment.newInstance(TimelineFragment.Kind.LIST, args.getOrNull(0).orEmpty(), true, enableStreaming) },
|
||||
{ args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty(), true, enableStreaming) },
|
||||
arguments,
|
||||
{ arguments.getOrNull(1).orEmpty() },
|
||||
enableStreaming
|
||||
|
|
|
@ -31,6 +31,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionManager
|
||||
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.MaterialContainerTransform
|
||||
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.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -256,7 +256,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
mastodonApi.getLists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe (
|
||||
.subscribe(
|
||||
{ lists ->
|
||||
adapter.addAll(lists)
|
||||
},
|
||||
|
@ -333,7 +333,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
.subscribeOn(Schedulers.io())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe()
|
||||
|
||||
}
|
||||
tabsChanged = true
|
||||
}
|
||||
|
@ -357,5 +356,4 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
private const val MIN_TAB_COUNT = 2
|
||||
private const val MAX_TAB_COUNT = 9
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,16 +22,16 @@ import android.util.Log
|
|||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.WorkManager
|
||||
import autodispose2.AutoDisposePlugins
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
||||
import com.keylesspalace.tusky.di.AppInjector
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
||||
import com.keylesspalace.tusky.util.LocaleManager
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.uber.autodispose.AutoDisposePlugins
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import org.conscrypt.Conscrypt
|
||||
import java.security.Security
|
||||
import javax.inject.Inject
|
||||
|
|
|
@ -41,27 +41,27 @@ import androidx.fragment.app.FragmentActivity
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||
import autodispose2.autoDispose
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.FutureTarget
|
||||
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
||||
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.fragment.ViewImageFragment
|
||||
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
||||
import com.keylesspalace.tusky.pager.ImagePagerAdapter
|
||||
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
||||
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
|
||||
|
||||
|
@ -102,7 +102,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
|
||||
// Setup the view pager.
|
||||
ImagePagerAdapter(this, realAttachs, initialPosition)
|
||||
|
||||
} else {
|
||||
imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
|
||||
?: throw IllegalArgumentException("attachment list or image url has to be set")
|
||||
|
@ -112,7 +111,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.viewPager.setCurrentItem(initialPosition, false)
|
||||
binding.viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
binding.toolbar.title = getPageTitle(position)
|
||||
}
|
||||
|
@ -138,6 +137,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
|
||||
|
||||
window.statusBarColor = Color.BLACK
|
||||
window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
|
@ -192,7 +192,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
|
||||
private fun getPageTitle(position: Int): CharSequence {
|
||||
if(attachments == null) {
|
||||
if (attachments == null) {
|
||||
return ""
|
||||
}
|
||||
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size)
|
||||
|
@ -205,8 +205,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
|
||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES,
|
||||
getString(R.string.app_name) + "/" + filename)
|
||||
request.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_PICTURES,
|
||||
getString(R.string.app_name) + "/" + filename
|
||||
)
|
||||
downloadManager.enqueue(request)
|
||||
}
|
||||
|
||||
|
@ -260,7 +262,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
|
||||
}
|
||||
|
||||
|
||||
private var isCreating: Boolean = false
|
||||
|
||||
private fun shareImage(directory: File, url: String) {
|
||||
|
@ -283,7 +284,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
Log.e(TAG, "Error writing temporary media.")
|
||||
}
|
||||
return@fromCallable false
|
||||
|
||||
}
|
||||
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
@ -308,7 +308,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
Log.e(TAG, "Failed to download image", error)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private fun shareMediaFile(directory: File, url: String) {
|
||||
|
@ -351,7 +350,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
}
|
||||
|
||||
abstract class ViewMediaAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) {
|
||||
abstract class ViewMediaAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
||||
abstract fun onTransitionEnd(position: Int)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,14 +24,8 @@ import androidx.appcompat.app.ActionBar;
|
|||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory;
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
||||
|
||||
import net.accelf.yuito.QuickTootView;
|
||||
import net.accelf.yuito.QuickTootViewModel;
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
|
@ -40,10 +34,6 @@ import javax.inject.Inject;
|
|||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.HasAndroidInjector;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||
|
||||
public class ViewTagActivity extends BottomSheetActivity implements HasAndroidInjector {
|
||||
|
||||
|
@ -51,10 +41,6 @@ public class ViewTagActivity extends BottomSheetActivity implements HasAndroidIn
|
|||
|
||||
@Inject
|
||||
public DispatchingAndroidInjector<Object> dispatchingAndroidInjector;
|
||||
@Inject
|
||||
public EventHub eventHub;
|
||||
@Inject
|
||||
public ViewModelFactory viewModelFactory;
|
||||
|
||||
public static Intent getIntent(Context context, String tag){
|
||||
Intent intent = new Intent(context,ViewTagActivity.class);
|
||||
|
@ -83,15 +69,6 @@ public class ViewTagActivity extends BottomSheetActivity implements HasAndroidIn
|
|||
Fragment fragment = TimelineFragment.newHashtagInstance(Collections.singletonList(hashtag));
|
||||
fragmentTransaction.replace(R.id.fragment_container, fragment);
|
||||
fragmentTransaction.commit();
|
||||
|
||||
QuickTootViewModel quickTootViewModel = viewModelFactory.create(QuickTootViewModel.class);
|
||||
QuickTootView quickTootView = findViewById(R.id.viewQuickToot);
|
||||
quickTootView.attachViewModel(quickTootViewModel, this);
|
||||
|
||||
eventHub.getEvents()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.subscribe(quickTootView::handleEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -25,10 +25,10 @@ import com.keylesspalace.tusky.entity.Emoji
|
|||
import com.keylesspalace.tusky.entity.Field
|
||||
import com.keylesspalace.tusky.entity.IdentityProof
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.*
|
||||
|
||||
class AccountFieldAdapter(
|
||||
private val linkListener: LinkListener,
|
||||
|
@ -50,7 +50,7 @@ class AccountFieldAdapter(
|
|||
val nameTextView = holder.binding.accountFieldName
|
||||
val valueTextView = holder.binding.accountFieldValue
|
||||
|
||||
if(proofOrField.isLeft()) {
|
||||
if (proofOrField.isLeft()) {
|
||||
val identityProof = proofOrField.asLeft()
|
||||
|
||||
nameTextView.text = identityProof.provider
|
||||
|
@ -67,12 +67,11 @@ class AccountFieldAdapter(
|
|||
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
||||
LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener)
|
||||
|
||||
if(field.verifiedAt != null) {
|
||||
if (field.verifiedAt != null) {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||
} else {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 )
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
fields.forEach { field ->
|
||||
fieldData.add(MutableStringPair(field.name, field.value))
|
||||
}
|
||||
if(fieldData.isEmpty()) {
|
||||
if (fieldData.isEmpty()) {
|
||||
fieldData.add(MutableStringPair("", ""))
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
holder.binding.accountFieldName.setText(fieldData[position].first)
|
||||
holder.binding.accountFieldValue.setText(fieldData[position].second)
|
||||
|
||||
holder.binding.accountFieldName.addTextChangedListener(object: TextWatcher {
|
||||
holder.binding.accountFieldName.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(newText: Editable) {
|
||||
fieldData[holder.bindingAdapterPosition].first = newText.toString()
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
holder.binding.accountFieldValue.addTextChangedListener(object: TextWatcher {
|
||||
holder.binding.accountFieldValue.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(newText: Editable) {
|
||||
fieldData[holder.bindingAdapterPosition].second = newText.toString()
|
||||
}
|
||||
|
@ -82,9 +82,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
|
||||
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.db.AccountEntity
|
||||
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) {
|
||||
|
||||
|
@ -48,7 +49,6 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
|
|||
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
|
||||
|
||||
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
|
||||
|
||||
}
|
||||
|
||||
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,15 +22,15 @@ import com.bumptech.glide.Glide
|
|||
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
class EmojiAdapter(
|
||||
emojiList: List<Emoji>,
|
||||
private val onEmojiSelectedListener: OnEmojiSelectedListener
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
|
||||
|
||||
private val emojiList : List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
|
||||
.sortedBy { it.shortcode.toLowerCase(Locale.ROOT) }
|
||||
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
|
||||
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
|
||||
|
||||
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.entity.Account
|
||||
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(
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
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
|
||||
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.Status
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
|
||||
class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding,
|
||||
private val retryCallback: () -> Unit)
|
||||
: RecyclerView.ViewHolder(binding.root) {
|
||||
class NetworkStateViewHolder(
|
||||
private val binding: ItemNetworkStateBinding,
|
||||
private val retryCallback: () -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) {
|
||||
binding.progressBar.visible(state?.status == Status.RUNNING)
|
||||
binding.retryButton.visible(state?.status == Status.FAILED)
|
||||
binding.errorMsg.visible(state?.msg != null)
|
||||
binding.errorMsg.text = state?.msg
|
||||
fun setUpWithNetworkState(state: LoadState) {
|
||||
binding.progressBar.visible(state == LoadState.Loading)
|
||||
binding.retryButton.visible(state is LoadState.Error)
|
||||
val msg = if (state is LoadState.Error) {
|
||||
state.error.message
|
||||
} else {
|
||||
null
|
||||
}
|
||||
binding.errorMsg.visible(msg != null)
|
||||
binding.errorMsg.text = msg
|
||||
binding.retryButton.setOnClickListener {
|
||||
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.
|
||||
*
|
||||
|
@ -199,14 +199,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
} else {
|
||||
holder.showNotificationContent(true);
|
||||
|
||||
holder.setDisplayName(statusViewData.getUserFullName(), statusViewData.getAccountEmojis());
|
||||
holder.setUsername(statusViewData.getNickname());
|
||||
holder.setCreatedAt(statusViewData.getCreatedAt());
|
||||
Status status = statusViewData.getActionable();
|
||||
holder.setDisplayName(status.getAccount().getName(), status.getAccount().getEmojis());
|
||||
holder.setUsername(status.getAccount().getUsername());
|
||||
holder.setCreatedAt(status.getCreatedAt());
|
||||
|
||||
if(concreteNotificaton.getType() == Notification.Type.STATUS) {
|
||||
holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot());
|
||||
if (concreteNotificaton.getType() == Notification.Type.STATUS) {
|
||||
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
|
||||
} else {
|
||||
holder.setAvatars(statusViewData.getAvatar(),
|
||||
holder.setAvatars(status.getAccount().getAvatar(),
|
||||
concreteNotificaton.getAccount().getAvatar());
|
||||
}
|
||||
}
|
||||
|
@ -219,7 +220,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
if (payloadForHolder instanceof List)
|
||||
for (Object item : (List) payloadForHolder) {
|
||||
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) {
|
||||
holder.setCreatedAt(statusViewData.getCreatedAt());
|
||||
holder.setCreatedAt(statusViewData.getStatus().getActionableStatus().getCreatedAt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -539,7 +540,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
message.setText(emojifiedText);
|
||||
|
||||
if (statusViewData != null) {
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText());
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus().getSpoilerText());
|
||||
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||
if (statusViewData.isExpanded()) {
|
||||
|
@ -627,7 +628,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private void setupContentAndSpoiler(final LinkListener listener) {
|
||||
|
||||
boolean shouldShowContentIfSpoiler = statusViewData.isExpanded();
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText());
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus(). getSpoilerText());
|
||||
if (!shouldShowContentIfSpoiler && hasSpoiler) {
|
||||
statusContent.setVisibility(View.GONE);
|
||||
} else {
|
||||
|
@ -635,7 +636,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
|
||||
Spanned content = statusViewData.getContent();
|
||||
List<Emoji> emojis = statusViewData.getStatusEmojis();
|
||||
List<Emoji> emojis = statusViewData.getActionable().getEmojis();
|
||||
|
||||
if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) {
|
||||
contentCollapseButton.setOnClickListener(view -> {
|
||||
|
@ -661,17 +662,22 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||
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.getStatusEmojis(),
|
||||
statusViewData.getActionable().getEmojis(),
|
||||
contentWarningDescriptionTextView,
|
||||
statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
} else {
|
||||
emojifiedContentWarning = "";
|
||||
}
|
||||
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
|
||||
|
||||
setQuoteContainer(statusViewData.getQuote(), listener, statusDisplayOptions);
|
||||
setQuoteContainer(statusViewData.getStatus().getQuote(), listener, statusDisplayOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ import com.keylesspalace.tusky.viewdata.PollOptionViewData
|
|||
import com.keylesspalace.tusky.viewdata.buildDescription
|
||||
import com.keylesspalace.tusky.viewdata.calculatePercent
|
||||
|
||||
class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||
class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||
|
||||
private var pollOptions: List<PollOptionViewData> = emptyList()
|
||||
private var voteCount: Int = 0
|
||||
|
@ -46,7 +46,8 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
emojis: List<Emoji>,
|
||||
mode: Int,
|
||||
resultClickListener: View.OnClickListener?,
|
||||
animateEmojis: Boolean) {
|
||||
animateEmojis: Boolean
|
||||
) {
|
||||
this.pollOptions = options
|
||||
this.voteCount = voteCount
|
||||
this.votersCount = votersCount
|
||||
|
@ -57,12 +58,11 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun getSelected() : List<Int> {
|
||||
fun getSelected(): List<Int> {
|
||||
return pollOptions.filter { it.selected }
|
||||
.map { pollOptions.indexOf(it) }
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
|
||||
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return BindingHolder(binding)
|
||||
|
@ -82,7 +82,7 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
radioButton.visible(mode == SINGLE)
|
||||
checkBox.visible(mode == MULTIPLE)
|
||||
|
||||
when(mode) {
|
||||
when (mode) {
|
||||
RESULT -> {
|
||||
val percent = calculatePercent(option.votesCount, votersCount, voteCount)
|
||||
val emojifiedPollOptionText = buildDescription(option.title, percent, resultTextView.context)
|
||||
|
@ -114,7 +114,6 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.core.widget.TextViewCompat
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
|
||||
class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
|
||||
class PreviewPollOptionsAdapter : RecyclerView.Adapter<PreviewViewHolder>() {
|
||||
|
||||
private var options: List<String> = emptyList()
|
||||
private var multiple: Boolean = false
|
||||
|
@ -60,7 +60,6 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
|
|||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -210,7 +210,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
protected void setSpoilerAndContent(boolean expanded,
|
||||
@NonNull Spanned content,
|
||||
@Nullable String spoilerText,
|
||||
@Nullable Status.Mention[] mentions,
|
||||
@Nullable List<Status.Mention> mentions,
|
||||
@NonNull List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||
|
@ -252,7 +252,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private void setTextVisible(boolean sensitive,
|
||||
boolean expanded,
|
||||
Spanned content,
|
||||
Status.Mention[] mentions,
|
||||
List<Status.Mention> mentions,
|
||||
List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
|
@ -814,23 +814,25 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
this.setupWithStatus(status, listener, statusDisplayOptions, null);
|
||||
}
|
||||
|
||||
protected void setupWithStatus(StatusViewData.Concrete status,
|
||||
public void setupWithStatus(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
if (payloads == null) {
|
||||
setDisplayName(status.getUserFullName(), status.getAccountEmojis(), statusDisplayOptions);
|
||||
setUsername(status.getNickname());
|
||||
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
||||
setStatusVisibility(status.getVisibility());
|
||||
setIsReply(status.getInReplyToId() != null);
|
||||
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions);
|
||||
setReblogged(status.isReblogged());
|
||||
setFavourited(status.isFavourited());
|
||||
setQuoteContainer(status.getQuote(), listener, statusDisplayOptions);
|
||||
setBookmarked(status.isBookmarked());
|
||||
List<Attachment> attachments = status.getAttachments();
|
||||
boolean sensitive = status.isSensitive();
|
||||
Status actionable = status.getActionable();
|
||||
setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
|
||||
setUsername(status.getUsername());
|
||||
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
|
||||
setStatusVisibility(actionable.getVisibility());
|
||||
setIsReply(actionable.getInReplyToId() != null);
|
||||
setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(),
|
||||
actionable.getAccount().getBot(), statusDisplayOptions);
|
||||
setReblogged(actionable.getReblogged());
|
||||
setFavourited(actionable.getFavourited());
|
||||
setQuoteContainer(actionable.getQuote(), listener, statusDisplayOptions);
|
||||
setBookmarked(actionable.getBookmarked());
|
||||
List<Attachment> attachments = actionable.getAttachments();
|
||||
boolean sensitive = actionable.getSensitive();
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
|
||||
|
||||
|
@ -855,12 +857,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions);
|
||||
}
|
||||
|
||||
setupButtons(listener, status.getSenderId(), status.getContent().toString(),
|
||||
status.isNotestock(), statusDisplayOptions);
|
||||
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
|
||||
setQuoteEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
|
||||
setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(),
|
||||
actionable.isNotestock(), statusDisplayOptions);
|
||||
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
||||
setQuoteEnabled(actionable.rebloggingAllowed() && !actionable.isNotestock(), 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);
|
||||
|
||||
|
@ -874,7 +879,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
if (payloads instanceof List)
|
||||
for (Object item : (List<?>) payloads) {
|
||||
if (Key.KEY_CREATED.equals(item)) {
|
||||
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
||||
setCreatedAt(status.getActionable().getCreatedAt(), statusDisplayOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -893,21 +898,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status,
|
||||
StatusDisplayOptions statusDisplayOptions) {
|
||||
Context context = itemView.getContext();
|
||||
Status actionable = status.getActionable();
|
||||
|
||||
String description = context.getString(R.string.description_status,
|
||||
status.getUserFullName(),
|
||||
actionable.getAccount().getName(),
|
||||
getContentWarningDescription(context, status),
|
||||
(TextUtils.isEmpty(status.getSpoilerText()) || !status.isSensitive() || status.isExpanded() ? status.getContent() : ""),
|
||||
getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions),
|
||||
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
|
||||
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
||||
getReblogDescription(context, status),
|
||||
status.getNickname(),
|
||||
status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
||||
status.isFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
||||
status.isBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
||||
status.getUsername(),
|
||||
actionable.getReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
||||
actionable.getFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
||||
actionable.getBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
||||
getMediaDescription(context, status),
|
||||
getVisibilityDescription(context, status.getVisibility()),
|
||||
getFavsText(context, status.getFavouritesCount()),
|
||||
getReblogsText(context, status.getReblogsCount()),
|
||||
getVisibilityDescription(context, actionable.getVisibility()),
|
||||
getFavsText(context, actionable.getFavouritesCount()),
|
||||
getReblogsText(context, actionable.getReblogsCount()),
|
||||
getPollDescription(status, context, statusDisplayOptions)
|
||||
);
|
||||
itemView.setContentDescription(description);
|
||||
|
@ -915,10 +921,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
private static CharSequence getReblogDescription(Context context,
|
||||
@NonNull StatusViewData.Concrete status) {
|
||||
String rebloggedUsername = status.getRebloggedByUsername();
|
||||
if (rebloggedUsername != null) {
|
||||
Status reblog = status.getRebloggingStatus();
|
||||
if (reblog != null) {
|
||||
return context
|
||||
.getString(R.string.status_boosted_format, rebloggedUsername);
|
||||
.getString(R.string.status_boosted_format, reblog.getAccount().getUsername());
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
@ -926,11 +932,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
private static CharSequence getMediaDescription(Context context,
|
||||
@NonNull StatusViewData.Concrete status) {
|
||||
if (status.getAttachments().isEmpty()) {
|
||||
if (status.getActionable().getAttachments().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder mediaDescriptions = CollectionsKt.fold(
|
||||
status.getAttachments(),
|
||||
status.getActionable().getAttachments(),
|
||||
new StringBuilder(),
|
||||
(builder, a) -> {
|
||||
if (a.getDescription() == null) {
|
||||
|
@ -983,7 +989,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status,
|
||||
Context context,
|
||||
StatusDisplayOptions statusDisplayOptions) {
|
||||
PollViewData poll = status.getPoll();
|
||||
PollViewData poll = PollViewDataKt.toViewData(status.getActionable().getPoll());
|
||||
if (poll == null) {
|
||||
return "";
|
||||
} else {
|
||||
|
@ -1089,7 +1095,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
StatusDisplayOptions statusDisplayOptions,
|
||||
Context context) {
|
||||
String votesText;
|
||||
if(poll.getVotersCount() == null) {
|
||||
if (poll.getVotersCount() == null) {
|
||||
String voters = numberFormat.format(poll.getVotesCount());
|
||||
votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), voters);
|
||||
} else {
|
||||
|
@ -1113,12 +1119,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) {
|
||||
final Card card = status.getActionable().getCard();
|
||||
if (cardViewMode != CardViewMode.NONE &&
|
||||
status.getAttachments().size() == 0 &&
|
||||
status.getCard() != null &&
|
||||
!TextUtils.isEmpty(status.getCard().getUrl()) &&
|
||||
status.getActionable().getAttachments().size() == 0 &&
|
||||
card != null &&
|
||||
!TextUtils.isEmpty(card.getUrl()) &&
|
||||
(!status.isCollapsible() || !status.isCollapsed())) {
|
||||
final Card card = status.getCard();
|
||||
cardView.setVisibility(View.VISIBLE);
|
||||
cardTitle.setText(card.getTitle());
|
||||
if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) {
|
||||
|
@ -1137,7 +1143,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
|
||||
// so let's blur the preview in that case
|
||||
// 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 topRightRadius = 0;
|
||||
|
|
|
@ -105,7 +105,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void setupWithStatus(final StatusViewData.Concrete status,
|
||||
public void setupWithStatus(final StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
|
@ -114,12 +114,13 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
if (payloads == null) {
|
||||
|
||||
if (!statusDisplayOptions.hideStats()) {
|
||||
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
|
||||
setReblogAndFavCount(status.getActionable().getReblogsCount(),
|
||||
status.getActionable().getFavouritesCount(), listener);
|
||||
} else {
|
||||
hideQuantitativeStats();
|
||||
}
|
||||
|
||||
setApplication(status.getApplication());
|
||||
setApplication(status.getActionable().getApplication());
|
||||
|
||||
View.OnLongClickListener longClickListener = view -> {
|
||||
TextView textView = (TextView) view;
|
||||
|
|
|
@ -26,6 +26,8 @@ import androidx.annotation.Nullable;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
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.util.CustomEmojiHelper;
|
||||
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.viewdata.StatusViewData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import at.connyduck.sparkbutton.helpers.Utils;
|
||||
|
||||
public class StatusViewHolder extends StatusBaseViewHolder {
|
||||
|
@ -54,7 +58,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void setupWithStatus(StatusViewData.Concrete status,
|
||||
public void setupWithStatus(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
|
@ -62,11 +66,13 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
|
||||
setupCollapsedState(status, listener);
|
||||
|
||||
String rebloggedByDisplayName = status.getRebloggedByUsername();
|
||||
if (rebloggedByDisplayName == null) {
|
||||
Status reblogging = status.getRebloggingStatus();
|
||||
if (reblogging == null) {
|
||||
hideStatusInfo();
|
||||
} else {
|
||||
setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions);
|
||||
String rebloggedByDisplayName = reblogging.getAccount().getName();
|
||||
setRebloggedByDisplayName(rebloggedByDisplayName,
|
||||
reblogging.getAccount().getEmojis(), statusDisplayOptions);
|
||||
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
|
||||
}
|
||||
|
||||
|
@ -76,13 +82,13 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
private void setRebloggedByDisplayName(final CharSequence name,
|
||||
final StatusViewData.Concrete status,
|
||||
final List<Emoji> accountEmoji,
|
||||
final StatusDisplayOptions statusDisplayOptions) {
|
||||
Context context = statusInfo.getContext();
|
||||
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
||||
CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName);
|
||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||
boostedText, status.getRebloggedByAccountEmojis(), statusInfo, statusDisplayOptions.animateEmojis()
|
||||
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
statusInfo.setText(emojifiedText);
|
||||
statusInfo.setVisibility(View.VISIBLE);
|
||||
|
|
|
@ -43,7 +43,8 @@ interface ItemInteractionListener {
|
|||
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 listener: ItemInteractionListener,
|
||||
private var removeButtonEnabled: Boolean = false
|
||||
|
@ -77,7 +78,6 @@ class TabAdapter(private var data: List<TabData>,
|
|||
binding.textView.setOnClickListener {
|
||||
listener.onTabAdded(tab)
|
||||
}
|
||||
|
||||
} else {
|
||||
val binding = holder.binding as ItemTabPreferenceBinding
|
||||
|
||||
|
@ -125,7 +125,7 @@ class TabAdapter(private var data: List<TabData>,
|
|||
|
||||
chip.text = arg
|
||||
|
||||
if(tab.arguments.size <= 1) {
|
||||
if (tab.arguments.size <= 1) {
|
||||
chip.chipIcon = null
|
||||
chip.setOnClickListener(null)
|
||||
} else {
|
||||
|
@ -136,14 +136,13 @@ class TabAdapter(private var data: List<TabData>,
|
|||
}
|
||||
}
|
||||
|
||||
while(binding.chipGroup.size - 1 > tab.arguments.size) {
|
||||
while (binding.chipGroup.size - 1 > tab.arguments.size) {
|
||||
binding.chipGroup.removeViewAt(tab.arguments.size)
|
||||
}
|
||||
|
||||
binding.actionChip.setOnClickListener {
|
||||
listener.onActionChipClicked(tab, holder.bindingAdapterPosition)
|
||||
}
|
||||
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -3,9 +3,9 @@ package com.keylesspalace.tusky.appstore
|
|||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
class CacheUpdater @Inject constructor(
|
||||
|
@ -27,7 +27,7 @@ class CacheUpdater @Inject constructor(
|
|||
is ReblogEvent ->
|
||||
timelineDao.setReblogged(accountId, event.statusId, event.reblog)
|
||||
is BookmarkEvent ->
|
||||
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark )
|
||||
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark)
|
||||
is UnfollowEvent ->
|
||||
timelineDao.removeAllByUser(accountId, event.accountId)
|
||||
is StatusDeletedEvent ->
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package com.keylesspalace.tusky.appstore
|
||||
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
|
||||
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
|
||||
data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable
|
||||
|
@ -20,7 +20,8 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
|||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
||||
data class AnnouncementReadEvent(val announcementId: String): Dispatchable
|
||||
data class DomainMuteEvent(val instance: String) : Dispatchable
|
||||
data class AnnouncementReadEvent(val announcementId: String) : Dispatchable
|
||||
data class PinEvent(val statusId: String, val pinned: Boolean) : Dispatchable
|
||||
data class QuickReplyEvent(val status: Status) : Dispatchable
|
||||
data class StreamUpdateEvent(val status: Status, val targetKind: TimelineFragment.Kind, val targetIdentifier: String?, val first: Boolean) : Dispatchable
|
||||
data class StreamUpdateEvent(val status: Status, val targetKind: TimelineViewModel.Kind, val targetIdentifier: String?) : Dispatchable
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package com.keylesspalace.tusky.appstore
|
||||
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
|
||||
interface Event
|
||||
interface Dispatchable : Event
|
||||
|
|
|
@ -31,7 +31,7 @@ import com.keylesspalace.tusky.util.BindingHolder
|
|||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
|
||||
interface AnnouncementActionListener: LinkListener {
|
||||
interface AnnouncementActionListener : LinkListener {
|
||||
fun openReactionPicker(announcementId: String, target: View)
|
||||
fun addReaction(announcementId: String, name: String)
|
||||
fun removeReaction(announcementId: String, name: String)
|
||||
|
@ -67,12 +67,12 @@ class AnnouncementAdapter(
|
|||
}
|
||||
|
||||
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 {
|
||||
isCheckable = true
|
||||
checkedIcon = null
|
||||
chips.addView(this, i)
|
||||
})
|
||||
}
|
||||
.apply {
|
||||
val emojiText = if (reaction.url == null) {
|
||||
reaction.name
|
||||
|
@ -81,12 +81,14 @@ class AnnouncementAdapter(
|
|||
}
|
||||
this.text = ("$emojiText ${reaction.count}")
|
||||
.emojify(
|
||||
listOf(Emoji(
|
||||
listOf(
|
||||
Emoji(
|
||||
reaction.name,
|
||||
reaction.url ?: "",
|
||||
reaction.staticUrl ?: "",
|
||||
null
|
||||
)),
|
||||
)
|
||||
),
|
||||
this,
|
||||
animateEmojis
|
||||
)
|
||||
|
|
|
@ -34,7 +34,12 @@ import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
|||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
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 javax.inject.Inject
|
||||
|
||||
|
|
|
@ -27,8 +27,13 @@ import com.keylesspalace.tusky.entity.Announcement
|
|||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.rxkotlin.Singles
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
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
|
||||
|
||||
class AnnouncementsViewModel @Inject constructor(
|
||||
|
@ -45,15 +50,15 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
val emojis: LiveData<List<Emoji>> = emojisMutable
|
||||
|
||||
init {
|
||||
Singles.zip(
|
||||
Single.zip(
|
||||
mastodonApi.getCustomEmojis(),
|
||||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||
.onErrorResumeNext(
|
||||
.onErrorResumeNext {
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right(it) }
|
||||
)
|
||||
) { emojis, either ->
|
||||
},
|
||||
{ emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
?: InstanceEntity(
|
||||
accountManager.activeAccount?.domain!!,
|
||||
|
@ -64,21 +69,26 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
either.asRight().version
|
||||
)
|
||||
}
|
||||
)
|
||||
.doOnSuccess {
|
||||
appDatabase.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
.subscribe({
|
||||
emojisMutable.postValue(it.emojiList)
|
||||
}, {
|
||||
.subscribe(
|
||||
{
|
||||
emojisMutable.postValue(it.emojiList.orEmpty())
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to get custom emojis.", it)
|
||||
})
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun load() {
|
||||
announcementsMutable.postValue(Loading())
|
||||
mastodonApi.listAnnouncements()
|
||||
.subscribe({
|
||||
.subscribe(
|
||||
{
|
||||
announcementsMutable.postValue(Success(it))
|
||||
it.filter { announcement -> !announcement.read }
|
||||
.forEach { announcement ->
|
||||
|
@ -93,15 +103,18 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
announcementsMutable.postValue(Error(cause = it))
|
||||
})
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun addReaction(announcementId: String, name: String) {
|
||||
mastodonApi.addAnnouncementReaction(announcementId, name)
|
||||
.subscribe({
|
||||
.subscribe(
|
||||
{
|
||||
announcementsMutable.postValue(
|
||||
Success(
|
||||
announcements.value!!.data!!.map { announcement ->
|
||||
|
@ -140,15 +153,18 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
}
|
||||
)
|
||||
)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
||||
})
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun removeReaction(announcementId: String, name: String) {
|
||||
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
||||
.subscribe({
|
||||
.subscribe(
|
||||
{
|
||||
announcementsMutable.postValue(
|
||||
Success(
|
||||
announcements.value!!.data!!.map { announcement ->
|
||||
|
@ -175,9 +191,11 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
}
|
||||
)
|
||||
)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
||||
})
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -25,18 +24,20 @@ import android.content.pm.PackageManager
|
|||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
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.annotation.ColorInt
|
||||
import androidx.annotation.StringRes
|
||||
|
@ -84,12 +85,13 @@ import com.mikepenz.iconics.utils.sizeDp
|
|||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class ComposeActivity : BaseActivity(),
|
||||
class ComposeActivity :
|
||||
BaseActivity(),
|
||||
ComposeOptionsListener,
|
||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||
OnEmojiSelectedListener,
|
||||
|
@ -121,6 +123,21 @@ class ComposeActivity : BaseActivity(),
|
|||
private val maxUploadMediaNumber = 4
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -168,11 +185,7 @@ class ComposeActivity : BaseActivity(),
|
|||
binding.composeEditField.setText(tootText)
|
||||
}
|
||||
|
||||
if (loadInstanceData(preferences, composeOptions?.tootRightNow == true)) {
|
||||
viewModel.loadInstanceDataFromNetwork()
|
||||
} else {
|
||||
viewModel.loadInstanceDataFromCache()
|
||||
}
|
||||
viewModel.loadInstanceDataFromNetwork(loadInstanceData(preferences, composeOptions?.tootRightNow == true))
|
||||
|
||||
if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
|
||||
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
|
||||
|
@ -333,8 +346,9 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
|
||||
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O
|
||||
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O ||
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1
|
||||
) {
|
||||
binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||
}
|
||||
}
|
||||
|
@ -375,9 +389,9 @@ class ComposeActivity : BaseActivity(),
|
|||
updateScheduleButton()
|
||||
}
|
||||
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
|
||||
val active = poll == null
|
||||
&& media!!.size != 4
|
||||
&& (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
||||
val active = poll == null &&
|
||||
media!!.size != 4 &&
|
||||
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
||||
enableButton(binding.composeAddMediaButton, active, active)
|
||||
enablePollButton(media.isNullOrEmpty())
|
||||
}.subscribe()
|
||||
|
@ -457,7 +471,6 @@ class ComposeActivity : BaseActivity(),
|
|||
setDisplayShowHomeEnabled(true)
|
||||
setHomeAsUpIndicator(R.drawable.ic_close_24dp)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) {
|
||||
|
@ -473,8 +486,10 @@ class ComposeActivity : BaseActivity(),
|
|||
avatarSize / 8,
|
||||
animateAvatars
|
||||
)
|
||||
binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description,
|
||||
activeAccount.fullName)
|
||||
binding.composeAvatar.contentDescription = getString(
|
||||
R.string.compose_active_account_description,
|
||||
activeAccount.fullName
|
||||
)
|
||||
}
|
||||
|
||||
private fun replaceTextAtCaret(text: CharSequence) {
|
||||
|
@ -532,7 +547,6 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun atButtonClicked() {
|
||||
prependSelectedWordsWith("@")
|
||||
}
|
||||
|
@ -548,7 +562,7 @@ class ComposeActivity : BaseActivity(),
|
|||
|
||||
private fun displayTransientError(@StringRes stringId: Int) {
|
||||
val bar = Snackbar.make(binding.activityCompose, stringId, Snackbar.LENGTH_LONG)
|
||||
//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)
|
||||
bar.show()
|
||||
}
|
||||
|
@ -566,7 +580,6 @@ class ComposeActivity : BaseActivity(),
|
|||
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
||||
binding.composeHideMediaButton.isClickable = false
|
||||
ContextCompat.getColor(this, R.color.transparent_tusky_blue)
|
||||
|
||||
} else {
|
||||
binding.composeHideMediaButton.isClickable = true
|
||||
if (markMediaSensitive) {
|
||||
|
@ -676,15 +689,17 @@ class ComposeActivity : BaseActivity(),
|
|||
private fun onMediaPick() {
|
||||
addMediaBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
//Wait until bottom sheet is not collapsed and show next screen after
|
||||
// Wait until bottom sheet is not collapsed and show next screen after
|
||||
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
addMediaBehavior.removeBottomSheetCallback(this)
|
||||
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),
|
||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
|
||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
|
||||
)
|
||||
} else {
|
||||
initiateMediaPicking()
|
||||
pickMediaFile.launch(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -698,8 +713,10 @@ class ComposeActivity : BaseActivity(),
|
|||
private fun openPollDialog() {
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
val instanceParams = viewModel.instanceParams.value!!
|
||||
showAddPollDialog(this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
||||
instanceParams.pollMaxLength, viewModel::updatePoll)
|
||||
showAddPollDialog(
|
||||
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
||||
instanceParams.pollMaxLength, viewModel::updatePoll
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupPollView() {
|
||||
|
@ -827,34 +844,39 @@ class ComposeActivity : BaseActivity(),
|
|||
if (viewModel.media.value!!.isNotEmpty()) {
|
||||
finishingUploadDialog = ProgressDialog.show(
|
||||
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()
|
||||
deleteDraftAndFinish()
|
||||
})
|
||||
|
||||
}
|
||||
)
|
||||
} else {
|
||||
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||
enableButtons(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
initiateMediaPicking()
|
||||
pickMediaFile.launch(true)
|
||||
} else {
|
||||
val bar = Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission,
|
||||
Snackbar.LENGTH_SHORT).apply {
|
||||
|
||||
Snackbar.make(
|
||||
binding.activityCompose, R.string.error_media_upload_permission,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).apply {
|
||||
setAction(R.string.action_retry) { onMediaPick() }
|
||||
// necessary so snackbar is shown over everything
|
||||
view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||
show()
|
||||
}
|
||||
bar.setAction(R.string.action_retry) { onMediaPick() }
|
||||
//necessary so snackbar is shown over everything
|
||||
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||
bar.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -862,11 +884,6 @@ class ComposeActivity : BaseActivity(),
|
|||
private fun initiateCameraApp() {
|
||||
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 {
|
||||
createNewImageFile(this)
|
||||
} catch (ex: IOException) {
|
||||
|
@ -875,37 +892,30 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
|
||||
// Continue only if the File was successfully created
|
||||
photoUploadUri = FileProvider.getUriForFile(this,
|
||||
photoUploadUri = FileProvider.getUriForFile(
|
||||
this,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
photoFile)
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri)
|
||||
startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
photoFile
|
||||
)
|
||||
takePicture.launch(photoUploadUri)
|
||||
}
|
||||
|
||||
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
||||
button.isEnabled = clickable
|
||||
ThemeUtils.setDrawableTint(this, button.drawable,
|
||||
ThemeUtils.setDrawableTint(
|
||||
this, button.drawable,
|
||||
if (colorActive) android.R.attr.textColorTertiary
|
||||
else R.attr.textColorDisabled)
|
||||
else R.attr.textColorDisabled
|
||||
)
|
||||
}
|
||||
|
||||
private fun enablePollButton(enable: Boolean) {
|
||||
binding.addPollTextActionTextView.isEnabled = enable
|
||||
val textColor = ThemeUtils.getColor(this,
|
||||
val textColor = ThemeUtils.getColor(
|
||||
this,
|
||||
if (enable) android.R.attr.textColorTertiary
|
||||
else R.attr.textColorDisabled)
|
||||
else R.attr.textColorDisabled
|
||||
)
|
||||
binding.addPollTextActionTextView.setTextColor(textColor)
|
||||
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
@ -914,31 +924,6 @@ class ComposeActivity : BaseActivity(),
|
|||
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) {
|
||||
withLifecycleContext {
|
||||
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
||||
|
@ -962,7 +947,6 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
displayTransientError(errorId)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -996,7 +980,8 @@ class ComposeActivity : BaseActivity(),
|
|||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||
) {
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
|
@ -1099,7 +1084,6 @@ class ComposeActivity : BaseActivity(),
|
|||
data class ComposeOptions(
|
||||
// Let's keep fields var until all consumers are Kotlin
|
||||
var scheduledTootId: String? = null,
|
||||
var savedTootUid: Int? = null,
|
||||
var draftId: Int? = null,
|
||||
var tootText: String? = null,
|
||||
var mediaUrls: List<String>? = null,
|
||||
|
@ -1125,8 +1109,6 @@ class ComposeActivity : BaseActivity(),
|
|||
|
||||
companion object {
|
||||
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
|
||||
|
||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||
|
|
|
@ -21,22 +21,34 @@ import androidx.core.net.toUri
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
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.network.MastodonApi
|
||||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.keylesspalace.tusky.service.TootToSend
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Observable.just
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxkotlin.Singles
|
||||
import java.util.*
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.VersionUtils
|
||||
import com.keylesspalace.tusky.util.combineLiveData
|
||||
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
|
||||
|
||||
class ComposeViewModel @Inject constructor(
|
||||
|
@ -45,14 +57,12 @@ class ComposeViewModel @Inject constructor(
|
|||
private val mediaUploader: MediaUploader,
|
||||
private val serviceClient: ServiceClient,
|
||||
private val draftHelper: DraftHelper,
|
||||
private val saveTootHelper: SaveTootHelper,
|
||||
private val db: AppDatabase
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
private var replyingStatusAuthor: String? = null
|
||||
private var replyingStatusContent: String? = null
|
||||
internal var startingText: String? = null
|
||||
private var savedTootUid: Int = 0
|
||||
private var draftId: Int = 0
|
||||
private var scheduledTootId: String? = null
|
||||
private var startingContentWarning: String = ""
|
||||
|
@ -94,9 +104,11 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
||||
|
||||
fun loadInstanceDataFromNetwork() {
|
||||
|
||||
Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance ->
|
||||
fun loadInstanceDataFromNetwork(loadActually: Boolean) {
|
||||
when (loadActually) {
|
||||
true -> Single.zip(
|
||||
api.getCustomEmojis(), api.getInstance(),
|
||||
{ emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = domain,
|
||||
emojiList = emojis,
|
||||
|
@ -106,31 +118,25 @@ class ComposeViewModel @Inject constructor(
|
|||
version = instance.version
|
||||
)
|
||||
}
|
||||
)
|
||||
false -> Single.error(Exception("skipped network access"))
|
||||
}
|
||||
.doOnSuccess {
|
||||
db.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
.onErrorResumeNext(
|
||||
.onErrorResumeNext {
|
||||
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
)
|
||||
.subscribe ({ instanceEntity ->
|
||||
emoji.postValue(instanceEntity.emojiList)
|
||||
instance.postValue(instanceEntity)
|
||||
}, { throwable ->
|
||||
// this can happen on network error when no cached data is available
|
||||
Log.w(TAG, "error loading instance data", throwable)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun loadInstanceDataFromCache() {
|
||||
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.subscribe ({ instanceEntity ->
|
||||
.subscribe(
|
||||
{ instanceEntity ->
|
||||
emoji.postValue(instanceEntity.emojiList)
|
||||
instance.postValue(instanceEntity)
|
||||
}, { throwable ->
|
||||
},
|
||||
{ throwable ->
|
||||
// this can happen on network error when no cached data is available
|
||||
Log.w(TAG, "error loading instance data", throwable)
|
||||
})
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
|
@ -141,19 +147,23 @@ class ComposeViewModel @Inject constructor(
|
|||
mediaUploader.prepareMedia(uri)
|
||||
.map { (type, uri, size) ->
|
||||
val mediaItems = media.value!!
|
||||
if (type != QueuedMedia.Type.IMAGE
|
||||
&& mediaItems.isNotEmpty()
|
||||
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
|
||||
if (type != QueuedMedia.Type.IMAGE &&
|
||||
mediaItems.isNotEmpty() &&
|
||||
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
||||
) {
|
||||
throw VideoOrImageException()
|
||||
} else {
|
||||
addMediaToQueue(type, uri, size, description)
|
||||
}
|
||||
}
|
||||
.subscribe({ queuedMedia ->
|
||||
.subscribe(
|
||||
{ queuedMedia ->
|
||||
liveData.postValue(Either.Right(queuedMedia))
|
||||
}, { error ->
|
||||
},
|
||||
{ error ->
|
||||
liveData.postValue(Either.Left(error))
|
||||
})
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
return liveData
|
||||
}
|
||||
|
@ -174,7 +184,8 @@ class ComposeViewModel @Inject constructor(
|
|||
media.value = media.value!! + mediaItem
|
||||
mediaToDisposable[mediaItem.localId] = mediaUploader
|
||||
.uploadMedia(mediaItem)
|
||||
.subscribe({ event ->
|
||||
.subscribe(
|
||||
{ event ->
|
||||
val item = media.value?.find { it.localId == mediaItem.localId }
|
||||
?: return@subscribe
|
||||
val newMediaItem = when (event) {
|
||||
|
@ -186,16 +197,20 @@ class ComposeViewModel @Inject constructor(
|
|||
synchronized(media) {
|
||||
val mediaValue = media.value!!
|
||||
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
|
||||
media.postValue(if (index == -1) {
|
||||
media.postValue(
|
||||
if (index == -1) {
|
||||
mediaValue + newMediaItem
|
||||
} else {
|
||||
mediaValue.toMutableList().also { it[index] = newMediaItem }
|
||||
})
|
||||
}
|
||||
}, { error ->
|
||||
)
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
|
||||
uploadError.postValue(error)
|
||||
})
|
||||
}
|
||||
)
|
||||
return mediaItem
|
||||
}
|
||||
|
||||
|
@ -215,12 +230,14 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
fun didChange(content: String?, contentWarning: String?): Boolean {
|
||||
|
||||
val textChanged = !(content.isNullOrEmpty()
|
||||
|| startingText?.startsWith(content.toString()) ?: false)
|
||||
val textChanged = !(
|
||||
content.isNullOrEmpty() ||
|
||||
startingText?.startsWith(content.toString()) ?: false
|
||||
)
|
||||
|
||||
val contentWarningChanged = showContentWarning.value!!
|
||||
&& !contentWarning.isNullOrEmpty()
|
||||
&& !startingContentWarning.startsWith(contentWarning.toString())
|
||||
val contentWarningChanged = showContentWarning.value!! &&
|
||||
!contentWarning.isNullOrEmpty() &&
|
||||
!startingContentWarning.startsWith(contentWarning.toString())
|
||||
val mediaChanged = !media.value.isNullOrEmpty()
|
||||
val pollChanged = poll.value != null
|
||||
|
||||
|
@ -233,17 +250,15 @@ class ComposeViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun deleteDraft() {
|
||||
if (savedTootUid != 0) {
|
||||
saveTootHelper.deleteDraft(savedTootUid)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
if (draftId != 0) {
|
||||
draftHelper.deleteDraftAndAttachments(draftId)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDraft(content: String, contentWarning: String) {
|
||||
|
||||
viewModelScope.launch {
|
||||
val mediaUris: MutableList<String> = mutableListOf()
|
||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||
media.value?.forEach { item ->
|
||||
|
@ -263,7 +278,8 @@ class ComposeViewModel @Inject constructor(
|
|||
mediaDescriptions = mediaDescriptions,
|
||||
poll = poll.value,
|
||||
failedToSend = false
|
||||
).subscribe()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,7 +295,7 @@ class ComposeViewModel @Inject constructor(
|
|||
val deletionObservable = if (isEditingScheduledToot) {
|
||||
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
||||
} else {
|
||||
just(Unit)
|
||||
Observable.just(Unit)
|
||||
}.toLiveData()
|
||||
|
||||
val sendObservable = media
|
||||
|
@ -309,7 +325,6 @@ class ComposeViewModel @Inject constructor(
|
|||
replyingStatusAuthorUsername = null,
|
||||
quoteId = quoteId,
|
||||
accountId = accountManager.activeAccount!!.id,
|
||||
savedTootUid = savedTootUid,
|
||||
draftId = draftId,
|
||||
idempotencyKey = randomAlphanumericString(16),
|
||||
retries = 0
|
||||
|
@ -336,11 +351,14 @@ class ComposeViewModel @Inject constructor(
|
|||
media.removeObserver(this)
|
||||
} else if (updatedItem.id != null) {
|
||||
api.updateMedia(updatedItem.id, description)
|
||||
.subscribe({
|
||||
.subscribe(
|
||||
{
|
||||
completedCaptioningLiveData.postValue(true)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
completedCaptioningLiveData.postValue(false)
|
||||
})
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
media.removeObserver(this)
|
||||
}
|
||||
|
@ -375,11 +393,11 @@ class ComposeViewModel @Inject constructor(
|
|||
':' -> {
|
||||
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 resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||
for (emoji in emojiList) {
|
||||
val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT)
|
||||
val shortcode = emoji.shortcode.lowercase(Locale.ROOT)
|
||||
if (shortcode.startsWith(incomplete)) {
|
||||
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
||||
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
||||
|
@ -409,7 +427,8 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
||||
startingVisibility = Status.Visibility.byNum(
|
||||
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
|
||||
preferredVisibility.num.coerceAtLeast(replyVisibility.num)
|
||||
)
|
||||
|
||||
inReplyToId = composeOptions?.inReplyToId
|
||||
|
||||
|
@ -428,20 +447,8 @@ class ComposeViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
// recreate media list
|
||||
val loadedDraftMediaUris = composeOptions?.mediaUrls
|
||||
val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
|
||||
val draftAttachments = composeOptions?.draftAttachments
|
||||
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != 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) {
|
||||
if (draftAttachments != null) {
|
||||
// when coming from DraftActivity
|
||||
draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
|
||||
} else composeOptions?.mediaAttachments?.forEach { a ->
|
||||
|
@ -454,7 +461,6 @@ class ComposeViewModel @Inject constructor(
|
|||
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
||||
}
|
||||
|
||||
savedTootUid = composeOptions?.savedTootUid ?: 0
|
||||
draftId = composeOptions?.draftId ?: 0
|
||||
scheduledTootId = composeOptions?.scheduledTootId
|
||||
startingText = composeOptions?.tootText
|
||||
|
@ -508,7 +514,6 @@ class ComposeViewModel @Inject constructor(
|
|||
private companion object {
|
||||
const val TAG = "ComposeViewModel"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
|
||||
|
|
|
@ -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 {
|
||||
return oldItem.localId == newItem.localId
|
||||
}
|
||||
|
@ -89,10 +91,11 @@ class MediaPreviewAdapter(
|
|||
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
inner class PreviewViewHolder(val progressImageView: ProgressImageView)
|
||||
: RecyclerView.ViewHolder(progressImageView) {
|
||||
inner class PreviewViewHolder(val progressImageView: ProgressImageView) :
|
||||
RecyclerView.ViewHolder(progressImageView) {
|
||||
init {
|
||||
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
||||
val margin = itemView.context.resources
|
||||
|
|
|
@ -28,16 +28,19 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
|||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||
import com.keylesspalace.tusky.util.getImageSquarePixels
|
||||
import com.keylesspalace.tusky.util.getMediaSize
|
||||
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.MultipartBody
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
sealed class UploadEvent {
|
||||
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
||||
|
@ -101,12 +104,13 @@ class MediaUploaderImpl(
|
|||
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
||||
FileOutputStream(file.absoluteFile).use { out ->
|
||||
input.copyTo(out)
|
||||
uri = FileProvider.getUriForFile(context,
|
||||
uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
file)
|
||||
file
|
||||
)
|
||||
mediaSize = getMediaSize(contentResolver, uri)
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
|
@ -151,20 +155,22 @@ class MediaUploaderImpl(
|
|||
var mimeType = contentResolver.getType(media.uri)
|
||||
val map = MimeTypeMap.getSingleton()
|
||||
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),
|
||||
Date().time.toString(),
|
||||
randomAlphanumericString(10),
|
||||
fileExtension)
|
||||
fileExtension
|
||||
)
|
||||
|
||||
val stream = contentResolver.openInputStream(media.uri)
|
||||
|
||||
if (mimeType == null) mimeType = "multipart/form-data"
|
||||
|
||||
|
||||
var lastProgress = -1
|
||||
val fileBody = ProgressRequestBody(stream, media.mediaSize,
|
||||
mimeType.toMediaTypeOrNull()) { percentage ->
|
||||
val fileBody = ProgressRequestBody(
|
||||
stream, media.mediaSize,
|
||||
mimeType.toMediaTypeOrNull()
|
||||
) { percentage ->
|
||||
if (percentage != lastProgress) {
|
||||
emitter.onNext(UploadEvent.ProgressEvent(percentage))
|
||||
}
|
||||
|
@ -180,7 +186,8 @@ class MediaUploaderImpl(
|
|||
}
|
||||
|
||||
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
||||
.subscribe({ attachment ->
|
||||
.subscribe(
|
||||
{ attachment ->
|
||||
if (media.uri.scheme == "file") {
|
||||
media.uri.path?.let {
|
||||
File(it).delete()
|
||||
|
@ -189,9 +196,11 @@ class MediaUploaderImpl(
|
|||
|
||||
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
||||
emitter.onComplete()
|
||||
}, { e ->
|
||||
},
|
||||
{ e ->
|
||||
emitter.onError(e)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Cancel the request when our observable is cancelled
|
||||
emitter.setDisposable(uploadDisposable)
|
||||
|
@ -200,15 +209,16 @@ class MediaUploaderImpl(
|
|||
|
||||
private fun downsize(media: QueuedMedia): QueuedMedia {
|
||||
val file = createNewImageFile(context)
|
||||
DownsizeImageTask.resize(arrayOf(media.uri),
|
||||
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file)
|
||||
DownsizeImageTask.resize(
|
||||
arrayOf(media.uri),
|
||||
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file
|
||||
)
|
||||
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
||||
}
|
||||
|
||||
private fun shouldResizeMedia(media: QueuedMedia): Boolean {
|
||||
return media.type == QueuedMedia.Type.IMAGE
|
||||
&& (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT
|
||||
|| getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
||||
return media.type == QueuedMedia.Type.IMAGE &&
|
||||
(media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
@ -217,6 +227,5 @@ class MediaUploaderImpl(
|
|||
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
|
||||
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
|
||||
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,11 +82,13 @@ fun showAddPollDialog(
|
|||
val pollDuration = context.resources
|
||||
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
||||
|
||||
onUpdatePoll(NewPoll(
|
||||
onUpdatePoll(
|
||||
NewPoll(
|
||||
options = adapter.pollOptions,
|
||||
expiresIn = pollDuration,
|
||||
multiple = binding.multipleChoicesCheckBox.isChecked
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class AddPollOptionsAdapter(
|
|||
private val maxOptionLength: Int,
|
||||
private val onOptionRemoved: (Boolean) -> Unit,
|
||||
private val onOptionChanged: (Boolean) -> Unit
|
||||
): RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() {
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() {
|
||||
|
||||
val pollOptions: List<String>
|
||||
get() = options.toList()
|
||||
|
@ -47,8 +47,8 @@ class AddPollOptionsAdapter(
|
|||
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
|
||||
|
||||
binding.optionEditText.onTextChanged { s, _, _, _ ->
|
||||
val pos = holder.adapterPosition
|
||||
if(pos != RecyclerView.NO_POSITION) {
|
||||
val pos = holder.bindingAdapterPosition
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
options[pos] = s.toString()
|
||||
onOptionChanged(validateInput())
|
||||
}
|
||||
|
@ -68,8 +68,8 @@ class AddPollOptionsAdapter(
|
|||
|
||||
holder.binding.deleteButton.setOnClickListener {
|
||||
holder.binding.optionEditText.clearFocus()
|
||||
options.removeAt(holder.adapterPosition)
|
||||
notifyItemRemoved(holder.adapterPosition)
|
||||
options.removeAt(holder.bindingAdapterPosition)
|
||||
notifyItemRemoved(holder.bindingAdapterPosition)
|
||||
onOptionRemoved(validateInput())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.graphics.drawable.Drawable
|
|||
import android.net.Uri
|
||||
import android.text.InputFilter
|
||||
import android.text.InputType
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
|
@ -31,6 +30,7 @@ import androidx.lifecycle.LifecycleOwner
|
|||
import androidx.lifecycle.LiveData
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
|
@ -40,7 +40,8 @@ import com.keylesspalace.tusky.util.withLifecycleContext
|
|||
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
|
||||
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
||||
|
||||
fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||
fun <T> T.makeCaptionDialog(
|
||||
existingDescription: String?,
|
||||
previewUri: Uri,
|
||||
onUpdateDescription: (String) -> LiveData<Boolean>
|
||||
) where T : Activity, T : LifecycleOwner {
|
||||
|
@ -53,9 +54,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
maximumScale = 6f
|
||||
}
|
||||
|
||||
val displayMetrics = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
|
||||
val margin = Utils.dpToPx(this, 4)
|
||||
dialogLayout.addView(imageView)
|
||||
(imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f
|
||||
|
@ -63,14 +61,18 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
|
||||
|
||||
val input = EditText(this)
|
||||
input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired,
|
||||
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT)
|
||||
input.hint = resources.getQuantityString(
|
||||
R.plurals.hint_describe_for_visually_impaired,
|
||||
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT
|
||||
)
|
||||
dialogLayout.addView(input)
|
||||
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
|
||||
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_CAP_SENTENCES)
|
||||
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
)
|
||||
input.setText(existingDescription)
|
||||
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
||||
|
||||
|
@ -79,7 +81,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
withLifecycleContext {
|
||||
onUpdateDescription(input.text.toString())
|
||||
.observe { success -> if (!success) showFailedCaptionMessage() }
|
||||
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
|
@ -93,16 +94,19 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
|
||||
val window = dialog.window
|
||||
window?.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
|
||||
// Load the image and manually set it into the ImageView because it doesn't have a fixed
|
||||
// size. Maybe we should limit the size of CustomTarget
|
||||
// Load the image and manually set it into the ImageView because it doesn't have a fixed size.
|
||||
Glide.with(this)
|
||||
.load(previewUri)
|
||||
.into(object : CustomTarget<Drawable>() {
|
||||
override fun onLoadCleared(placeholder: Drawable?) {}
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.into(object : CustomTarget<Drawable>(4096, 4096) {
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
imageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
imageView.setImageDrawable(resource)
|
||||
|
@ -110,7 +114,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
private fun Activity.showFailedCaptionMessage() {
|
||||
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
|
@ -71,12 +71,10 @@ class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
R.id.directRadioButton
|
||||
else ->
|
||||
R.id.directRadioButton
|
||||
|
||||
}
|
||||
|
||||
check(selectedButton)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface ComposeOptionsListener {
|
||||
|
|
|
@ -16,25 +16,27 @@
|
|||
package com.keylesspalace.tusky.components.compose.view
|
||||
|
||||
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.method.KeyListener
|
||||
import android.util.AttributeSet
|
||||
import android.view.inputmethod.EditorInfo
|
||||
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,
|
||||
attributeSet: AttributeSet? = null)
|
||||
: AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||
class EditTextTyped @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attributeSet: AttributeSet? = null
|
||||
) :
|
||||
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||
|
||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
||||
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
||||
|
||||
init {
|
||||
//fix a bug with autocomplete and some keyboards
|
||||
// fix a bug with autocomplete and some keyboards
|
||||
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
||||
inputType = newInputType
|
||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener))
|
||||
|
@ -52,8 +54,13 @@ class EditTextTyped @JvmOverloads constructor(context: Context,
|
|||
val connection = super.onCreateInputConnection(editorInfo)
|
||||
return if (onCommitContentListener != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
getEmojiEditTextHelper().onCreateInputConnection(InputConnectionCompat.createWrapper(connection, editorInfo,
|
||||
onCommitContentListener!!), editorInfo)!!
|
||||
getEmojiEditTextHelper().onCreateInputConnection(
|
||||
InputConnectionCompat.createWrapper(
|
||||
connection, editorInfo,
|
||||
onCommitContentListener!!
|
||||
),
|
||||
editorInfo
|
||||
)!!
|
||||
} else {
|
||||
connection
|
||||
}
|
||||
|
|
|
@ -27,8 +27,9 @@ import com.keylesspalace.tusky.entity.NewPoll
|
|||
class PollPreviewView @JvmOverloads constructor(
|
||||
context: Context?,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0)
|
||||
: LinearLayout(context, attrs, defStyleAttr) {
|
||||
defStyleAttr: Int = 0
|
||||
) :
|
||||
LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val adapter = PreviewPollOptionsAdapter()
|
||||
|
||||
|
@ -46,7 +47,7 @@ class PollPreviewView @JvmOverloads constructor(
|
|||
binding.pollPreviewOptions.adapter = adapter
|
||||
}
|
||||
|
||||
fun setPoll(poll: NewPoll){
|
||||
fun setPoll(poll: NewPoll) {
|
||||
adapter.update(poll.options, poll.multiple)
|
||||
|
||||
val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast {
|
||||
|
|
|
@ -36,7 +36,7 @@ class TootButton
|
|||
private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button)
|
||||
|
||||
init {
|
||||
if(smallStyle) {
|
||||
if (smallStyle) {
|
||||
setIconResource(R.drawable.ic_send_24dp)
|
||||
} else {
|
||||
setText(R.string.action_send)
|
||||
|
@ -47,7 +47,7 @@ class TootButton
|
|||
}
|
||||
|
||||
fun setStatusVisibility(visibility: Status.Visibility) {
|
||||
if(!smallStyle) {
|
||||
if (!smallStyle) {
|
||||
|
||||
icon = when (visibility) {
|
||||
Status.Visibility.PUBLIC -> {
|
||||
|
@ -69,8 +69,5 @@ class TootButton
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -17,114 +17,39 @@ package com.keylesspalace.tusky.components.conversation
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.AsyncPagedListDiffer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
|
||||
class ConversationAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val listener: StatusActionListener,
|
||||
private val topLoadedCallback: () -> Unit,
|
||||
private val retryCallback: () -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private val listener: StatusActionListener
|
||||
) : PagingDataAdapter<ConversationEntity, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
||||
|
||||
private var networkState: NetworkState? = null
|
||||
|
||||
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object : ListUpdateCallback {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position, count)
|
||||
if (position == 0) {
|
||||
topLoadedCallback()
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
||||
return ConversationViewHolder(view, statusDisplayOptions, listener)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position, count)
|
||||
override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) {
|
||||
holder.setupWithConversation(getItem(position))
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
fun item(position: Int): ConversationEntity? {
|
||||
return getItem(position)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationEntity>() {
|
||||
override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean =
|
||||
oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
||||
return 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.
|
||||
*
|
||||
|
@ -21,11 +21,16 @@ import androidx.room.Embedded
|
|||
import androidx.room.Entity
|
||||
import androidx.room.TypeConverters
|
||||
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 java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@Entity(primaryKeys = ["id","accountId"])
|
||||
@Entity(primaryKeys = ["id", "accountId"])
|
||||
@TypeConverters(Converters::class)
|
||||
data class ConversationEntity(
|
||||
val accountId: Long,
|
||||
|
@ -73,13 +78,13 @@ data class ConversationStatusEntity(
|
|||
val sensitive: Boolean,
|
||||
val spoilerText: String,
|
||||
val attachments: ArrayList<Attachment>,
|
||||
val mentions: Array<Status.Mention>,
|
||||
val mentions: List<Status.Mention>,
|
||||
val showingHiddenContent: Boolean,
|
||||
val expanded: Boolean,
|
||||
val collapsible: Boolean,
|
||||
val collapsed: Boolean,
|
||||
val muted: Boolean,
|
||||
val poll: Poll?
|
||||
|
||||
) {
|
||||
/** its necessary to override this because Spanned.equals does not work as expected */
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
@ -93,7 +98,7 @@ data class ConversationStatusEntity(
|
|||
if (inReplyToId != other.inReplyToId) return false
|
||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||
if (account != other.account) return false
|
||||
if (content.toString() != other.content.toString()) return false //TODO find a better method to compare two spanned strings
|
||||
if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (emojis != other.emojis) return false
|
||||
if (favouritesCount != other.favouritesCount) return false
|
||||
|
@ -101,11 +106,12 @@ data class ConversationStatusEntity(
|
|||
if (sensitive != other.sensitive) return false
|
||||
if (spoilerText != other.spoilerText) 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 (expanded != other.expanded) return false
|
||||
if (collapsible != other.collapsible) return false
|
||||
if (collapsed != other.collapsed) return false
|
||||
if (muted != other.muted) return false
|
||||
if (poll != other.poll) return false
|
||||
|
||||
return true
|
||||
|
@ -125,11 +131,12 @@ data class ConversationStatusEntity(
|
|||
result = 31 * result + sensitive.hashCode()
|
||||
result = 31 * result + spoilerText.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + mentions.contentHashCode()
|
||||
result = 31 * result + mentions.hashCode()
|
||||
result = 31 * result + showingHiddenContent.hashCode()
|
||||
result = 31 * result + expanded.hashCode()
|
||||
result = 31 * result + collapsible.hashCode()
|
||||
result = 31 * result + collapsed.hashCode()
|
||||
result = 31 * result + muted.hashCode()
|
||||
result = 31 * result + poll.hashCode()
|
||||
return result
|
||||
}
|
||||
|
@ -150,47 +157,60 @@ data class ConversationStatusEntity(
|
|||
reblogged = false,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive= sensitive,
|
||||
sensitive = sensitive,
|
||||
spoilerText = spoilerText,
|
||||
visibility = Status.Visibility.DIRECT,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
application = null,
|
||||
pinned = false,
|
||||
muted = false,
|
||||
muted = muted,
|
||||
poll = poll,
|
||||
card = null,
|
||||
quote = null)
|
||||
quote = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Account.toEntity() =
|
||||
ConversationAccountEntity(
|
||||
id,
|
||||
username,
|
||||
name,
|
||||
avatar,
|
||||
emojis ?: emptyList()
|
||||
id = id,
|
||||
username = username,
|
||||
displayName = name,
|
||||
avatar = avatar,
|
||||
emojis = emojis ?: emptyList()
|
||||
)
|
||||
|
||||
fun Status.toEntity() =
|
||||
ConversationStatusEntity(
|
||||
id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content,
|
||||
createdAt, emojis, favouritesCount, favourited, bookmarked, sensitive,
|
||||
spoilerText, attachments, mentions,
|
||||
false,
|
||||
false,
|
||||
shouldTrimStatus(content),
|
||||
true,
|
||||
poll
|
||||
id = id,
|
||||
url = url,
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
account = account.toEntity(),
|
||||
content = content,
|
||||
createdAt = createdAt,
|
||||
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) =
|
||||
ConversationEntity(
|
||||
accountId,
|
||||
id,
|
||||
accounts.map { it.toEntity() },
|
||||
unread,
|
||||
lastStatus!!.toEntity()
|
||||
accountId = accountId,
|
||||
id = id,
|
||||
accounts = accounts.map { it.toEntity() },
|
||||
unread = unread,
|
||||
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.
|
||||
*
|
||||
|
@ -20,7 +20,12 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadState
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -37,10 +42,15 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
|||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
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
|
||||
|
||||
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
|
||||
|
@ -53,13 +63,17 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
||||
|
||||
private lateinit var adapter: ConversationAdapter
|
||||
private lateinit var loadStateAdapter: ConversationLoadStateAdapter
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
|
||||
private var initialRefreshDone: Boolean = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||
|
||||
|
@ -73,15 +87,16 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
|
||||
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
||||
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
|
||||
quoteEnabled = accountManager.activeAccount?.domain in CAN_USE_QUOTE_ID
|
||||
quoteEnabled = accountManager.activeAccount?.domain in CAN_USE_QUOTE_ID,
|
||||
)
|
||||
|
||||
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)
|
||||
adapter = ConversationAdapter(statusDisplayOptions, this)
|
||||
loadStateAdapter = ConversationLoadStateAdapter(adapter::retry)
|
||||
|
||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.adapter = adapter.withLoadStateFooter(loadStateAdapter)
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
binding.progressBar.hide()
|
||||
|
@ -89,37 +104,60 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
|
||||
initSwipeToRefresh()
|
||||
|
||||
viewModel.conversations.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(it)
|
||||
lifecycleScope.launch {
|
||||
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() {
|
||||
viewModel.refreshState.observe(viewLifecycleOwner) {
|
||||
binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
|
||||
}
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
viewModel.refresh()
|
||||
adapter.refresh()
|
||||
}
|
||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
}
|
||||
|
||||
private fun onTopLoaded() {
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
}
|
||||
|
||||
override fun onReblog(reblog: Boolean, position: Int) {
|
||||
// its impossible to reblog private messages
|
||||
}
|
||||
|
||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||
viewModel.favourite(favourite, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.favourite(favourite, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQuote(position: Int) {
|
||||
|
@ -127,24 +165,44 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
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) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
more(it.toStatus(), view, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
|
||||
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?) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
viewMedia(attachmentIndex, it.toStatus(), view)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
viewThread(it.toStatus())
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewThread(conversation.lastStatus.id, conversation.lastStatus.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,11 +211,15 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
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) {
|
||||
viewModel.showContent(isShowing, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.showContent(isShowing, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadMore(position: Int) {
|
||||
|
@ -165,7 +227,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -180,15 +244,25 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun removeItem(position: Int) {
|
||||
viewModel.remove(position)
|
||||
// not needed
|
||||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
reply(it.toStatus())
|
||||
adapter.item(position)?.let { conversation ->
|
||||
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() {
|
||||
if (isAdded) {
|
||||
layoutManager?.scrollToPosition(0)
|
||||
|
@ -200,12 +274,10 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
jumpToTop()
|
||||
}
|
||||
|
||||
override fun onReset() {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
viewModel.voteInPoll(position, choices)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.voteInPoll(choices, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ExperimentalPagingApi
|
||||
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
|
||||
|
||||
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.entity.Conversation
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.concurrent.Executors
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, val db: AppDatabase) {
|
||||
|
||||
private val ioExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
class ConversationsRepository @Inject constructor(
|
||||
val mastodonApi: MastodonApi,
|
||||
val db: AppDatabase
|
||||
) {
|
||||
|
||||
fun deleteCacheForAccount(accountId: Long) {
|
||||
Single.fromCallable {
|
||||
|
@ -101,11 +34,4 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi,
|
|||
}.subscribeOn(Schedulers.io())
|
||||
.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
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.PagedList
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
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 io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConversationsViewModel @Inject constructor(
|
||||
private val repository: ConversationsRepository,
|
||||
private val timelineCases: TimelineCases,
|
||||
private val database: AppDatabase,
|
||||
private val accountManager: AccountManager
|
||||
private val accountManager: AccountManager,
|
||||
private val api: MastodonApi
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
private val repoResult = MutableLiveData<Listing<ConversationEntity>>()
|
||||
@ExperimentalPagingApi
|
||||
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 }
|
||||
val networkState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkState }
|
||||
val refreshState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
|
||||
fun favourite(favourite: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
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(
|
||||
lastStatus = conversation.lastStatus.copy(favourited = favourite)
|
||||
)
|
||||
|
||||
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(
|
||||
lastStatus = conversation.lastStatus.copy(bookmarked = bookmark)
|
||||
)
|
||||
|
||||
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(position: Int, choices: MutableList<Int>) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices)
|
||||
.flatMap { poll ->
|
||||
fun voteInPoll(choices: List<Int>, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await()
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(poll = poll)
|
||||
)
|
||||
|
||||
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, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(expanded = expanded)
|
||||
)
|
||||
|
@ -109,8 +102,8 @@ class ConversationsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun collapseLongStatus(collapsed: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
|
||||
)
|
||||
|
@ -118,8 +111,8 @@ class ConversationsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun showContent(showing: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
fun showContent(showing: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
|
||||
)
|
||||
|
@ -127,16 +120,42 @@ class ConversationsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun remove(position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let {
|
||||
refresh()
|
||||
fun remove(conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
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)
|
||||
.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.Status
|
||||
import com.keylesspalace.tusky.util.IOUtils
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class DraftHelper @Inject constructor(
|
||||
|
@ -44,7 +43,7 @@ class DraftHelper @Inject constructor(
|
|||
|
||||
private val draftDao = db.draftDao()
|
||||
|
||||
fun saveDraft(
|
||||
suspend fun saveDraft(
|
||||
draftId: Int,
|
||||
accountId: Long,
|
||||
inReplyToId: String?,
|
||||
|
@ -56,9 +55,7 @@ class DraftHelper @Inject constructor(
|
|||
mediaDescriptions: List<String?>,
|
||||
poll: NewPoll?,
|
||||
failedToSend: Boolean
|
||||
): Completable {
|
||||
return Single.fromCallable {
|
||||
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
||||
|
||||
if (externalFilesDir == null || !(externalFilesDir.exists())) {
|
||||
|
@ -103,7 +100,7 @@ class DraftHelper @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
DraftEntity(
|
||||
val draft = DraftEntity(
|
||||
id = draftId,
|
||||
accountId = accountId,
|
||||
inReplyToId = inReplyToId,
|
||||
|
@ -116,40 +113,34 @@ class DraftHelper @Inject constructor(
|
|||
failedToSend = failedToSend
|
||||
)
|
||||
|
||||
}.flatMapCompletable { draft ->
|
||||
draftDao.insertOrReplace(draft)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun deleteDraftAndAttachments(draftId: Int): Completable {
|
||||
return draftDao.find(draftId)
|
||||
.flatMapCompletable { draft ->
|
||||
suspend fun deleteDraftAndAttachments(draftId: Int) {
|
||||
draftDao.find(draftId)?.let { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDraftAndAttachments(draft: DraftEntity): Completable {
|
||||
return deleteAttachments(draft)
|
||||
.andThen(draftDao.delete(draft.id))
|
||||
suspend fun deleteDraftAndAttachments(draft: DraftEntity) {
|
||||
deleteAttachments(draft)
|
||||
draftDao.delete(draft.id)
|
||||
}
|
||||
|
||||
fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
|
||||
draftDao.loadDraftsSingle(accountId)
|
||||
.flatMapObservable { Observable.fromIterable(it) }
|
||||
.flatMapCompletable { draft ->
|
||||
suspend fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
|
||||
draftDao.loadDrafts(accountId).forEach { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAttachments(draft: DraftEntity): Completable {
|
||||
return Completable.fromCallable {
|
||||
suspend fun deleteAttachments(draft: DraftEntity) {
|
||||
withContext(Dispatchers.IO) {
|
||||
draft.attachments.forEach { attachment ->
|
||||
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
|
||||
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
|
||||
}
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Uri.isNotInFolder(folder: File): Boolean {
|
||||
|
@ -171,5 +162,4 @@ class DraftHelper @Inject constructor(
|
|||
IOUtils.copyToFile(contentResolver, this, file)
|
||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
||||
}
|
||||
|
||||
}
|
|
@ -30,7 +30,7 @@ import com.keylesspalace.tusky.db.DraftAttachment
|
|||
class DraftMediaAdapter(
|
||||
private val attachmentClick: () -> Unit
|
||||
) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>(
|
||||
object: DiffUtil.ItemCallback<DraftAttachment>() {
|
||||
object : DiffUtil.ItemCallback<DraftAttachment>() {
|
||||
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ class DraftMediaAdapter(
|
|||
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
}
|
||||
) {
|
||||
|
||||
|
@ -60,8 +59,8 @@ class DraftMediaAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
inner class DraftMediaViewHolder(val imageView: ImageView)
|
||||
: RecyclerView.ViewHolder(imageView) {
|
||||
inner class DraftMediaViewHolder(val imageView: ImageView) :
|
||||
RecyclerView.ViewHolder(imageView) {
|
||||
init {
|
||||
val thumbnailViewSize =
|
||||
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||
|
|
|
@ -19,28 +19,26 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
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.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.SavedTootActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
||||
import com.keylesspalace.tusky.db.DraftEntity
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -54,10 +52,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
private lateinit var binding: ActivityDraftsBinding
|
||||
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
||||
|
||||
private var oldDraftsButton: MenuItem? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityDraftsBinding.inflate(layoutInflater)
|
||||
|
@ -70,7 +65,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
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)
|
||||
|
||||
|
@ -80,44 +75,15 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
|
||||
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
||||
|
||||
viewModel.drafts.observe(this) { draftList ->
|
||||
if (draftList.isEmpty()) {
|
||||
binding.draftsRecyclerView.hide()
|
||||
binding.draftsErrorMessageView.show()
|
||||
} else {
|
||||
binding.draftsRecyclerView.show()
|
||||
binding.draftsErrorMessageView.hide()
|
||||
adapter.submitList(draftList)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
viewModel.drafts.collectLatest { draftData ->
|
||||
adapter.submitData(draftData)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.drafts, menu)
|
||||
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
|
||||
adapter.addLoadStateListener {
|
||||
binding.draftsErrorMessageView.visible(adapter.itemCount == 0)
|
||||
}
|
||||
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) {
|
||||
|
@ -126,8 +92,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
viewModel.getToot(draft.inReplyToId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this)
|
||||
.subscribe({ status ->
|
||||
.autoDispose(from(this))
|
||||
.subscribe(
|
||||
{ status ->
|
||||
val composeOptions = ComposeActivity.ComposeOptions(
|
||||
draftId = draft.id,
|
||||
tootText = draft.content,
|
||||
|
@ -144,8 +111,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
|
||||
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
||||
|
||||
}, { throwable ->
|
||||
},
|
||||
{ throwable ->
|
||||
|
||||
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)
|
||||
.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
} else {
|
||||
openDraftWithoutReply(draft)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.drafts
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -35,7 +35,7 @@ interface DraftActionListener {
|
|||
|
||||
class DraftsAdapter(
|
||||
private val listener: DraftActionListener
|
||||
) : PagedListAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
|
||||
) : PagingDataAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
|
||||
object : DiffUtil.ItemCallback<DraftEntity>() {
|
||||
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
|
@ -87,6 +87,5 @@ class DraftsAdapter(
|
|||
holder.binding.draftPoll.hide()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -16,14 +16,17 @@
|
|||
package com.keylesspalace.tusky.components.drafts
|
||||
|
||||
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.AppDatabase
|
||||
import com.keylesspalace.tusky.db.DraftEntity
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class DraftsViewModel @Inject constructor(
|
||||
|
@ -33,37 +36,39 @@ class DraftsViewModel @Inject constructor(
|
|||
val draftHelper: DraftHelper
|
||||
) : 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()
|
||||
|
||||
fun showOldDraftsButton(): Observable<Boolean> {
|
||||
return database.tootDao().savedTootCount()
|
||||
.map { count -> count > 0 }
|
||||
}
|
||||
|
||||
fun deleteDraft(draft: DraftEntity) {
|
||||
// this does not immediately delete media files to avoid unnecessary file operations
|
||||
// in case the user decides to restore the draft
|
||||
viewModelScope.launch {
|
||||
database.draftDao().delete(draft.id)
|
||||
.subscribe()
|
||||
deletedDrafts.add(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreDraft(draft: DraftEntity) {
|
||||
viewModelScope.launch {
|
||||
database.draftDao().insertOrReplace(draft)
|
||||
.subscribe()
|
||||
deletedDrafts.remove(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun getToot(tootId: String): Single<Status> {
|
||||
return api.status(tootId)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
viewModelScope.launch {
|
||||
deletedDrafts.forEach {
|
||||
draftHelper.deleteAttachments(it).subscribe()
|
||||
draftHelper.deleteAttachments(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import dagger.android.DispatchingAndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class InstanceListActivity: BaseActivity(), HasAndroidInjector {
|
||||
class InstanceListActivity : BaseActivity(), HasAndroidInjector {
|
||||
|
||||
@Inject
|
||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
||||
|
@ -33,5 +33,4 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
|
|||
}
|
||||
|
||||
override fun androidInjector() = androidInjector
|
||||
|
||||
}
|
|
@ -9,7 +9,7 @@ import com.keylesspalace.tusky.util.BindingHolder
|
|||
|
||||
class DomainMutesAdapter(
|
||||
private val actionListener: InstanceActionListener
|
||||
): RecyclerView.Adapter<BindingHolder<ItemMutedDomainBinding>>() {
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemMutedDomainBinding>>() {
|
||||
|
||||
var instances: MutableList<String> = mutableListOf()
|
||||
var bottomLoading: Boolean = false
|
||||
|
|
|
@ -8,6 +8,8 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
|
||||
|
@ -20,16 +22,14 @@ import com.keylesspalace.tusky.util.hide
|
|||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
|
||||
class InstanceListFragment : Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
|
||||
|
||||
@Inject
|
||||
lateinit var api: MastodonApi
|
||||
|
@ -65,7 +65,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
|
||||
override fun mute(mute: Boolean, instance: String, position: Int) {
|
||||
if (mute) {
|
||||
api.blockDomain(instance).enqueue(object: Callback<Any> {
|
||||
api.blockDomain(instance).enqueue(object : Callback<Any> {
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error muting domain $instance")
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
}
|
||||
})
|
||||
} else {
|
||||
api.unblockDomain(instance).enqueue(object: Callback<Any> {
|
||||
api.unblockDomain(instance).enqueue(object : Callback<Any> {
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error unmuting domain $instance")
|
||||
}
|
||||
|
@ -114,7 +114,8 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
api.domainBlocks(id, bottomId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({ response ->
|
||||
.subscribe(
|
||||
{ response ->
|
||||
val instances = response.body()
|
||||
|
||||
if (response.isSuccessful && instances != null) {
|
||||
|
@ -122,9 +123,11 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
} else {
|
||||
onFetchInstancesFailure(Exception(response.message()))
|
||||
}
|
||||
}, {throwable ->
|
||||
},
|
||||
{ throwable ->
|
||||
onFetchInstancesFailure(throwable)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
|
||||
|
|
|
@ -70,8 +70,8 @@ import java.util.List;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
||||
|
||||
|
@ -316,7 +316,7 @@ public class NotificationHelper {
|
|||
Status actionableStatus = status.getActionableStatus();
|
||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||
String contentWarning = actionableStatus.getSpoilerText();
|
||||
Status.Mention[] mentions = actionableStatus.getMentions();
|
||||
List<Status.Mention> mentions = actionableStatus.getMentions();
|
||||
List<String> mentionedUsernames = new ArrayList<>();
|
||||
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
||||
for (Status.Mention mention : mentions) {
|
||||
|
@ -381,7 +381,6 @@ public class NotificationHelper {
|
|||
|
||||
NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName());
|
||||
|
||||
//noinspection ConstantConditions
|
||||
notificationManager.createNotificationChannelGroup(channelGroup);
|
||||
|
||||
for (int i = 0; i < channelIds.length; i++) {
|
||||
|
|
|
@ -22,7 +22,11 @@ import android.util.Log
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
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.PreferenceChangedEvent
|
||||
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.Status
|
||||
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.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
|
@ -75,8 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
setOnPreferenceClickListener {
|
||||
val intent = Intent(context, TabPreferenceActivity::class.java)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
||||
R.anim.slide_to_left)
|
||||
activity?.overridePendingTransition(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.slide_to_left
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -88,8 +99,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
val intent = Intent(context, AccountListActivity::class.java)
|
||||
intent.putExtra("type", AccountListActivity.Type.MUTES)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
||||
R.anim.slide_to_left)
|
||||
activity?.overridePendingTransition(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.slide_to_left
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -104,8 +117,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
val intent = Intent(context, AccountListActivity::class.java)
|
||||
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
||||
R.anim.slide_to_left)
|
||||
activity?.overridePendingTransition(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.slide_to_left
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -116,8 +131,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
setOnPreferenceClickListener {
|
||||
val intent = Intent(context, InstanceListActivity::class.java)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
||||
R.anim.slide_to_left)
|
||||
activity?.overridePendingTransition(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.slide_to_left
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -201,8 +218,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
preference {
|
||||
setTitle(R.string.pref_title_public_filter_keywords)
|
||||
setOnPreferenceClickListener {
|
||||
launchFilterActivity(Filter.PUBLIC,
|
||||
R.string.pref_title_public_filter_keywords)
|
||||
launchFilterActivity(
|
||||
Filter.PUBLIC,
|
||||
R.string.pref_title_public_filter_keywords
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -226,8 +245,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
preference {
|
||||
setTitle(R.string.pref_title_thread_filter_keywords)
|
||||
setOnPreferenceClickListener {
|
||||
launchFilterActivity(Filter.THREAD,
|
||||
R.string.pref_title_thread_filter_keywords)
|
||||
launchFilterActivity(
|
||||
Filter.THREAD,
|
||||
R.string.pref_title_thread_filter_keywords
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -255,7 +276,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
it.startActivity(intent)
|
||||
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)
|
||||
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.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
@ -124,8 +124,6 @@ class EmojiPreference(
|
|||
finishDownload(font, binding)
|
||||
}
|
||||
).also { downloadDisposables[font.id] = it }
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
||||
|
@ -222,12 +220,14 @@ class EmojiPreference(
|
|||
context,
|
||||
0x1f973, // This is the codepoint of the party face emoji :D
|
||||
launchIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
)
|
||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
mgr.set(
|
||||
AlarmManager.RTC,
|
||||
System.currentTimeMillis() + 100,
|
||||
mPendingIntent)
|
||||
mPendingIntent
|
||||
)
|
||||
exitProcess(0)
|
||||
}.show()
|
||||
}
|
||||
|
|
|
@ -176,5 +176,4 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
return NotificationPreferencesFragment()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,7 +36,9 @@ import dagger.android.DispatchingAndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
class PreferencesActivity :
|
||||
BaseActivity(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
HasAndroidInjector {
|
||||
|
||||
@Inject
|
||||
|
@ -91,7 +93,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
|
|||
}
|
||||
|
||||
restartActivitiesOnExit = intent.getBooleanExtra("restart", false)
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -122,7 +123,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
|
|||
|
||||
restartActivitiesOnExit = true
|
||||
this.restartCurrentActivity()
|
||||
|
||||
}
|
||||
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
|
||||
"useBlurhash", "showCardsInTimelines", "confirmReblogs", "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR, "viewPagerOffScreenLimit" -> {
|
||||
|
@ -179,5 +179,4 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
|
|||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,7 +25,14 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
|
|||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.settings.*
|
||||
import com.keylesspalace.tusky.settings.AppTheme
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.settings.emojiPreference
|
||||
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.deserialize
|
||||
import com.keylesspalace.tusky.util.getNonNullString
|
||||
|
|
|
@ -50,7 +50,6 @@ class ProxyPreferencesFragment : PreferenceFragmentCompat() {
|
|||
setSummaryProvider { text }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
|
|
@ -51,7 +51,6 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID))
|
||||
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
|
|
@ -17,28 +17,38 @@ package com.keylesspalace.tusky.components.report
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.PagedList
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||
import com.keylesspalace.tusky.components.report.adapter.StatusesRepository
|
||||
import com.keylesspalace.tusky.components.report.adapter.StatusesPagingSource
|
||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
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.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReportViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub,
|
||||
private val statusesRepository: StatusesRepository) : RxAwareViewModel() {
|
||||
private val eventHub: EventHub
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
private val navigationMutable = MutableLiveData<Screen>()
|
||||
val navigation: LiveData<Screen> = navigationMutable
|
||||
private val navigationMutable = MutableLiveData<Screen?>()
|
||||
val navigation: LiveData<Screen?> = navigationMutable
|
||||
|
||||
private val muteStateMutable = MutableLiveData<Resource<Boolean>>()
|
||||
val muteState: LiveData<Resource<Boolean>> = muteStateMutable
|
||||
|
@ -49,14 +59,22 @@ class ReportViewModel @Inject constructor(
|
|||
private val reportingStateMutable = MutableLiveData<Resource<Boolean>>()
|
||||
var reportingState: LiveData<Resource<Boolean>> = reportingStateMutable
|
||||
|
||||
private val checkUrlMutable = MutableLiveData<String>()
|
||||
val checkUrl: LiveData<String> = checkUrlMutable
|
||||
private val checkUrlMutable = MutableLiveData<String?>()
|
||||
val checkUrl: LiveData<String?> = checkUrlMutable
|
||||
|
||||
private val repoResult = MutableLiveData<BiListing<Status>>()
|
||||
val statuses: LiveData<PagedList<Status>> = Transformations.switchMap(repoResult) { it.pagedList }
|
||||
val networkStateAfter: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkStateAfter }
|
||||
val networkStateBefore: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkStateBefore }
|
||||
val networkStateRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
|
||||
private val accountIdFlow = MutableSharedFlow<String>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
val statusesFlow = accountIdFlow.flatMapLatest { accountId ->
|
||||
Pager(
|
||||
initialKey = statusId,
|
||||
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
||||
pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) }
|
||||
).flow
|
||||
}
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
private val selectedIds = HashSet<String>()
|
||||
val statusViewState = StatusViewState()
|
||||
|
@ -84,7 +102,10 @@ class ReportViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
obtainRelationship()
|
||||
repoResult.value = statusesRepository.getStatuses(accountId, statusId, disposables)
|
||||
|
||||
viewModelScope.launch {
|
||||
accountIdFlow.emit(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateTo(screen: Screen) {
|
||||
|
@ -95,7 +116,6 @@ class ReportViewModel @Inject constructor(
|
|||
navigationMutable.value = null
|
||||
}
|
||||
|
||||
|
||||
private fun obtainRelationship() {
|
||||
val ids = listOf(accountId)
|
||||
muteStateMutable.value = Loading()
|
||||
|
@ -106,7 +126,6 @@ class ReportViewModel @Inject constructor(
|
|||
.subscribe(
|
||||
{ data ->
|
||||
updateRelationship(data.getOrNull(0))
|
||||
|
||||
},
|
||||
{
|
||||
updateRelationship(null)
|
||||
|
@ -115,7 +134,6 @@ class ReportViewModel @Inject constructor(
|
|||
.autoDispose()
|
||||
}
|
||||
|
||||
|
||||
private fun updateRelationship(relationship: Relationship?) {
|
||||
if (relationship != null) {
|
||||
muteStateMutable.value = Success(relationship.muting)
|
||||
|
@ -191,15 +209,6 @@ class ReportViewModel @Inject constructor(
|
|||
}
|
||||
)
|
||||
.autoDispose()
|
||||
|
||||
}
|
||||
|
||||
fun retryStatusLoad() {
|
||||
repoResult.value?.retry?.invoke()
|
||||
}
|
||||
|
||||
fun refreshStatuses() {
|
||||
repoResult.value?.refresh?.invoke()
|
||||
}
|
||||
|
||||
fun checkClickedUrl(url: String?) {
|
||||
|
@ -221,5 +230,4 @@ class ReportViewModel @Inject constructor(
|
|||
fun isStatusChecked(id: String): Boolean {
|
||||
return selectedIds.contains(id)
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue