Merge tag 'v20.0' into develop
135
app/build.gradle
|
@ -19,13 +19,13 @@ def getGitSha = {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 31
|
compileSdkVersion 33
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId APP_ID
|
applicationId APP_ID
|
||||||
minSdkVersion 21
|
minSdkVersion 23
|
||||||
targetSdkVersion 31
|
targetSdkVersion 33
|
||||||
versionCode 94
|
versionCode 97
|
||||||
versionName "19.0"
|
versionName "20.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
@ -93,109 +93,64 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.coroutinesVersion = "1.6.1"
|
// library versions are in PROJECT_ROOT/gradle/libs.versions.toml
|
||||||
ext.lifecycleVersion = "2.4.1"
|
|
||||||
ext.roomVersion = '2.4.2'
|
|
||||||
ext.retrofitVersion = '2.9.0'
|
|
||||||
ext.okhttpVersion = '4.9.3'
|
|
||||||
ext.glideVersion = '4.13.1'
|
|
||||||
ext.daggerVersion = '2.42'
|
|
||||||
ext.materialdrawerVersion = '8.4.5'
|
|
||||||
ext.emoji2_version = '1.1.0'
|
|
||||||
ext.filemojicompat_version = '3.2.2'
|
|
||||||
|
|
||||||
// if libraries are changed here, they should also be changed in LicenseActivity
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
implementation libs.kotlinx.coroutines.android
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
implementation libs.kotlinx.coroutines.rx3
|
||||||
|
|
||||||
implementation "androidx.core:core-ktx:1.7.0"
|
implementation libs.bundles.androidx
|
||||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
implementation libs.bundles.room
|
||||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
kapt libs.androidx.room.compiler
|
||||||
implementation "androidx.browser:browser:1.4.0"
|
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
|
||||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
|
||||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
|
||||||
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
|
|
||||||
implementation "androidx.emoji2:emoji2:$emoji2_version"
|
|
||||||
implementation "androidx.emoji2:emoji2-views:$emoji2_version"
|
|
||||||
implementation "androidx.emoji2:emoji2-views-helper:$emoji2_version"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
|
||||||
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
|
|
||||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
|
||||||
implementation "androidx.work:work-runtime:2.7.1"
|
|
||||||
implementation "androidx.room:room-ktx:$roomVersion"
|
|
||||||
implementation "androidx.room:room-paging:$roomVersion"
|
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
|
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.6.0"
|
implementation libs.android.material
|
||||||
|
|
||||||
implementation "com.google.code.gson:gson:2.9.0"
|
implementation libs.gson
|
||||||
|
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
implementation libs.bundles.retrofit
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
implementation libs.networkresult.calladapter
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
|
||||||
implementation "at.connyduck:networkresult-calladapter:1.0.0"
|
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation libs.bundles.okhttp
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
|
||||||
|
|
||||||
implementation "org.conscrypt:conscrypt-android:2.5.2"
|
implementation libs.conscrypt.android
|
||||||
|
|
||||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
implementation libs.bundles.glide
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
kapt libs.glide.compiler
|
||||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
|
||||||
|
|
||||||
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.22.0"
|
implementation libs.bundles.rxjava3
|
||||||
|
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.1.3"
|
implementation libs.bundles.autodispose
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
|
||||||
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
|
|
||||||
|
|
||||||
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"
|
implementation libs.bundles.dagger
|
||||||
implementation "com.uber.autodispose2:autodispose:2.1.1"
|
kapt libs.bundles.dagger.processors
|
||||||
|
|
||||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
implementation libs.sparkbutton
|
||||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
|
||||||
implementation "com.google.dagger:dagger-android:$daggerVersion"
|
|
||||||
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
|
|
||||||
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
|
|
||||||
|
|
||||||
implementation "com.github.connyduck:sparkbutton:4.1.0"
|
implementation libs.photoview
|
||||||
|
|
||||||
implementation "com.github.chrisbanes:PhotoView:2.3.0"
|
implementation libs.bundles.material.drawer
|
||||||
|
implementation libs.material.typeface, {
|
||||||
|
artifact {
|
||||||
|
type = "aar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
|
implementation libs.image.cropper
|
||||||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
|
||||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
|
||||||
|
|
||||||
implementation "com.github.CanHub:Android-Image-Cropper:4.2.1"
|
implementation libs.bundles.filemojicompat
|
||||||
|
|
||||||
implementation "de.c1710:filemojicompat-ui:$filemojicompat_version"
|
implementation libs.bouncycastle
|
||||||
implementation "de.c1710:filemojicompat:$filemojicompat_version"
|
implementation libs.unified.push
|
||||||
implementation "de.c1710:filemojicompat-defaults:$filemojicompat_version"
|
|
||||||
|
|
||||||
implementation "org.bouncycastle:bcprov-jdk15on:1.70"
|
testImplementation libs.androidx.test.junit
|
||||||
implementation "com.github.UnifiedPush:android-connector:2.0.0"
|
testImplementation libs.robolectric
|
||||||
|
testImplementation libs.bundles.mockito
|
||||||
|
testImplementation libs.mockwebserver
|
||||||
|
testImplementation libs.androidx.core.testing
|
||||||
|
testImplementation libs.kotlinx.coroutines.test
|
||||||
|
testImplementation libs.androidx.work.testing
|
||||||
|
|
||||||
testImplementation "androidx.test.ext:junit:1.1.3"
|
androidTestImplementation libs.espresso.core
|
||||||
testImplementation "org.robolectric:robolectric:4.4"
|
androidTestImplementation libs.androidx.room.testing
|
||||||
testImplementation "org.mockito:mockito-inline:4.4.0"
|
androidTestImplementation libs.androidx.test.junit
|
||||||
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
|
|
||||||
|
|
||||||
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
|
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
|
||||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
|
||||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
|
||||||
|
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
|
||||||
}
|
}
|
||||||
|
|
929
app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json
Normal file
|
@ -0,0 +1,929 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 40,
|
||||||
|
"identityHash": "0423fb3f7d09db5f12023f2f4e7297b5",
|
||||||
|
"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, `clientId` TEXT, `clientSecret` TEXT, `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, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` 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, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
|
||||||
|
"columnName": "clientId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "clientSecret",
|
||||||
|
"columnName": "clientSecret",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "oauthScopes",
|
||||||
|
"columnName": "oauthScopes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unifiedPushUrl",
|
||||||
|
"columnName": "unifiedPushUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPubKey",
|
||||||
|
"columnName": "pushPubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPrivKey",
|
||||||
|
"columnName": "pushPrivKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushAuth",
|
||||||
|
"columnName": "pushAuth",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushServerKey",
|
||||||
|
"columnName": "pushServerKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, 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": "minPollDuration",
|
||||||
|
"columnName": "minPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollDuration",
|
||||||
|
"columnName": "maxPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charactersReservedPerUrl",
|
||||||
|
"columnName": "charactersReservedPerUrl",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoSizeLimit",
|
||||||
|
"columnName": "videoSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageSizeLimit",
|
||||||
|
"columnName": "imageSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageMatrixLimit",
|
||||||
|
"columnName": "imageMatrixLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxMediaAttachments",
|
||||||
|
"columnName": "maxMediaAttachments",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFields",
|
||||||
|
"columnName": "maxFields",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldNameLength",
|
||||||
|
"columnName": "maxFieldNameLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldValueLength",
|
||||||
|
"columnName": "maxFieldValueLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, 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": "repliesCount",
|
||||||
|
"columnName": "repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `order` INTEGER 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_repliesCount` 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_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` 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": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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.repliesCount",
|
||||||
|
"columnName": "s_repliesCount",
|
||||||
|
"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.tags",
|
||||||
|
"columnName": "s_tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"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, '0423fb3f7d09db5f12023f2f4e7297b5')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
935
app/schemas/com.keylesspalace.tusky.db.AppDatabase/41.json
Normal file
|
@ -0,0 +1,935 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 41,
|
||||||
|
"identityHash": "1de8f20c7f28e1f11b33e7a55137feef",
|
||||||
|
"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, `scheduledAt` TEXT)",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scheduledAt",
|
||||||
|
"columnName": "scheduledAt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, `clientId` TEXT, `clientSecret` TEXT, `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, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` 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, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
|
||||||
|
"columnName": "clientId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "clientSecret",
|
||||||
|
"columnName": "clientSecret",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "oauthScopes",
|
||||||
|
"columnName": "oauthScopes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unifiedPushUrl",
|
||||||
|
"columnName": "unifiedPushUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPubKey",
|
||||||
|
"columnName": "pushPubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPrivKey",
|
||||||
|
"columnName": "pushPrivKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushAuth",
|
||||||
|
"columnName": "pushAuth",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushServerKey",
|
||||||
|
"columnName": "pushServerKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, 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": "minPollDuration",
|
||||||
|
"columnName": "minPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollDuration",
|
||||||
|
"columnName": "maxPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charactersReservedPerUrl",
|
||||||
|
"columnName": "charactersReservedPerUrl",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoSizeLimit",
|
||||||
|
"columnName": "videoSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageSizeLimit",
|
||||||
|
"columnName": "imageSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageMatrixLimit",
|
||||||
|
"columnName": "imageMatrixLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxMediaAttachments",
|
||||||
|
"columnName": "maxMediaAttachments",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFields",
|
||||||
|
"columnName": "maxFields",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldNameLength",
|
||||||
|
"columnName": "maxFieldNameLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldValueLength",
|
||||||
|
"columnName": "maxFieldValueLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, 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": "repliesCount",
|
||||||
|
"columnName": "repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `order` INTEGER 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_repliesCount` 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_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` 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": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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.repliesCount",
|
||||||
|
"columnName": "s_repliesCount",
|
||||||
|
"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.tags",
|
||||||
|
"columnName": "s_tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"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, '1de8f20c7f28e1f11b33e7a55137feef')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
953
app/schemas/com.keylesspalace.tusky.db.AppDatabase/42.json
Normal file
|
@ -0,0 +1,953 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 42,
|
||||||
|
"identityHash": "a62399cb3859de7fcbb9bd7053f7cb1d",
|
||||||
|
"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, `scheduledAt` TEXT, `language` TEXT)",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scheduledAt",
|
||||||
|
"columnName": "scheduledAt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, `clientId` TEXT, `clientSecret` TEXT, `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, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` 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, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
|
||||||
|
"columnName": "clientId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "clientSecret",
|
||||||
|
"columnName": "clientSecret",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "oauthScopes",
|
||||||
|
"columnName": "oauthScopes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unifiedPushUrl",
|
||||||
|
"columnName": "unifiedPushUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPubKey",
|
||||||
|
"columnName": "pushPubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPrivKey",
|
||||||
|
"columnName": "pushPrivKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushAuth",
|
||||||
|
"columnName": "pushAuth",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushServerKey",
|
||||||
|
"columnName": "pushServerKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, 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": "minPollDuration",
|
||||||
|
"columnName": "minPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollDuration",
|
||||||
|
"columnName": "maxPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charactersReservedPerUrl",
|
||||||
|
"columnName": "charactersReservedPerUrl",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoSizeLimit",
|
||||||
|
"columnName": "videoSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageSizeLimit",
|
||||||
|
"columnName": "imageSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageMatrixLimit",
|
||||||
|
"columnName": "imageMatrixLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxMediaAttachments",
|
||||||
|
"columnName": "maxMediaAttachments",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFields",
|
||||||
|
"columnName": "maxFields",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldNameLength",
|
||||||
|
"columnName": "maxFieldNameLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldValueLength",
|
||||||
|
"columnName": "maxFieldValueLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, 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": "repliesCount",
|
||||||
|
"columnName": "repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `order` INTEGER 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_repliesCount` 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_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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.repliesCount",
|
||||||
|
"columnName": "s_repliesCount",
|
||||||
|
"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.tags",
|
||||||
|
"columnName": "s_tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.language",
|
||||||
|
"columnName": "s_language",
|
||||||
|
"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, 'a62399cb3859de7fcbb9bd7053f7cb1d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
959
app/schemas/com.keylesspalace.tusky.db.AppDatabase/43.json
Normal file
|
@ -0,0 +1,959 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 43,
|
||||||
|
"identityHash": "bf68abe55bb58765da7f9d6f7ef618e2",
|
||||||
|
"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, `scheduledAt` TEXT, `language` TEXT)",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scheduledAt",
|
||||||
|
"columnName": "scheduledAt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, `clientId` TEXT, `clientSecret` TEXT, `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, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT 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, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
|
||||||
|
"columnName": "clientId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "clientSecret",
|
||||||
|
"columnName": "clientSecret",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"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": "defaultPostLanguage",
|
||||||
|
"columnName": "defaultPostLanguage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "oauthScopes",
|
||||||
|
"columnName": "oauthScopes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unifiedPushUrl",
|
||||||
|
"columnName": "unifiedPushUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPubKey",
|
||||||
|
"columnName": "pushPubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPrivKey",
|
||||||
|
"columnName": "pushPrivKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushAuth",
|
||||||
|
"columnName": "pushAuth",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushServerKey",
|
||||||
|
"columnName": "pushServerKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, 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": "minPollDuration",
|
||||||
|
"columnName": "minPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollDuration",
|
||||||
|
"columnName": "maxPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charactersReservedPerUrl",
|
||||||
|
"columnName": "charactersReservedPerUrl",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoSizeLimit",
|
||||||
|
"columnName": "videoSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageSizeLimit",
|
||||||
|
"columnName": "imageSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageMatrixLimit",
|
||||||
|
"columnName": "imageMatrixLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxMediaAttachments",
|
||||||
|
"columnName": "maxMediaAttachments",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFields",
|
||||||
|
"columnName": "maxFields",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldNameLength",
|
||||||
|
"columnName": "maxFieldNameLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldValueLength",
|
||||||
|
"columnName": "maxFieldValueLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, 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": "repliesCount",
|
||||||
|
"columnName": "repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `order` INTEGER 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_repliesCount` 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_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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.repliesCount",
|
||||||
|
"columnName": "s_repliesCount",
|
||||||
|
"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.tags",
|
||||||
|
"columnName": "s_tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.language",
|
||||||
|
"columnName": "s_language",
|
||||||
|
"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, 'bf68abe55bb58765da7f9d6f7ef618e2')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
|
||||||
<size android:width="108dp" android:height="108dp" />
|
|
||||||
<gradient
|
|
||||||
android:type="radial"
|
|
||||||
android:centerX="50%"
|
|
||||||
android:centerY="50%"
|
|
||||||
android:startColor="#2588d0"
|
|
||||||
android:endColor="#1967a3"
|
|
||||||
android:gradientRadius="100"/>
|
|
||||||
</shape>
|
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 14 KiB |
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
|
||||||
<size android:width="108dp" android:height="108dp" />
|
|
||||||
<gradient
|
|
||||||
android:type="radial"
|
|
||||||
android:centerX="50%"
|
|
||||||
android:centerY="50%"
|
|
||||||
android:startColor="#25d069"
|
|
||||||
android:endColor="#19a341"
|
|
||||||
android:gradientRadius="100"/>
|
|
||||||
</shape>
|
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 14 KiB |
|
@ -3,4 +3,6 @@
|
||||||
|
|
||||||
<color name="notification_color">#19A341</color>
|
<color name="notification_color">#19A341</color>
|
||||||
|
|
||||||
|
<color name="icon_background">#097b44</color>
|
||||||
|
<color name="icon_highlight">#39ff9e</color>
|
||||||
</resources>
|
</resources>
|
|
@ -4,12 +4,10 @@
|
||||||
package="com.keylesspalace.tusky">
|
package="com.keylesspalace.tusky">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" /> <!-- For notifications -->
|
<uses-permission android:name="android.permission.VIBRATE" /> <!-- For notifications -->
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.ACCESS_COARSE_LOCATION"
|
|
||||||
android:maxSdkVersion="22" /> <!-- for day/night mode -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
@ -20,7 +18,8 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/TuskyTheme"
|
android:theme="@style/TuskyTheme"
|
||||||
android:usesCleartextTraffic="false">
|
android:usesCleartextTraffic="false"
|
||||||
|
android:localeConfig="@xml/locales_config">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SplashActivity"
|
android:name=".SplashActivity"
|
||||||
|
@ -101,11 +100,12 @@
|
||||||
android:theme="@style/TuskyDialogActivityTheme"
|
android:theme="@style/TuskyDialogActivityTheme"
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ViewThreadActivity"
|
android:name=".components.viewthread.ViewThreadActivity"
|
||||||
android:configChanges="orientation|screenSize" />
|
android:configChanges="orientation|screenSize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ViewMediaActivity"
|
android:name=".ViewMediaActivity"
|
||||||
android:theme="@style/TuskyBaseTheme" />
|
android:theme="@style/TuskyBaseTheme"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.account.AccountActivity"
|
android:name=".components.account.AccountActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
||||||
|
|
BIN
app/src/main/ic_launcher-web.png
Normal file
After Width: | Height: | Size: 27 KiB |
|
@ -16,7 +16,6 @@
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
@ -92,11 +91,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
requesters = new HashMap<>();
|
requesters = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean requiresLogin() {
|
protected boolean requiresLogin() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -132,7 +126,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
if (item.getItemId() == android.R.id.home) {
|
if (item.getItemId() == android.R.id.home) {
|
||||||
onBackPressed();
|
getOnBackPressedDispatcher().onBackPressed();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
|
|
|
@ -27,6 +27,7 @@ import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||||
import autodispose2.autoDispose
|
import autodispose2.autoDispose
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
|
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.openLink
|
import com.keylesspalace.tusky.util.openLink
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
@ -35,8 +36,8 @@ import java.net.URISyntaxException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/** this is the base class for all activities that open links
|
/** this is the base class for all activities that open links
|
||||||
* links are checked against the api if they are mastodon links so they can be openend in Tusky
|
* links are checked against the api if they are mastodon links so they can be opened in Tusky
|
||||||
* Subclasses must have a bottom sheet with Id item_status_bottom_sheet in their layout hierachy
|
* Subclasses must have a bottom sheet with Id item_status_bottom_sheet in their layout hierarchy
|
||||||
*/
|
*/
|
||||||
|
|
||||||
abstract class BottomSheetActivity : BaseActivity() {
|
abstract class BottomSheetActivity : BaseActivity() {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import android.widget.ImageView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
@ -37,6 +38,7 @@ import com.canhub.cropper.CropImageContract
|
||||||
import com.canhub.cropper.options
|
import com.canhub.cropper.options
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||||
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
@ -50,6 +52,7 @@ import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class EditProfileActivity : BaseActivity(), Injectable {
|
class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
@ -58,8 +61,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
const val AVATAR_SIZE = 400
|
const val AVATAR_SIZE = 400
|
||||||
const val HEADER_WIDTH = 1500
|
const val HEADER_WIDTH = 1500
|
||||||
const val HEADER_HEIGHT = 500
|
const val HEADER_HEIGHT = 500
|
||||||
|
|
||||||
private const val MAX_ACCOUNT_FIELDS = 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -71,6 +72,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
||||||
|
|
||||||
|
private var maxAccountFields = InstanceInfoRepository.DEFAULT_MAX_ACCOUNT_FIELDS
|
||||||
|
|
||||||
private enum class PickType {
|
private enum class PickType {
|
||||||
AVATAR,
|
AVATAR,
|
||||||
HEADER
|
HEADER
|
||||||
|
@ -112,7 +115,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
binding.addFieldButton.setOnClickListener {
|
binding.addFieldButton.setOnClickListener {
|
||||||
accountFieldEditAdapter.addField()
|
accountFieldEditAdapter.addField()
|
||||||
if (accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
|
if (accountFieldEditAdapter.itemCount >= maxAccountFields) {
|
||||||
it.isVisible = false
|
it.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +137,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
binding.lockedCheckBox.isChecked = me.locked
|
binding.lockedCheckBox.isChecked = me.locked
|
||||||
|
|
||||||
accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList())
|
accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList())
|
||||||
binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS
|
binding.addFieldButton.isVisible =
|
||||||
|
(me.source?.fields?.size ?: 0) < maxAccountFields
|
||||||
|
|
||||||
if (viewModel.avatarData.value == null) {
|
if (viewModel.avatarData.value == null) {
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
|
@ -165,13 +169,12 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.obtainInstance()
|
lifecycleScope.launch {
|
||||||
viewModel.instanceData.observe(this) { result ->
|
viewModel.instanceData.collect { instanceInfo ->
|
||||||
if (result is Success) {
|
maxAccountFields = instanceInfo.maxFields
|
||||||
val instance = result.data
|
accountFieldEditAdapter.setFieldLimits(instanceInfo.maxFieldNameLength, instanceInfo.maxFieldValueLength)
|
||||||
if (instance?.maxBioChars != null && instance.maxBioChars > 0) {
|
binding.addFieldButton.isVisible =
|
||||||
binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars
|
accountFieldEditAdapter.itemCount < maxAccountFields
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.format.DateUtils
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.databinding.ActivityFiltersBinding
|
import com.keylesspalace.tusky.databinding.ActivityFiltersBinding
|
||||||
import com.keylesspalace.tusky.databinding.DialogFilterBinding
|
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.view.getSecondsForDurationIndex
|
||||||
|
import com.keylesspalace.tusky.view.setupEditDialogForFilter
|
||||||
|
import com.keylesspalace.tusky.view.showAddFilterDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -47,7 +46,7 @@ class FiltersActivity : BaseActivity() {
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
binding.addFilterButton.setOnClickListener {
|
binding.addFilterButton.setOnClickListener {
|
||||||
showAddFilterDialog()
|
showAddFilterDialog(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
title = intent?.getStringExtra(FILTERS_TITLE)
|
title = intent?.getStringExtra(FILTERS_TITLE)
|
||||||
|
@ -55,15 +54,10 @@ class FiltersActivity : BaseActivity() {
|
||||||
loadFilters()
|
loadFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFilter(filter: Filter, itemIndex: Int) {
|
fun updateFilter(id: String, phrase: String, filterContext: List<String>, irreversible: Boolean, wholeWord: Boolean, expiresInSeconds: Int?, itemIndex: Int) {
|
||||||
api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt)
|
lifecycleScope.launch {
|
||||||
.enqueue(object : Callback<Filter> {
|
api.updateFilter(id, phrase, filterContext, irreversible, wholeWord, expiresInSeconds).fold(
|
||||||
override fun onFailure(call: Call<Filter>, t: Throwable) {
|
{ updatedFilter ->
|
||||||
Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
|
|
||||||
val updatedFilter = response.body()!!
|
|
||||||
if (updatedFilter.context.contains(context)) {
|
if (updatedFilter.context.contains(context)) {
|
||||||
filters[itemIndex] = updatedFilter
|
filters[itemIndex] = updatedFilter
|
||||||
} else {
|
} else {
|
||||||
|
@ -71,25 +65,30 @@ class FiltersActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
refreshFilterDisplay()
|
refreshFilterDisplay()
|
||||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Toast.makeText(this@FiltersActivity, "Error updating filter '$phrase'", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteFilter(itemIndex: Int) {
|
fun deleteFilter(itemIndex: Int) {
|
||||||
val filter = filters[itemIndex]
|
val filter = filters[itemIndex]
|
||||||
if (filter.context.size == 1) {
|
if (filter.context.size == 1) {
|
||||||
// This is the only context for this filter; delete it
|
lifecycleScope.launch {
|
||||||
api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback<ResponseBody> {
|
// This is the only context for this filter; delete it
|
||||||
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
|
api.deleteFilter(filters[itemIndex].id).fold(
|
||||||
Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show()
|
{
|
||||||
}
|
filters.removeAt(itemIndex)
|
||||||
|
refreshFilterDisplay()
|
||||||
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
filters.removeAt(itemIndex)
|
},
|
||||||
refreshFilterDisplay()
|
{
|
||||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
Toast.makeText(this@FiltersActivity, "Error deleting filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Keep the filter, but remove it from this context
|
// Keep the filter, but remove it from this context
|
||||||
val oldFilter = filters[itemIndex]
|
val oldFilter = filters[itemIndex]
|
||||||
|
@ -97,69 +96,50 @@ class FiltersActivity : BaseActivity() {
|
||||||
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
||||||
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
|
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
|
||||||
)
|
)
|
||||||
updateFilter(newFilter, itemIndex)
|
updateFilter(
|
||||||
|
newFilter.id, newFilter.phrase, newFilter.context, newFilter.irreversible, newFilter.wholeWord,
|
||||||
|
getSecondsForDurationIndex(-1, this, oldFilter.expiresAt), itemIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFilter(phrase: String, wholeWord: Boolean) {
|
fun createFilter(phrase: String, wholeWord: Boolean, expiresInSeconds: Int? = null) {
|
||||||
api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object : Callback<Filter> {
|
lifecycleScope.launch {
|
||||||
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
|
api.createFilter(phrase, listOf(context), false, wholeWord, expiresInSeconds).fold(
|
||||||
val filterResponse = response.body()
|
{ filter ->
|
||||||
if (response.isSuccessful && filterResponse != null) {
|
filters.add(filter)
|
||||||
filters.add(filterResponse)
|
|
||||||
refreshFilterDisplay()
|
refreshFilterDisplay()
|
||||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
} else {
|
},
|
||||||
|
{
|
||||||
Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
}
|
||||||
override fun onFailure(call: Call<Filter>, t: Throwable) {
|
|
||||||
Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showAddFilterDialog() {
|
|
||||||
val binding = DialogFilterBinding.inflate(layoutInflater)
|
|
||||||
binding.phraseWholeWord.isChecked = true
|
|
||||||
AlertDialog.Builder(this@FiltersActivity)
|
|
||||||
.setTitle(R.string.filter_addition_dialog_title)
|
|
||||||
.setView(binding.root)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked)
|
|
||||||
}
|
|
||||||
.setNeutralButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupEditDialogForItem(itemIndex: Int) {
|
|
||||||
val binding = DialogFilterBinding.inflate(layoutInflater)
|
|
||||||
val filter = filters[itemIndex]
|
|
||||||
binding.phraseEditText.setText(filter.phrase)
|
|
||||||
binding.phraseWholeWord.isChecked = filter.wholeWord
|
|
||||||
|
|
||||||
AlertDialog.Builder(this@FiltersActivity)
|
|
||||||
.setTitle(R.string.filter_edit_dialog_title)
|
|
||||||
.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
|
|
||||||
)
|
|
||||||
updateFilter(newFilter, itemIndex)
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
|
||||||
deleteFilter(itemIndex)
|
|
||||||
}
|
|
||||||
.setNeutralButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFilterDisplay() {
|
private fun refreshFilterDisplay() {
|
||||||
binding.filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase })
|
binding.filtersView.adapter = ArrayAdapter(
|
||||||
binding.filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) }
|
this,
|
||||||
|
android.R.layout.simple_list_item_1,
|
||||||
|
filters.map { filter ->
|
||||||
|
if (filter.expiresAt == null) {
|
||||||
|
filter.phrase
|
||||||
|
} else {
|
||||||
|
getString(
|
||||||
|
R.string.filter_expiration_format,
|
||||||
|
filter.phrase,
|
||||||
|
DateUtils.getRelativeTimeSpanString(
|
||||||
|
filter.expiresAt.time,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
DateUtils.MINUTE_IN_MILLIS,
|
||||||
|
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
binding.filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForFilter(this, filters[position], position) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFilters() {
|
private fun loadFilters() {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import android.util.Log
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RawRes
|
import androidx.annotation.RawRes
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
||||||
import com.keylesspalace.tusky.util.IOUtils
|
import com.keylesspalace.tusky.util.closeQuietly
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
@ -60,7 +60,7 @@ class LicenseActivity : BaseActivity() {
|
||||||
Log.w("LicenseActivity", e)
|
Log.w("LicenseActivity", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
IOUtils.closeQuietly(br)
|
br.closeQuietly()
|
||||||
|
|
||||||
textView.text = sb.toString()
|
textView.text = sb.toString()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,11 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
@ -31,8 +33,10 @@ import android.view.KeyEvent
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
|
@ -73,6 +77,7 @@ import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.entity.Notification
|
||||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
|
@ -176,6 +181,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
if (accountRequested && accountId != activeAccount.id) {
|
if (accountRequested && accountId != activeAccount.id) {
|
||||||
accountManager.setActiveAccount(accountId)
|
accountManager.setActiveAccount(accountId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val openDrafts = intent.getBooleanExtra(OPEN_DRAFTS, false)
|
||||||
|
|
||||||
if (canHandleMimeType(intent.type)) {
|
if (canHandleMimeType(intent.type)) {
|
||||||
// Sharing to Tusky from an external app
|
// Sharing to Tusky from an external app
|
||||||
if (accountRequested) {
|
if (accountRequested) {
|
||||||
|
@ -200,9 +208,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (openDrafts) {
|
||||||
|
val intent = DraftsActivity.newIntent(this)
|
||||||
|
startActivity(intent)
|
||||||
} else if (accountRequested && savedInstanceState == null) {
|
} else if (accountRequested && savedInstanceState == null) {
|
||||||
// user clicked a notification, show notification tab
|
// user clicked a notification, show follow requests for type FOLLOW_REQUEST,
|
||||||
showNotificationTab = true
|
// otherwise show notification tab
|
||||||
|
if (intent.getStringExtra(NotificationHelper.TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
|
||||||
|
val intent = AccountListActivity.newIntent(this, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = true)
|
||||||
|
startActivityWithSlideInAnimation(intent)
|
||||||
|
} else {
|
||||||
|
showNotificationTab = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
|
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
|
||||||
|
@ -262,6 +279,33 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
|
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(
|
||||||
|
this,
|
||||||
|
object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
when {
|
||||||
|
binding.mainDrawerLayout.isOpen -> {
|
||||||
|
binding.mainDrawerLayout.close()
|
||||||
|
}
|
||||||
|
binding.viewPager.currentItem != 0 -> {
|
||||||
|
binding.viewPager.currentItem = 0
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -287,20 +331,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
when {
|
|
||||||
binding.mainDrawerLayout.isOpen -> {
|
|
||||||
binding.mainDrawerLayout.close()
|
|
||||||
}
|
|
||||||
binding.viewPager.currentItem != 0 -> {
|
|
||||||
binding.viewPager.currentItem = 0
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_MENU -> {
|
KeyEvent.KEYCODE_MENU -> {
|
||||||
|
@ -376,7 +406,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
closeDrawerOnProfileListClick = true
|
closeDrawerOnProfileListClick = true
|
||||||
}
|
}
|
||||||
|
|
||||||
header.accountHeaderBackground.setColorFilter(ContextCompat.getColor(this, R.color.headerBackgroundFilter))
|
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter))
|
||||||
header.accountHeaderBackground.setBackgroundColor(ThemeUtils.getColor(this, R.attr.colorBackgroundAccent))
|
header.accountHeaderBackground.setBackgroundColor(ThemeUtils.getColor(this, R.attr.colorBackgroundAccent))
|
||||||
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
|
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
|
||||||
|
|
||||||
|
@ -829,6 +859,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
header.clear()
|
header.clear()
|
||||||
header.profiles = profiles
|
header.profiles = profiles
|
||||||
header.setActiveProfile(accountManager.activeAccount!!.id)
|
header.setActiveProfile(accountManager.activeAccount!!.id)
|
||||||
|
binding.mainToolbar.subtitle = if (accountManager.shouldDisplaySelfUsername(this)) {
|
||||||
|
accountManager.activeAccount!!.fullName
|
||||||
|
} else null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getActionButton() = binding.composeButton
|
override fun getActionButton() = binding.composeButton
|
||||||
|
@ -840,6 +873,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
||||||
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
||||||
const val REDIRECT_URL = "redirectUrl"
|
const val REDIRECT_URL = "redirectUrl"
|
||||||
|
const val OPEN_DRAFTS = "draft"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,20 @@ package com.keylesspalace.tusky
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
|
||||||
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
|
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
@ -31,16 +39,21 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
||||||
|
|
||||||
|
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
|
||||||
|
private lateinit var kind: Kind
|
||||||
|
private var hashtag: String? = null
|
||||||
|
private var followTagItem: MenuItem? = null
|
||||||
|
private var unfollowTagItem: MenuItem? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val binding = ActivityStatuslistBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
|
|
||||||
val kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
||||||
val listId = intent.getStringExtra(EXTRA_LIST_ID)
|
val listId = intent.getStringExtra(EXTRA_LIST_ID)
|
||||||
val hashtag = intent.getStringExtra(EXTRA_HASHTAG)
|
hashtag = intent.getStringExtra(EXTRA_HASHTAG)
|
||||||
|
|
||||||
val title = when (kind) {
|
val title = when (kind) {
|
||||||
Kind.FAVOURITES -> getString(R.string.title_favourites)
|
Kind.FAVOURITES -> getString(R.string.title_favourites)
|
||||||
|
@ -67,6 +80,70 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
val tag = hashtag
|
||||||
|
if (kind == Kind.TAG && tag != null) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
mastodonApi.tag(tag).fold(
|
||||||
|
{ tagEntity ->
|
||||||
|
menuInflater.inflate(R.menu.view_hashtag_toolbar, menu)
|
||||||
|
followTagItem = menu.findItem(R.id.action_follow_hashtag)
|
||||||
|
unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag)
|
||||||
|
followTagItem?.isVisible = tagEntity.following == false
|
||||||
|
unfollowTagItem?.isVisible = tagEntity.following == true
|
||||||
|
followTagItem?.setOnMenuItemClickListener { followTag() }
|
||||||
|
unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Log.w(TAG, "Failed to query tag #$tag", it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun followTag(): Boolean {
|
||||||
|
val tag = hashtag
|
||||||
|
if (tag != null) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
mastodonApi.followTag(tag).fold(
|
||||||
|
{
|
||||||
|
followTagItem?.isVisible = false
|
||||||
|
unfollowTagItem?.isVisible = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Snackbar.make(binding.root, getString(R.string.error_following_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||||
|
Log.e(TAG, "Failed to follow #$tag", it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unfollowTag(): Boolean {
|
||||||
|
val tag = hashtag
|
||||||
|
if (tag != null) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
mastodonApi.unfollowTag(tag).fold(
|
||||||
|
{
|
||||||
|
followTagItem?.isVisible = true
|
||||||
|
unfollowTagItem?.isVisible = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Snackbar.make(binding.root, getString(R.string.error_unfollowing_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||||
|
Log.e(TAG, "Failed to unfollow #$tag", it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun androidInjector() = dispatchingAndroidInjector
|
override fun androidInjector() = dispatchingAndroidInjector
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -75,6 +152,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
private const val EXTRA_LIST_ID = "id"
|
private const val EXTRA_LIST_ID = "id"
|
||||||
private const val EXTRA_LIST_TITLE = "title"
|
private const val EXTRA_LIST_TITLE = "title"
|
||||||
private const val EXTRA_HASHTAG = "tag"
|
private const val EXTRA_HASHTAG = "tag"
|
||||||
|
const val TAG = "StatusListActivity"
|
||||||
|
|
||||||
fun newFavouritesIntent(context: Context) =
|
fun newFavouritesIntent(context: Context) =
|
||||||
Intent(context, StatusListActivity::class.java).apply {
|
Intent(context, StatusListActivity::class.java).apply {
|
||||||
|
|
|
@ -100,6 +100,6 @@ fun defaultTabs(): List<TabData> {
|
||||||
createTabDataFromId(HOME),
|
createTabDataFromId(HOME),
|
||||||
createTabDataFromId(NOTIFICATIONS),
|
createTabDataFromId(NOTIFICATIONS),
|
||||||
createTabDataFromId(LOCAL),
|
createTabDataFromId(LOCAL),
|
||||||
createTabDataFromId(FEDERATED)
|
createTabDataFromId(DIRECT)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -74,6 +74,12 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
|
|
||||||
private val hashtagRegex by lazy { Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE) }
|
private val hashtagRegex by lazy { Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE) }
|
||||||
|
|
||||||
|
private val onFabDismissedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
toggleFab(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -149,6 +155,8 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
binding.maxTabsInfo.text = resources.getQuantityString(R.plurals.max_tab_number_reached, MAX_TAB_COUNT, MAX_TAB_COUNT)
|
binding.maxTabsInfo.text = resources.getQuantityString(R.plurals.max_tab_number_reached, MAX_TAB_COUNT, MAX_TAB_COUNT)
|
||||||
|
|
||||||
updateAvailableTabs()
|
updateAvailableTabs()
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(onFabDismissedCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabAdded(tab: TabData) {
|
override fun onTabAdded(tab: TabData) {
|
||||||
|
@ -209,6 +217,8 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
binding.actionButton.visible(!expand)
|
binding.actionButton.visible(!expand)
|
||||||
binding.sheet.visible(expand)
|
binding.sheet.visible(expand)
|
||||||
binding.scrim.visible(expand)
|
binding.scrim.visible(expand)
|
||||||
|
|
||||||
|
onFabDismissedCallback.isEnabled = expand
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) {
|
private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) {
|
||||||
|
@ -338,14 +348,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
tabsChanged = true
|
tabsChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (binding.actionButton.isVisible) {
|
|
||||||
super.onBackPressed()
|
|
||||||
} else {
|
|
||||||
toggleFab(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
if (tabsChanged) {
|
if (tabsChanged) {
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
@ -44,6 +42,9 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var notificationWorkerFactory: NotificationWorkerFactory
|
lateinit var notificationWorkerFactory: NotificationWorkerFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var localeManager: LocaleManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
// Uncomment me to get StrictMode violation logs
|
// Uncomment me to get StrictMode violation logs
|
||||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||||
|
@ -74,6 +75,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||||
ThemeUtils.setAppNightMode(theme)
|
ThemeUtils.setAppNightMode(theme)
|
||||||
|
|
||||||
|
localeManager.setLocale()
|
||||||
|
|
||||||
RxJavaPlugins.setErrorHandler {
|
RxJavaPlugins.setErrorHandler {
|
||||||
Log.w("RxJava", "undeliverable exception", it)
|
Log.w("RxJava", "undeliverable exception", it)
|
||||||
}
|
}
|
||||||
|
@ -86,20 +89,5 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
|
||||||
localeManager = LocaleManager(base)
|
|
||||||
super.attachBaseContext(localeManager.setLocale(base))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
localeManager.setLocale(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
override fun androidInjector() = androidInjector
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
lateinit var localeManager: LocaleManager
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.transition.Transition
|
import android.transition.Transition
|
||||||
|
@ -47,6 +48,7 @@ import autodispose2.autoDispose
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.FutureTarget
|
import com.bumptech.glide.request.FutureTarget
|
||||||
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
||||||
|
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
|
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.fragment.ViewImageFragment
|
import com.keylesspalace.tusky.fragment.ViewImageFragment
|
||||||
|
@ -211,12 +213,20 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestDownloadMedia() {
|
private fun requestDownloadMedia() {
|
||||||
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
|
||||||
downloadMedia()
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
} else {
|
downloadMedia()
|
||||||
showErrorDialog(binding.toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() }
|
} else {
|
||||||
|
showErrorDialog(
|
||||||
|
binding.toolbar,
|
||||||
|
R.string.error_media_download_permission,
|
||||||
|
R.string.action_retry
|
||||||
|
) { requestDownloadMedia() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
downloadMedia()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,130 +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.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.fragment.ViewThreadFragment;
|
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import dagger.android.AndroidInjector;
|
|
||||||
import dagger.android.DispatchingAndroidInjector;
|
|
||||||
import dagger.android.HasAndroidInjector;
|
|
||||||
|
|
||||||
public class ViewThreadActivity extends BottomSheetActivity implements HasAndroidInjector {
|
|
||||||
|
|
||||||
public static final int REVEAL_BUTTON_HIDDEN = 1;
|
|
||||||
public static final int REVEAL_BUTTON_REVEAL = 2;
|
|
||||||
public static final int REVEAL_BUTTON_HIDE = 3;
|
|
||||||
|
|
||||||
public static Intent startIntent(Context context, String id, String url) {
|
|
||||||
Intent intent = new Intent(context, ViewThreadActivity.class);
|
|
||||||
intent.putExtra(ID_EXTRA, id);
|
|
||||||
intent.putExtra(URL_EXTRA, url);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String ID_EXTRA = "id";
|
|
||||||
private static final String URL_EXTRA = "url";
|
|
||||||
private static final String FRAGMENT_TAG = "ViewThreadFragment_";
|
|
||||||
|
|
||||||
private int revealButtonState = REVEAL_BUTTON_HIDDEN;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public DispatchingAndroidInjector<Object> dispatchingAndroidInjector;
|
|
||||||
|
|
||||||
private ViewThreadFragment fragment;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_view_thread);
|
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
|
||||||
actionBar.setTitle(R.string.title_view_thread);
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
actionBar.setDisplayShowHomeEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
String id = getIntent().getStringExtra(ID_EXTRA);
|
|
||||||
|
|
||||||
fragment = (ViewThreadFragment)getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG + id);
|
|
||||||
if(fragment == null) {
|
|
||||||
fragment = ViewThreadFragment.newInstance(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
|
||||||
fragmentTransaction.replace(R.id.fragment_container, fragment, FRAGMENT_TAG + id);
|
|
||||||
fragmentTransaction.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
getMenuInflater().inflate(R.menu.view_thread_toolbar, menu);
|
|
||||||
MenuItem menuItem = menu.findItem(R.id.action_reveal);
|
|
||||||
menuItem.setVisible(revealButtonState != REVEAL_BUTTON_HIDDEN);
|
|
||||||
menuItem.setIcon(revealButtonState == REVEAL_BUTTON_REVEAL ?
|
|
||||||
R.drawable.ic_eye_24dp : R.drawable.ic_hide_media_24dp);
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRevealButtonState(int state) {
|
|
||||||
switch (state) {
|
|
||||||
case REVEAL_BUTTON_HIDDEN:
|
|
||||||
case REVEAL_BUTTON_REVEAL:
|
|
||||||
case REVEAL_BUTTON_HIDE:
|
|
||||||
this.revealButtonState = state;
|
|
||||||
invalidateOptionsMenu();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Invalid reveal button state: " + state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.action_open_in_web: {
|
|
||||||
openLink(getIntent().getStringExtra(URL_EXTRA));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case R.id.action_reveal: {
|
|
||||||
fragment.onRevealPressed();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AndroidInjector<Object> androidInjector() {
|
|
||||||
return dispatchingAndroidInjector;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -27,6 +27,8 @@ import com.keylesspalace.tusky.util.BindingHolder
|
||||||
class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
|
class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
|
||||||
|
|
||||||
private val fieldData = mutableListOf<MutableStringPair>()
|
private val fieldData = mutableListOf<MutableStringPair>()
|
||||||
|
private var maxNameLength: Int? = null
|
||||||
|
private var maxValueLength: Int? = null
|
||||||
|
|
||||||
fun setFields(fields: List<StringField>) {
|
fun setFields(fields: List<StringField>) {
|
||||||
fieldData.clear()
|
fieldData.clear()
|
||||||
|
@ -41,6 +43,12 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setFieldLimits(maxNameLength: Int?, maxValueLength: Int?) {
|
||||||
|
this.maxNameLength = maxNameLength
|
||||||
|
this.maxValueLength = maxValueLength
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
fun getFieldData(): List<StringField> {
|
fun getFieldData(): List<StringField> {
|
||||||
return fieldData.map {
|
return fieldData.map {
|
||||||
StringField(it.first, it.second)
|
StringField(it.first, it.second)
|
||||||
|
@ -60,10 +68,20 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemEditFieldBinding>, position: Int) {
|
override fun onBindViewHolder(holder: BindingHolder<ItemEditFieldBinding>, position: Int) {
|
||||||
holder.binding.accountFieldName.setText(fieldData[position].first)
|
holder.binding.accountFieldNameText.setText(fieldData[position].first)
|
||||||
holder.binding.accountFieldValue.setText(fieldData[position].second)
|
holder.binding.accountFieldValueText.setText(fieldData[position].second)
|
||||||
|
|
||||||
holder.binding.accountFieldName.addTextChangedListener(object : TextWatcher {
|
holder.binding.accountFieldNameTextLayout.isCounterEnabled = maxNameLength != null
|
||||||
|
maxNameLength?.let {
|
||||||
|
holder.binding.accountFieldNameTextLayout.counterMaxLength = it
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.accountFieldValueTextLayout.isCounterEnabled = maxValueLength != null
|
||||||
|
maxValueLength?.let {
|
||||||
|
holder.binding.accountFieldValueTextLayout.counterMaxLength = it
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.accountFieldNameText.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(newText: Editable) {
|
override fun afterTextChanged(newText: Editable) {
|
||||||
fieldData[holder.bindingAdapterPosition].first = newText.toString()
|
fieldData[holder.bindingAdapterPosition].first = newText.toString()
|
||||||
}
|
}
|
||||||
|
@ -73,7 +91,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
||||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
holder.binding.accountFieldValue.addTextChangedListener(object : TextWatcher {
|
holder.binding.accountFieldValueText.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(newText: Editable) {
|
override fun afterTextChanged(newText: Editable) {
|
||||||
fieldData[holder.bindingAdapterPosition].second = newText.toString()
|
fieldData[holder.bindingAdapterPosition].second = newText.toString()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/* Copyright 2022 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.content.Context
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
|
import com.keylesspalace.tusky.util.modernLanguageCode
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
return (super.getView(position, convertView, parent) as TextView).apply {
|
||||||
|
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
|
||||||
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
|
text = super.getItem(position)?.modernLanguageCode?.uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
return (super.getDropDownView(position, convertView, parent) as TextView).apply {
|
||||||
|
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
|
||||||
|
val locale = super.getItem(position)
|
||||||
|
text = "${locale?.displayLanguage} (${locale?.getDisplayLanguage(locale)})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -174,12 +174,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NotificationViewData.Concrete concreteNotificaton =
|
NotificationViewData.Concrete concreteNotification =
|
||||||
(NotificationViewData.Concrete) notification;
|
(NotificationViewData.Concrete) notification;
|
||||||
switch (viewHolder.getItemViewType()) {
|
switch (viewHolder.getItemViewType()) {
|
||||||
case VIEW_TYPE_STATUS: {
|
case VIEW_TYPE_STATUS: {
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||||
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
|
StatusViewData.Concrete status = concreteNotification.getStatusViewData();
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
/* in some very rare cases servers sends null status even though they should not,
|
/* in some very rare cases servers sends null status even though they should not,
|
||||||
* we have to handle it somehow */
|
* we have to handle it somehow */
|
||||||
|
@ -190,8 +190,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
holder.setupWithStatus(status, statusListener, statusDisplayOptions, payloadForHolder);
|
holder.setupWithStatus(status, statusListener, statusDisplayOptions, payloadForHolder);
|
||||||
}
|
}
|
||||||
if (concreteNotificaton.getType() == Notification.Type.POLL) {
|
if (concreteNotification.getType() == Notification.Type.POLL) {
|
||||||
holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId()));
|
holder.setPollInfo(accountId.equals(concreteNotification.getAccount().getId()));
|
||||||
} else {
|
} else {
|
||||||
holder.hideStatusInfo();
|
holder.hideStatusInfo();
|
||||||
}
|
}
|
||||||
|
@ -199,7 +199,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
||||||
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
||||||
StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData();
|
StatusViewData.Concrete statusViewData = concreteNotification.getStatusViewData();
|
||||||
if (payloadForHolder == null) {
|
if (payloadForHolder == null) {
|
||||||
if (statusViewData == null) {
|
if (statusViewData == null) {
|
||||||
/* in some very rare cases servers sends null status even though they should not,
|
/* in some very rare cases servers sends null status even though they should not,
|
||||||
|
@ -213,19 +213,19 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
holder.setUsername(status.getAccount().getUsername());
|
holder.setUsername(status.getAccount().getUsername());
|
||||||
holder.setCreatedAt(status.getCreatedAt());
|
holder.setCreatedAt(status.getCreatedAt());
|
||||||
|
|
||||||
if (concreteNotificaton.getType() == Notification.Type.STATUS ||
|
if (concreteNotification.getType() == Notification.Type.STATUS ||
|
||||||
concreteNotificaton.getType() == Notification.Type.UPDATE) {
|
concreteNotification.getType() == Notification.Type.UPDATE) {
|
||||||
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
|
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
|
||||||
} else {
|
} else {
|
||||||
holder.setAvatars(status.getAccount().getAvatar(),
|
holder.setAvatars(status.getAccount().getAvatar(),
|
||||||
concreteNotificaton.getAccount().getAvatar());
|
concreteNotification.getAccount().getAvatar());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.setMessage(concreteNotificaton, statusListener);
|
holder.setMessage(concreteNotification, statusListener);
|
||||||
holder.setupButtons(notificationActionListener,
|
holder.setupButtons(notificationActionListener,
|
||||||
concreteNotificaton.getAccount().getId(),
|
concreteNotification.getAccount().getId(),
|
||||||
concreteNotificaton.getId());
|
concreteNotification.getId());
|
||||||
} else {
|
} else {
|
||||||
if (payloadForHolder instanceof List)
|
if (payloadForHolder instanceof List)
|
||||||
for (Object item : (List) payloadForHolder) {
|
for (Object item : (List) payloadForHolder) {
|
||||||
|
@ -239,16 +239,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_FOLLOW: {
|
case VIEW_TYPE_FOLLOW: {
|
||||||
if (payloadForHolder == null) {
|
if (payloadForHolder == null) {
|
||||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||||
holder.setMessage(concreteNotificaton.getAccount(), concreteNotificaton.getType() == Notification.Type.SIGN_UP);
|
holder.setMessage(concreteNotification.getAccount(), concreteNotification.getType() == Notification.Type.SIGN_UP);
|
||||||
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
|
holder.setupButtons(notificationActionListener, concreteNotification.getAccount().getId());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case VIEW_TYPE_FOLLOW_REQUEST: {
|
case VIEW_TYPE_FOLLOW_REQUEST: {
|
||||||
if (payloadForHolder == null) {
|
if (payloadForHolder == null) {
|
||||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||||
holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
|
holder.setupWithAccount(concreteNotification.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
|
||||||
holder.setupActionListener(accountActionListener, concreteNotificaton.getAccount().getId());
|
holder.setupActionListener(accountActionListener, concreteNotification.getAccount().getId());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -491,7 +491,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
Drawable getIconWithColor(Context context, @DrawableRes int drawable, @ColorRes int color) {
|
Drawable getIconWithColor(Context context, @DrawableRes int drawable, @ColorRes int color) {
|
||||||
Drawable icon = ContextCompat.getDrawable(context, drawable);
|
Drawable icon = ContextCompat.getDrawable(context, drawable);
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
icon.setColorFilter(ContextCompat.getColor(context, color), PorterDuff.Mode.SRC_ATOP);
|
icon.setColorFilter(context.getColor(color), PorterDuff.Mode.SRC_ATOP);
|
||||||
}
|
}
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ package com.keylesspalace.tusky.adapter
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemPollBinding
|
import com.keylesspalace.tusky.databinding.ItemPollBinding
|
||||||
|
@ -97,7 +96,7 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
resultTextView.background.level = level
|
resultTextView.background.level = level
|
||||||
resultTextView.background.setTint(ContextCompat.getColor(resultTextView.context, optionColor))
|
resultTextView.background.setTint(resultTextView.context.getColor(optionColor))
|
||||||
resultTextView.setOnClickListener(resultClickListener)
|
resultTextView.setOnClickListener(resultClickListener)
|
||||||
}
|
}
|
||||||
SINGLE -> {
|
SINGLE -> {
|
||||||
|
|
|
@ -29,10 +29,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.RequestBuilder;
|
import com.bumptech.glide.RequestBuilder;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
|
|
||||||
import com.google.android.material.button.MaterialButton;
|
import com.google.android.material.button.MaterialButton;
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView;
|
||||||
|
import com.google.android.material.shape.CornerFamily;
|
||||||
|
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
|
@ -44,6 +44,7 @@ import com.keylesspalace.tusky.entity.HashTag;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
|
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
|
||||||
|
import com.keylesspalace.tusky.util.AttachmentHelper;
|
||||||
import com.keylesspalace.tusky.util.CardViewMode;
|
import com.keylesspalace.tusky.util.CardViewMode;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
|
@ -100,7 +101,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private LinearLayout cardView;
|
private LinearLayout cardView;
|
||||||
private LinearLayout cardInfo;
|
private LinearLayout cardInfo;
|
||||||
private ImageView cardImage;
|
private ShapeableImageView cardImage;
|
||||||
private TextView cardTitle;
|
private TextView cardTitle;
|
||||||
private TextView cardDescription;
|
private TextView cardDescription;
|
||||||
private TextView cardUrl;
|
private TextView cardUrl;
|
||||||
|
@ -563,7 +564,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
if (i < attachments.size()) {
|
if (i < attachments.size()) {
|
||||||
Attachment attachment = attachments.get(i);
|
Attachment attachment = attachments.get(i);
|
||||||
mediaLabel.setVisibility(View.VISIBLE);
|
mediaLabel.setVisibility(View.VISIBLE);
|
||||||
mediaDescriptions[i] = getAttachmentDescription(context, attachment);
|
mediaDescriptions[i] = AttachmentHelper.getFormattedDescription(attachment, context);
|
||||||
updateMediaLabel(i, sensitive, showingContent);
|
updateMediaLabel(i, sensitive, showingContent);
|
||||||
|
|
||||||
// Set the icon next to the label.
|
// Set the icon next to the label.
|
||||||
|
@ -590,24 +591,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
view.setOnLongClickListener(v -> {
|
view.setOnLongClickListener(v -> {
|
||||||
CharSequence description = getAttachmentDescription(view.getContext(), attachment);
|
CharSequence description = AttachmentHelper.getFormattedDescription(attachment, view.getContext());
|
||||||
Toast.makeText(view.getContext(), description, Toast.LENGTH_LONG).show();
|
Toast.makeText(view.getContext(), description, Toast.LENGTH_LONG).show();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharSequence getAttachmentDescription(Context context, Attachment attachment) {
|
|
||||||
String duration = "";
|
|
||||||
if (attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) {
|
|
||||||
duration = formatDuration(attachment.getMeta().getDuration()) + " ";
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(attachment.getDescription())) {
|
|
||||||
return duration + context.getString(R.string.description_post_media_no_description_placeholder);
|
|
||||||
} else {
|
|
||||||
return duration + attachment.getDescription();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void hideSensitiveMediaWarning() {
|
protected void hideSensitiveMediaWarning() {
|
||||||
sensitiveMediaWarning.setVisibility(View.GONE);
|
sensitiveMediaWarning.setVisibility(View.GONE);
|
||||||
sensitiveMediaShow.setVisibility(View.GONE);
|
sensitiveMediaShow.setVisibility(View.GONE);
|
||||||
|
@ -632,7 +621,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
});
|
});
|
||||||
if (reblogButton != null) {
|
if (reblogButton != null) {
|
||||||
reblogButton.setEventListener((button, buttonState) -> {
|
reblogButton.setEventListener((button, buttonState) -> {
|
||||||
// return true to play animaion
|
// return true to play animation
|
||||||
int position = getBindingAdapterPosition();
|
int position = getBindingAdapterPosition();
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
if (statusDisplayOptions.confirmReblogs()) {
|
if (statusDisplayOptions.confirmReblogs()) {
|
||||||
|
@ -649,7 +638,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
favouriteButton.setEventListener((button, buttonState) -> {
|
favouriteButton.setEventListener((button, buttonState) -> {
|
||||||
// return true to play animaion
|
// return true to play animation
|
||||||
int position = getBindingAdapterPosition();
|
int position = getBindingAdapterPosition();
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
if (statusDisplayOptions.confirmFavourites()) {
|
if (statusDisplayOptions.confirmFavourites()) {
|
||||||
|
@ -710,9 +699,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showConfirmFavouriteDialog(StatusActionListener listener,
|
private void showConfirmFavouriteDialog(StatusActionListener listener,
|
||||||
String statusContent,
|
String statusContent,
|
||||||
boolean buttonState,
|
boolean buttonState,
|
||||||
int position) {
|
int position) {
|
||||||
int okButtonTextId = buttonState ? R.string.action_unfavourite : R.string.action_favourite;
|
int okButtonTextId = buttonState ? R.string.action_unfavourite : R.string.action_favourite;
|
||||||
new AlertDialog.Builder(favouriteButton.getContext())
|
new AlertDialog.Builder(favouriteButton.getContext())
|
||||||
.setMessage(statusContent)
|
.setMessage(statusContent)
|
||||||
|
@ -884,16 +873,16 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
int resource;
|
int resource;
|
||||||
switch (visibility) {
|
switch (visibility) {
|
||||||
case PUBLIC:
|
case PUBLIC:
|
||||||
resource = R.string.description_visiblity_public;
|
resource = R.string.description_visibility_public;
|
||||||
break;
|
break;
|
||||||
case UNLISTED:
|
case UNLISTED:
|
||||||
resource = R.string.description_visiblity_unlisted;
|
resource = R.string.description_visibility_unlisted;
|
||||||
break;
|
break;
|
||||||
case PRIVATE:
|
case PRIVATE:
|
||||||
resource = R.string.description_visiblity_private;
|
resource = R.string.description_visibility_private;
|
||||||
break;
|
break;
|
||||||
case DIRECT:
|
case DIRECT:
|
||||||
resource = R.string.description_visiblity_direct;
|
resource = R.string.description_visibility_direct;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
|
@ -1068,13 +1057,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
// If media previews are disabled, show placeholder for cards as well
|
// If media previews are disabled, show placeholder for cards as well
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||||
|
|
||||||
int topLeftRadius = 0;
|
|
||||||
int topRightRadius = 0;
|
|
||||||
int bottomRightRadius = 0;
|
|
||||||
int bottomLeftRadius = 0;
|
|
||||||
|
|
||||||
int radius = cardImage.getContext().getResources()
|
int radius = cardImage.getContext().getResources()
|
||||||
.getDimensionPixelSize(R.dimen.card_radius);
|
.getDimensionPixelSize(R.dimen.card_radius);
|
||||||
|
ShapeAppearanceModel.Builder cardImageShape = ShapeAppearanceModel.builder();
|
||||||
|
|
||||||
if (card.getWidth() > card.getHeight()) {
|
if (card.getWidth() > card.getHeight()) {
|
||||||
cardView.setOrientation(LinearLayout.VERTICAL);
|
cardView.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
@ -1084,8 +1069,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
topLeftRadius = radius;
|
cardImageShape.setTopLeftCorner(CornerFamily.ROUNDED, radius);
|
||||||
topRightRadius = radius;
|
cardImageShape.setTopRightCorner(CornerFamily.ROUNDED, radius);
|
||||||
} else {
|
} else {
|
||||||
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
@ -1093,19 +1078,21 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
topLeftRadius = radius;
|
cardImageShape.setTopLeftCorner(CornerFamily.ROUNDED, radius);
|
||||||
bottomLeftRadius = radius;
|
cardImageShape.setBottomLeftCorner(CornerFamily.ROUNDED, radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestBuilder<Drawable> builder = Glide.with(cardImage).load(card.getImage());
|
cardImage.setShapeAppearanceModel(cardImageShape.build());
|
||||||
|
|
||||||
|
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||||
|
|
||||||
|
RequestBuilder<Drawable> builder = Glide.with(cardImage.getContext())
|
||||||
|
.load(card.getImage())
|
||||||
|
.dontTransform();
|
||||||
if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
||||||
builder = builder.placeholder(decodeBlurHash(card.getBlurhash()));
|
builder = builder.placeholder(decodeBlurHash(card.getBlurhash()));
|
||||||
}
|
}
|
||||||
builder.transform(
|
builder.into(cardImage);
|
||||||
new CenterCrop(),
|
|
||||||
new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)
|
|
||||||
)
|
|
||||||
.into(cardImage);
|
|
||||||
} else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
} else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
||||||
int radius = cardImage.getContext().getResources()
|
int radius = cardImage.getContext().getResources()
|
||||||
.getDimensionPixelSize(R.dimen.card_radius);
|
.getDimensionPixelSize(R.dimen.card_radius);
|
||||||
|
@ -1116,11 +1103,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
Glide.with(cardImage).load(decodeBlurHash(card.getBlurhash()))
|
|
||||||
.transform(
|
ShapeAppearanceModel cardImageShape = ShapeAppearanceModel.builder()
|
||||||
new CenterCrop(),
|
.setTopLeftCorner(CornerFamily.ROUNDED, radius)
|
||||||
new GranularRoundedCorners(radius, 0, 0, radius)
|
.setBottomLeftCorner(CornerFamily.ROUNDED, radius)
|
||||||
)
|
.build();
|
||||||
|
cardImage.setShapeAppearanceModel(cardImageShape);
|
||||||
|
|
||||||
|
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||||
|
|
||||||
|
Glide.with(cardImage.getContext())
|
||||||
|
.load(decodeBlurHash(card.getBlurhash()))
|
||||||
|
.dontTransform()
|
||||||
.into(cardImage);
|
.into(cardImage);
|
||||||
} else {
|
} else {
|
||||||
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
@ -1129,16 +1123,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardImage.setImageResource(R.drawable.card_image_placeholder);
|
|
||||||
|
cardImage.setShapeAppearanceModel(new ShapeAppearanceModel());
|
||||||
|
|
||||||
|
cardImage.setScaleType(ImageView.ScaleType.CENTER);
|
||||||
|
|
||||||
|
Glide.with(cardImage.getContext())
|
||||||
|
.load(ContextCompat.getDrawable(cardImage.getContext(), R.drawable.card_image_placeholder))
|
||||||
|
.into(cardImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl());
|
View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl());
|
||||||
View.OnClickListener openImage = v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbed_url()));
|
|
||||||
|
|
||||||
cardInfo.setOnClickListener(visitLink);
|
cardView.setOnClickListener(visitLink);
|
||||||
// View embedded photos in our image viewer instead of opening the browser
|
// View embedded photos in our image viewer instead of opening the browser
|
||||||
cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbed_url()) ?
|
cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbedUrl()) ?
|
||||||
openImage :
|
v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbedUrl())) :
|
||||||
visitLink);
|
visitLink);
|
||||||
|
|
||||||
cardView.setClipToOutline(true);
|
cardView.setClipToOutline(true);
|
||||||
|
@ -1168,13 +1168,4 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
bookmarkButton.setVisibility(visibility);
|
bookmarkButton.setVisibility(visibility);
|
||||||
moreButton.setVisibility(visibility);
|
moreButton.setVisibility(visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String formatDuration(double durationInSeconds) {
|
|
||||||
int seconds = (int) Math.round(durationInSeconds) % 60;
|
|
||||||
int minutes = (int) durationInSeconds % 3600 / 60;
|
|
||||||
int hours = (int) durationInSeconds / 3600;
|
|
||||||
|
|
||||||
return String.format("%d:%02d:%02d", hours, minutes, seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,12 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
private TextView reblogs;
|
private final TextView reblogs;
|
||||||
private TextView favourites;
|
private final TextView favourites;
|
||||||
private View infoDivider;
|
private final View infoDivider;
|
||||||
|
|
||||||
StatusDetailedViewHolder(View view) {
|
public StatusDetailedViewHolder(View view) {
|
||||||
super(view);
|
super(view);
|
||||||
reblogs = view.findViewById(R.id.status_reblogs);
|
reblogs = view.findViewById(R.id.status_reblogs);
|
||||||
favourites = view.findViewById(R.id.status_favourites);
|
favourites = view.findViewById(R.id.status_favourites);
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
/* 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,7 +31,6 @@ import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.Px
|
import androidx.annotation.Px
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
@ -171,7 +170,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
*/
|
*/
|
||||||
private fun loadResources() {
|
private fun loadResources() {
|
||||||
toolbarColor = ThemeUtils.getColor(this, R.attr.colorSurface)
|
toolbarColor = ThemeUtils.getColor(this, R.attr.colorSurface)
|
||||||
statusBarColorTransparent = ContextCompat.getColor(this, R.color.transparent_statusbar_background)
|
statusBarColorTransparent = getColor(R.color.transparent_statusbar_background)
|
||||||
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark)
|
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark)
|
||||||
avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size)
|
avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size)
|
||||||
titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height)
|
titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height)
|
||||||
|
|
|
@ -35,7 +35,7 @@ class AccountPagerAdapter(
|
||||||
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false)
|
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false)
|
||||||
1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false)
|
1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false)
|
||||||
2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
|
2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
|
||||||
3 -> AccountMediaFragment.newInstance(accountId, false)
|
3 -> AccountMediaFragment.newInstance(accountId)
|
||||||
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
|
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
/* Copyright 2022 Tusky Contributors
|
||||||
*
|
*
|
||||||
* This file is a part of Tusky.
|
* This file is a part of Tusky.
|
||||||
*
|
*
|
||||||
|
@ -15,41 +15,33 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.account.media
|
package com.keylesspalace.tusky.components.account.media
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import autodispose2.androidx.lifecycle.autoDispose
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Status
|
|
||||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.openLink
|
import com.keylesspalace.tusky.util.openLink
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.view.SquareImageView
|
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import io.reactivex.rxjava3.core.SingleObserver
|
import kotlinx.coroutines.launch
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Random
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,192 +50,107 @@ import javax.inject.Inject
|
||||||
* Fragment with multiple columns of media previews for the specified account.
|
* Fragment with multiple columns of media previews for the specified account.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable {
|
class AccountMediaFragment :
|
||||||
|
Fragment(R.layout.fragment_timeline),
|
||||||
|
RefreshableFragment,
|
||||||
|
Injectable {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var api: MastodonApi
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
||||||
|
|
||||||
private lateinit var accountId: String
|
private val viewModel: AccountMediaViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
private val adapter = MediaGridAdapter()
|
private lateinit var adapter: AccountMediaGridAdapter
|
||||||
private val statuses = mutableListOf<Status>()
|
|
||||||
private var fetchingStatus = FetchingStatus.NOT_FETCHING
|
|
||||||
|
|
||||||
private var isSwipeToRefreshEnabled: Boolean = true
|
|
||||||
private var needToRefresh = false
|
|
||||||
|
|
||||||
private val callback = object : SingleObserver<Response<List<Status>>> {
|
|
||||||
override fun onError(t: Throwable) {
|
|
||||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
|
||||||
|
|
||||||
if (isAdded) {
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
binding.progressBar.visibility = View.GONE
|
|
||||||
binding.topProgressBar.hide()
|
|
||||||
binding.statusView.show()
|
|
||||||
if (t is IOException) {
|
|
||||||
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
|
||||||
doInitialLoadingIfNeeded()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
|
||||||
doInitialLoadingIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Failed to fetch account media", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(response: Response<List<Status>>) {
|
|
||||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
|
||||||
if (isAdded) {
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
binding.progressBar.visibility = View.GONE
|
|
||||||
binding.topProgressBar.hide()
|
|
||||||
|
|
||||||
val body = response.body()
|
|
||||||
body?.let { fetched ->
|
|
||||||
statuses.addAll(0, fetched)
|
|
||||||
// flatMap requires iterable but I don't want to box each array into list
|
|
||||||
val result = mutableListOf<AttachmentViewData>()
|
|
||||||
for (status in fetched) {
|
|
||||||
result.addAll(AttachmentViewData.list(status))
|
|
||||||
}
|
|
||||||
adapter.addTop(result)
|
|
||||||
if (result.isNotEmpty())
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
|
|
||||||
if (statuses.isEmpty()) {
|
|
||||||
binding.statusView.show()
|
|
||||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSubscribe(d: Disposable) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val bottomCallback = object : SingleObserver<Response<List<Status>>> {
|
|
||||||
override fun onError(t: Throwable) {
|
|
||||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
|
||||||
|
|
||||||
Log.d(TAG, "Failed to fetch account media", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(response: Response<List<Status>>) {
|
|
||||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
|
||||||
val body = response.body()
|
|
||||||
body?.let { fetched ->
|
|
||||||
Log.d(TAG, "fetched ${fetched.size} statuses")
|
|
||||||
if (fetched.isNotEmpty()) Log.d(TAG, "first: ${fetched.first().id}, last: ${fetched.last().id}")
|
|
||||||
statuses.addAll(fetched)
|
|
||||||
Log.d(TAG, "now there are ${statuses.size} statuses")
|
|
||||||
// flatMap requires iterable but I don't want to box each array into list
|
|
||||||
val result = mutableListOf<AttachmentViewData>()
|
|
||||||
for (status in fetched) {
|
|
||||||
result.addAll(AttachmentViewData.list(status))
|
|
||||||
}
|
|
||||||
adapter.addBottom(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSubscribe(d: Disposable) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) == true
|
viewModel.accountId = arguments?.getString(ACCOUNT_ID_ARG)!!
|
||||||
accountId = arguments?.getString(ACCOUNT_ID_ARG)!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
val alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia
|
||||||
|
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||||
|
val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true)
|
||||||
|
|
||||||
|
adapter = AccountMediaGridAdapter(
|
||||||
|
alwaysShowSensitiveMedia = alwaysShowSensitiveMedia,
|
||||||
|
useBlurhash = useBlurhash,
|
||||||
|
context = view.context,
|
||||||
|
onAttachmentClickListener = ::onAttachmentClick
|
||||||
|
)
|
||||||
|
|
||||||
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
||||||
val layoutManager = GridLayoutManager(view.context, columnCount)
|
val imageSpacing = view.context.resources.getDimensionPixelSize(R.dimen.profile_media_spacing)
|
||||||
|
|
||||||
adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground)
|
binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(columnCount, imageSpacing, 0))
|
||||||
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
if (isSwipeToRefreshEnabled) {
|
binding.swipeRefreshLayout.isEnabled = false
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
|
||||||
}
|
|
||||||
binding.statusView.visibility = View.GONE
|
binding.statusView.visibility = View.GONE
|
||||||
|
|
||||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewModel.media.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) {
|
adapter.addLoadStateListener { loadState ->
|
||||||
if (dy > 0) {
|
binding.statusView.hide()
|
||||||
val itemCount = layoutManager.itemCount
|
binding.progressBar.hide()
|
||||||
val lastItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
|
||||||
if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) {
|
if (adapter.itemCount == 0) {
|
||||||
statuses.lastOrNull()?.let { (id) ->
|
when (loadState.refresh) {
|
||||||
Log.d(TAG, "Requesting statuses with max_id: $id, (bottom)")
|
is LoadState.NotLoading -> {
|
||||||
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
|
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||||
api.accountStatuses(accountId, id, null, null, null, true, null)
|
binding.statusView.show()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
||||||
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
|
|
||||||
.subscribe(bottomCallback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is LoadState.Error -> {
|
||||||
|
binding.statusView.show()
|
||||||
|
|
||||||
|
if ((loadState.refresh as LoadState.Error).error is IOException) {
|
||||||
|
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, null)
|
||||||
|
} else {
|
||||||
|
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LoadState.Loading -> {
|
||||||
|
binding.progressBar.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
doInitialLoadingIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refresh() {
|
|
||||||
binding.statusView.hide()
|
|
||||||
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
|
|
||||||
if (statuses.isEmpty()) {
|
|
||||||
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
|
||||||
api.accountStatuses(accountId, null, null, null, null, true, null)
|
|
||||||
} else {
|
|
||||||
fetchingStatus = FetchingStatus.REFRESHING
|
|
||||||
api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null)
|
|
||||||
}.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
|
||||||
.subscribe(callback)
|
|
||||||
|
|
||||||
if (!isSwipeToRefreshEnabled)
|
|
||||||
binding.topProgressBar.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doInitialLoadingIfNeeded() {
|
|
||||||
if (isAdded) {
|
|
||||||
binding.statusView.hide()
|
|
||||||
}
|
}
|
||||||
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
|
|
||||||
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
|
||||||
api.accountStatuses(accountId, null, null, null, null, true, null)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
|
|
||||||
.subscribe(callback)
|
|
||||||
} else if (needToRefresh)
|
|
||||||
refresh()
|
|
||||||
needToRefresh = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun viewMedia(items: List<AttachmentViewData>, currentIndex: Int, view: View?) {
|
private fun onAttachmentClick(selected: AttachmentViewData, view: View) {
|
||||||
|
if (!selected.isRevealed) {
|
||||||
|
viewModel.revealAttachment(selected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val attachmentsFromSameStatus = viewModel.attachmentData.filter { attachmentViewData ->
|
||||||
|
attachmentViewData.statusId == selected.statusId
|
||||||
|
}
|
||||||
|
val currentIndex = attachmentsFromSameStatus.indexOf(selected)
|
||||||
|
|
||||||
when (items[currentIndex].attachment.type) {
|
when (selected.attachment.type) {
|
||||||
Attachment.Type.IMAGE,
|
Attachment.Type.IMAGE,
|
||||||
Attachment.Type.GIFV,
|
Attachment.Type.GIFV,
|
||||||
Attachment.Type.VIDEO,
|
Attachment.Type.VIDEO,
|
||||||
Attachment.Type.AUDIO -> {
|
Attachment.Type.AUDIO -> {
|
||||||
val intent = ViewMediaActivity.newIntent(context, items, currentIndex)
|
val intent = ViewMediaActivity.newIntent(context, attachmentsFromSameStatus, currentIndex)
|
||||||
if (view != null && activity != null) {
|
if (activity != null) {
|
||||||
val url = items[currentIndex].attachment.url
|
val url = selected.attachment.url
|
||||||
ViewCompat.setTransitionName(view, url)
|
ViewCompat.setTransitionName(view, url)
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url)
|
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url)
|
||||||
startActivity(intent, options.toBundle())
|
startActivity(intent, options.toBundle())
|
||||||
|
@ -252,96 +159,26 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Attachment.Type.UNKNOWN -> {
|
Attachment.Type.UNKNOWN -> {
|
||||||
context?.openLink(items[currentIndex].attachment.url)
|
context?.openLink(selected.attachment.url)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class FetchingStatus {
|
|
||||||
NOT_FETCHING, INITIAL_FETCHING, FETCHING_BOTTOM, REFRESHING
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class MediaGridAdapter :
|
|
||||||
RecyclerView.Adapter<MediaGridAdapter.MediaViewHolder>() {
|
|
||||||
|
|
||||||
var baseItemColor = Color.BLACK
|
|
||||||
|
|
||||||
private val items = mutableListOf<AttachmentViewData>()
|
|
||||||
private val itemBgBaseHSV = FloatArray(3)
|
|
||||||
private val random = Random()
|
|
||||||
|
|
||||||
fun addTop(newItems: List<AttachmentViewData>) {
|
|
||||||
items.addAll(0, newItems)
|
|
||||||
notifyItemRangeInserted(0, newItems.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addBottom(newItems: List<AttachmentViewData>) {
|
|
||||||
if (newItems.isEmpty()) return
|
|
||||||
|
|
||||||
val oldLen = items.size
|
|
||||||
items.addAll(newItems)
|
|
||||||
notifyItemRangeInserted(oldLen, newItems.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToRecyclerView(recycler_view: RecyclerView) {
|
|
||||||
val hsv = FloatArray(3)
|
|
||||||
Color.colorToHSV(baseItemColor, hsv)
|
|
||||||
super.onAttachedToRecyclerView(recycler_view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
|
||||||
val view = SquareImageView(parent.context)
|
|
||||||
view.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
||||||
return MediaViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = items.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
|
|
||||||
itemBgBaseHSV[2] = random.nextFloat() * (1f - 0.3f) + 0.3f
|
|
||||||
holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
|
||||||
val item = items[position]
|
|
||||||
|
|
||||||
Glide.with(holder.imageView)
|
|
||||||
.load(item.attachment.previewUrl)
|
|
||||||
.centerInside()
|
|
||||||
.into(holder.imageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class MediaViewHolder(val imageView: ImageView) :
|
|
||||||
RecyclerView.ViewHolder(imageView),
|
|
||||||
View.OnClickListener {
|
|
||||||
init {
|
|
||||||
itemView.setOnClickListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// saving some allocations
|
|
||||||
override fun onClick(v: View?) {
|
|
||||||
viewMedia(items, bindingAdapterPosition, imageView)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refreshContent() {
|
override fun refreshContent() {
|
||||||
if (isAdded)
|
adapter.refresh()
|
||||||
refresh()
|
|
||||||
else
|
|
||||||
needToRefresh = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
|
||||||
fun newInstance(accountId: String, enableSwipeToRefresh: Boolean = true): AccountMediaFragment {
|
fun newInstance(accountId: String): AccountMediaFragment {
|
||||||
val fragment = AccountMediaFragment()
|
val fragment = AccountMediaFragment()
|
||||||
val args = Bundle()
|
val args = Bundle(1)
|
||||||
args.putString(ACCOUNT_ID_ARG, accountId)
|
args.putString(ACCOUNT_ID_ARG, accountId)
|
||||||
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh)
|
|
||||||
fragment.arguments = args
|
fragment.arguments = args
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val ACCOUNT_ID_ARG = "account_id"
|
private const val ACCOUNT_ID_ARG = "account_id"
|
||||||
private const val TAG = "AccountMediaFragment"
|
private const val TAG = "AccountMediaFragment"
|
||||||
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
package com.keylesspalace.tusky.components.account.media
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.view.setPadding
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.ItemAccountMediaBinding
|
||||||
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
|
import com.keylesspalace.tusky.util.decodeBlurHash
|
||||||
|
import com.keylesspalace.tusky.util.getFormattedDescription
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
|
class AccountMediaGridAdapter(
|
||||||
|
private val alwaysShowSensitiveMedia: Boolean,
|
||||||
|
private val useBlurhash: Boolean,
|
||||||
|
context: Context,
|
||||||
|
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit
|
||||||
|
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
|
||||||
|
object : DiffUtil.ItemCallback<AttachmentViewData>() {
|
||||||
|
override fun areItemsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
|
||||||
|
return oldItem.attachment.id == newItem.attachment.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val baseItemBackgroundColor = ThemeUtils.getColor(context, R.attr.colorSurface)
|
||||||
|
private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator)
|
||||||
|
private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp)
|
||||||
|
|
||||||
|
private val itemBgBaseHSV = FloatArray(3)
|
||||||
|
private val random = Random()
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountMediaBinding> {
|
||||||
|
val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV)
|
||||||
|
itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f
|
||||||
|
binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
||||||
|
return BindingHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BindingHolder<ItemAccountMediaBinding>, position: Int) {
|
||||||
|
val context = holder.binding.root.context
|
||||||
|
getItem(position)?.let { item ->
|
||||||
|
|
||||||
|
val imageView = holder.binding.accountMediaImageView
|
||||||
|
val overlay = holder.binding.accountMediaImageViewOverlay
|
||||||
|
|
||||||
|
val blurhash = item.attachment.blurhash
|
||||||
|
val placeholder = if (useBlurhash && blurhash != null) {
|
||||||
|
decodeBlurHash(context, blurhash)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.attachment.type == Attachment.Type.AUDIO) {
|
||||||
|
overlay.hide()
|
||||||
|
|
||||||
|
imageView.setPadding(context.resources.getDimensionPixelSize(R.dimen.profile_media_audio_icon_padding))
|
||||||
|
|
||||||
|
Glide.with(imageView)
|
||||||
|
.load(R.drawable.ic_music_box_preview_24dp)
|
||||||
|
.centerInside()
|
||||||
|
.into(imageView)
|
||||||
|
|
||||||
|
imageView.contentDescription = item.attachment.getFormattedDescription(context)
|
||||||
|
} else if (item.sensitive && !item.isRevealed && !alwaysShowSensitiveMedia) {
|
||||||
|
overlay.show()
|
||||||
|
overlay.setImageDrawable(mediaHiddenDrawable)
|
||||||
|
|
||||||
|
imageView.setPadding(0)
|
||||||
|
|
||||||
|
Glide.with(imageView)
|
||||||
|
.load(placeholder)
|
||||||
|
.centerInside()
|
||||||
|
.into(imageView)
|
||||||
|
|
||||||
|
imageView.contentDescription = imageView.context.getString(R.string.post_media_hidden_title)
|
||||||
|
} else {
|
||||||
|
if (item.attachment.type == Attachment.Type.VIDEO || item.attachment.type == Attachment.Type.GIFV) {
|
||||||
|
overlay.show()
|
||||||
|
overlay.setImageDrawable(videoIndicator)
|
||||||
|
} else {
|
||||||
|
overlay.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.setPadding(0)
|
||||||
|
|
||||||
|
Glide.with(imageView)
|
||||||
|
.asBitmap()
|
||||||
|
.load(item.attachment.previewUrl)
|
||||||
|
.placeholder(placeholder)
|
||||||
|
.centerInside()
|
||||||
|
.into(imageView)
|
||||||
|
|
||||||
|
imageView.contentDescription = item.attachment.getFormattedDescription(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.root.setOnClickListener {
|
||||||
|
onAttachmentClickListener(item, imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.root.setOnLongClickListener { view ->
|
||||||
|
val description = item.attachment.getFormattedDescription(view.context)
|
||||||
|
Toast.makeText(view.context, description, Toast.LENGTH_LONG).show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/* Copyright 2022 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.account.media
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
|
|
||||||
|
class AccountMediaPagingSource(
|
||||||
|
private val viewModel: AccountMediaViewModel
|
||||||
|
) : PagingSource<String, AttachmentViewData>() {
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<String, AttachmentViewData>): String? = null
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, AttachmentViewData> {
|
||||||
|
|
||||||
|
return if (params is LoadParams.Refresh) {
|
||||||
|
val list = viewModel.attachmentData.toList()
|
||||||
|
LoadResult.Page(list, null, list.lastOrNull()?.statusId)
|
||||||
|
} else {
|
||||||
|
LoadResult.Page(emptyList(), null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/* Copyright 2022 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.account.media
|
||||||
|
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.LoadType
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.paging.RemoteMediator
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
|
import retrofit2.HttpException
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
class AccountMediaRemoteMediator(
|
||||||
|
private val api: MastodonApi,
|
||||||
|
private val viewModel: AccountMediaViewModel
|
||||||
|
) : RemoteMediator<String, AttachmentViewData>() {
|
||||||
|
|
||||||
|
override suspend fun load(
|
||||||
|
loadType: LoadType,
|
||||||
|
state: PagingState<String, AttachmentViewData>
|
||||||
|
): MediatorResult {
|
||||||
|
|
||||||
|
try {
|
||||||
|
val statusResponse = when (loadType) {
|
||||||
|
LoadType.REFRESH -> {
|
||||||
|
api.accountStatuses(viewModel.accountId, onlyMedia = true)
|
||||||
|
}
|
||||||
|
LoadType.PREPEND -> {
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
}
|
||||||
|
LoadType.APPEND -> {
|
||||||
|
val maxId = state.lastItemOrNull()?.statusId
|
||||||
|
if (maxId != null) {
|
||||||
|
api.accountStatuses(viewModel.accountId, maxId = maxId, onlyMedia = true)
|
||||||
|
} else {
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val statuses = statusResponse.body()
|
||||||
|
if (!statusResponse.isSuccessful || statuses == null) {
|
||||||
|
return MediatorResult.Error(HttpException(statusResponse))
|
||||||
|
}
|
||||||
|
|
||||||
|
val attachments = statuses.flatMap { status ->
|
||||||
|
AttachmentViewData.list(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadType == LoadType.REFRESH) {
|
||||||
|
viewModel.attachmentData.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.attachmentData.addAll(attachments)
|
||||||
|
|
||||||
|
viewModel.currentSource?.invalidate()
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return ifExpected(e) {
|
||||||
|
MediatorResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/* Copyright 2022 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.account.media
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class AccountMediaViewModel @Inject constructor (
|
||||||
|
api: MastodonApi
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
lateinit var accountId: String
|
||||||
|
|
||||||
|
val attachmentData: MutableList<AttachmentViewData> = mutableListOf()
|
||||||
|
|
||||||
|
var currentSource: AccountMediaPagingSource? = null
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
val media = Pager(
|
||||||
|
config = PagingConfig(
|
||||||
|
pageSize = LOAD_AT_ONCE,
|
||||||
|
prefetchDistance = LOAD_AT_ONCE * 2
|
||||||
|
),
|
||||||
|
pagingSourceFactory = {
|
||||||
|
AccountMediaPagingSource(
|
||||||
|
viewModel = this
|
||||||
|
).also { source ->
|
||||||
|
currentSource = source
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remoteMediator = AccountMediaRemoteMediator(api, this)
|
||||||
|
).flow
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
fun revealAttachment(viewData: AttachmentViewData) {
|
||||||
|
val position = attachmentData.indexOfFirst { oldViewData -> oldViewData.id == viewData.id }
|
||||||
|
attachmentData[position] = viewData.copy(isRevealed = true)
|
||||||
|
currentSource?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOAD_AT_ONCE = 30
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/* Copyright 2022 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.account.media
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
|
||||||
|
|
||||||
|
class GridSpacingItemDecoration(
|
||||||
|
private val spanCount: Int,
|
||||||
|
private val spacing: Int,
|
||||||
|
private val topOffset: Int
|
||||||
|
) : ItemDecoration() {
|
||||||
|
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
val position = parent.getChildAdapterPosition(view) // item position
|
||||||
|
if (position < topOffset) return
|
||||||
|
|
||||||
|
val column = (position - topOffset) % spanCount // item column
|
||||||
|
|
||||||
|
outRect.left = column * spacing / spanCount // column * ((1f / spanCount) * spacing)
|
||||||
|
outRect.right =
|
||||||
|
spacing - (column + 1) * spacing / spanCount // spacing - (column + 1) * ((1f / spanCount) * spacing)
|
||||||
|
if (position - topOffset >= spanCount) {
|
||||||
|
outRect.top = spacing // item top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.keylesspalace.tusky.view
|
package com.keylesspalace.tusky.components.account.media
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
|
@ -34,6 +34,7 @@ import com.keylesspalace.tusky.util.EmojiSpan
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
interface AnnouncementActionListener : LinkListener {
|
interface AnnouncementActionListener : LinkListener {
|
||||||
|
@ -73,6 +74,9 @@ class AnnouncementAdapter(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hide button if announcement badge limit is already reached
|
||||||
|
addReactionChip.visible(item.reactions.size < 8)
|
||||||
|
|
||||||
item.reactions.forEachIndexed { i, reaction ->
|
item.reactions.forEachIndexed { i, reaction ->
|
||||||
(
|
(
|
||||||
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
||||||
|
|
|
@ -35,24 +35,27 @@ import android.view.KeyEvent
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.core.view.ContentInfoCompat
|
import androidx.core.view.ContentInfoCompat
|
||||||
import androidx.core.view.OnReceiveContentListener
|
import androidx.core.view.OnReceiveContentListener
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
@ -66,8 +69,10 @@ import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
||||||
|
import com.keylesspalace.tusky.adapter.LocaleAdapter
|
||||||
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
||||||
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
|
import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog
|
||||||
|
import com.keylesspalace.tusky.components.compose.dialog.makeFocusDialog
|
||||||
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
|
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
|
||||||
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
|
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
|
||||||
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
|
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
|
||||||
|
@ -85,25 +90,27 @@ import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.PickMediaFiles
|
import com.keylesspalace.tusky.util.PickMediaFiles
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.afterTextChanged
|
import com.keylesspalace.tusky.util.afterTextChanged
|
||||||
import com.keylesspalace.tusky.util.combineLiveData
|
|
||||||
import com.keylesspalace.tusky.util.combineOptionalLiveData
|
|
||||||
import com.keylesspalace.tusky.util.getMediaSize
|
import com.keylesspalace.tusky.util.getMediaSize
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.highlightSpans
|
import com.keylesspalace.tusky.util.highlightSpans
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
import com.keylesspalace.tusky.util.modernLanguageCode
|
||||||
import com.keylesspalace.tusky.util.onTextChanged
|
import com.keylesspalace.tusky.util.onTextChanged
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.keylesspalace.tusky.util.withLifecycleContext
|
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.text.DecimalFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
@ -116,7 +123,8 @@ class ComposeActivity :
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
Injectable,
|
Injectable,
|
||||||
OnReceiveContentListener,
|
OnReceiveContentListener,
|
||||||
ComposeScheduleView.OnTimeSetListener {
|
ComposeScheduleView.OnTimeSetListener,
|
||||||
|
CaptionDialog.Listener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
@ -138,8 +146,7 @@ class ComposeActivity :
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityComposeBinding::inflate)
|
private val binding by viewBinding(ActivityComposeBinding::inflate)
|
||||||
|
|
||||||
private val maxUploadMediaNumber = 4
|
private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS
|
||||||
private var mediaCount = 0
|
|
||||||
|
|
||||||
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -147,7 +154,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
|
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
|
||||||
if (mediaCount + uris.size > maxUploadMediaNumber) {
|
if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) {
|
||||||
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
uris.forEach { uri ->
|
uris.forEach { uri ->
|
||||||
|
@ -169,6 +176,7 @@ class ComposeActivity :
|
||||||
uriNew,
|
uriNew,
|
||||||
size,
|
size,
|
||||||
itemOld.description,
|
itemOld.description,
|
||||||
|
null, // Intentionally reset focus when cropping
|
||||||
itemOld
|
itemOld
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -212,8 +220,12 @@ class ComposeActivity :
|
||||||
val mediaAdapter = MediaPreviewAdapter(
|
val mediaAdapter = MediaPreviewAdapter(
|
||||||
this,
|
this,
|
||||||
onAddCaption = { item ->
|
onAddCaption = { item ->
|
||||||
makeCaptionDialog(item.description, item.uri) { newDescription ->
|
CaptionDialog.newInstance(item.localId, item.description, item.uri)
|
||||||
viewModel.updateDescription(item.localId, newDescription)
|
.show(supportFragmentManager, "caption_dialog")
|
||||||
|
},
|
||||||
|
onAddFocus = { item ->
|
||||||
|
makeFocusDialog(item.focus, item.uri) { newFocus ->
|
||||||
|
viewModel.updateFocus(item.localId, newFocus)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEditImage = this::editImageInQueue,
|
onEditImage = this::editImageInQueue,
|
||||||
|
@ -224,17 +236,25 @@ class ComposeActivity :
|
||||||
binding.composeMediaPreviewBar.adapter = mediaAdapter
|
binding.composeMediaPreviewBar.adapter = mediaAdapter
|
||||||
binding.composeMediaPreviewBar.itemAnimator = null
|
binding.composeMediaPreviewBar.itemAnimator = null
|
||||||
|
|
||||||
subscribeToUpdates(mediaAdapter)
|
|
||||||
setupButtons()
|
setupButtons()
|
||||||
|
subscribeToUpdates(mediaAdapter)
|
||||||
photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY)
|
|
||||||
|
|
||||||
/* If the composer is started up as a reply to another post, override the "starting" state
|
/* If the composer is started up as a reply to another post, override the "starting" state
|
||||||
* based on what the intent from the reply request passes. */
|
* based on what the intent from the reply request passes. */
|
||||||
|
|
||||||
val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
|
val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
|
||||||
|
|
||||||
viewModel.setup(composeOptions)
|
viewModel.setup(composeOptions)
|
||||||
|
|
||||||
|
if (accountManager.shouldDisplaySelfUsername(this)) {
|
||||||
|
binding.composeUsernameView.text = getString(
|
||||||
|
R.string.compose_active_account_description,
|
||||||
|
activeAccount.fullName
|
||||||
|
)
|
||||||
|
binding.composeUsernameView.show()
|
||||||
|
} else {
|
||||||
|
binding.composeUsernameView.hide()
|
||||||
|
}
|
||||||
|
|
||||||
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
||||||
val statusContent = composeOptions?.content
|
val statusContent = composeOptions?.content
|
||||||
if (!statusContent.isNullOrEmpty()) {
|
if (!statusContent.isNullOrEmpty()) {
|
||||||
|
@ -245,11 +265,32 @@ class ComposeActivity :
|
||||||
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
|
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupLanguageSpinner(getInitialLanguage(composeOptions?.language))
|
||||||
setupComposeField(preferences, viewModel.startingText)
|
setupComposeField(preferences, viewModel.startingText)
|
||||||
setupContentWarningField(composeOptions?.contentWarning)
|
setupContentWarningField(composeOptions?.contentWarning)
|
||||||
setupPollView()
|
setupPollView()
|
||||||
applyShareIntent(intent, savedInstanceState)
|
applyShareIntent(intent, savedInstanceState)
|
||||||
viewModel.setupComplete.value = true
|
|
||||||
|
/* Finally, overwrite state with data from saved instance state. */
|
||||||
|
savedInstanceState?.let {
|
||||||
|
photoUploadUri = it.getParcelable(PHOTO_UPLOAD_URI_KEY)
|
||||||
|
|
||||||
|
(it.getSerializable(VISIBILITY_KEY) as Status.Visibility).apply {
|
||||||
|
setStatusVisibility(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.getBoolean(CONTENT_WARNING_VISIBLE_KEY).apply {
|
||||||
|
viewModel.contentWarningChanged(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.getString(SCHEDULED_TIME_KEY)?.let { time ->
|
||||||
|
viewModel.updateScheduledAt(time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.composeEditField.post {
|
||||||
|
binding.composeEditField.requestFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) {
|
private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) {
|
||||||
|
@ -363,36 +404,48 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) {
|
private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) {
|
||||||
withLifecycleContext {
|
lifecycleScope.launch {
|
||||||
viewModel.instanceInfo.observe { instanceData ->
|
viewModel.instanceInfo.collect { instanceData ->
|
||||||
maximumTootCharacters = instanceData.maxChars
|
maximumTootCharacters = instanceData.maxChars
|
||||||
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
||||||
|
maxUploadMediaNumber = instanceData.maxMediaAttachments
|
||||||
updateVisibleCharactersLeft()
|
updateVisibleCharactersLeft()
|
||||||
}
|
}
|
||||||
viewModel.emoji.observe { emoji -> setEmojiList(emoji) }
|
}
|
||||||
combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning ->
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.emoji.collect(::setEmojiList)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive ->
|
||||||
updateSensitiveMediaToggle(markSensitive, showContentWarning)
|
updateSensitiveMediaToggle(markSensitive, showContentWarning)
|
||||||
showContentWarning(showContentWarning)
|
showContentWarning(showContentWarning)
|
||||||
}.subscribe()
|
}.collect()
|
||||||
viewModel.statusVisibility.observe { visibility ->
|
}
|
||||||
setStatusVisibility(visibility)
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
viewModel.media.collect { media ->
|
|
||||||
mediaAdapter.submitList(media)
|
|
||||||
if (media.size != mediaCount) {
|
|
||||||
mediaCount = media.size
|
|
||||||
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
|
|
||||||
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.poll.observe { poll ->
|
lifecycleScope.launch {
|
||||||
|
viewModel.statusVisibility.collect(::setStatusVisibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.media.collect { media ->
|
||||||
|
mediaAdapter.submitList(media)
|
||||||
|
|
||||||
|
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
|
||||||
|
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value, viewModel.showContentWarning.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.poll.collect { poll ->
|
||||||
binding.pollPreview.visible(poll != null)
|
binding.pollPreview.visible(poll != null)
|
||||||
poll?.let(binding.pollPreview::setPoll)
|
poll?.let(binding.pollPreview::setPoll)
|
||||||
}
|
}
|
||||||
viewModel.scheduledAt.observe { scheduledAt ->
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.scheduledAt.collect { scheduledAt ->
|
||||||
if (scheduledAt == null) {
|
if (scheduledAt == null) {
|
||||||
binding.composeScheduleView.resetSchedule()
|
binding.composeScheduleView.resetSchedule()
|
||||||
} else {
|
} else {
|
||||||
|
@ -400,25 +453,26 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
updateScheduleButton()
|
updateScheduleButton()
|
||||||
}
|
}
|
||||||
combineOptionalLiveData(viewModel.media.asLiveData(), viewModel.poll) { media, poll ->
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.media.combine(viewModel.poll) { media, poll ->
|
||||||
val active = poll == null &&
|
val active = poll == null &&
|
||||||
media!!.size != 4 &&
|
media.size < maxUploadMediaNumber &&
|
||||||
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
||||||
enableButton(binding.composeAddMediaButton, active, active)
|
enableButton(binding.composeAddMediaButton, active, active)
|
||||||
enablePollButton(media.isNullOrEmpty())
|
enablePollButton(media.isEmpty())
|
||||||
}.subscribe()
|
}.collect()
|
||||||
viewModel.uploadError.observe { throwable ->
|
}
|
||||||
Log.w(TAG, "media upload failed", throwable)
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.uploadError.collect { throwable ->
|
||||||
if (throwable is UploadServerError) {
|
if (throwable is UploadServerError) {
|
||||||
displayTransientError(throwable.errorMessage)
|
displayTransientError(throwable.errorMessage)
|
||||||
} else {
|
} else {
|
||||||
displayTransientError(R.string.error_media_upload_sending)
|
displayTransientError(R.string.error_media_upload_sending)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.setupComplete.observe {
|
|
||||||
// Focus may have changed during view model setup, ensure initial focus is on the edit field
|
|
||||||
binding.composeEditField.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,6 +513,105 @@ class ComposeActivity :
|
||||||
binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
|
binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
|
||||||
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
||||||
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(
|
||||||
|
this,
|
||||||
|
object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
) {
|
||||||
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mergeLocaleListCompat(list: MutableList<Locale>, localeListCompat: LocaleListCompat) {
|
||||||
|
for (index in 0 until localeListCompat.size()) {
|
||||||
|
val locale = localeListCompat[index]
|
||||||
|
if (locale != null && list.none { locale.language == it.language }) {
|
||||||
|
list.add(locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the locale whose code matches the given language is first in the list
|
||||||
|
private fun ensureLanguageIsFirst(locales: MutableList<Locale>, language: String) {
|
||||||
|
var currentLocaleIndex = locales.indexOfFirst { it.language == language }
|
||||||
|
if (currentLocaleIndex < 0) {
|
||||||
|
// Recheck against modern language codes
|
||||||
|
// This should only happen when replying or when the per-account post language is set
|
||||||
|
// to a modern code
|
||||||
|
currentLocaleIndex = locales.indexOfFirst { it.modernLanguageCode == language }
|
||||||
|
|
||||||
|
if (currentLocaleIndex < 0) {
|
||||||
|
// This can happen when:
|
||||||
|
// - Your per-account posting language is set to one android doesn't know (e.g. toki pona)
|
||||||
|
// - Replying to a post in a language android doesn't know
|
||||||
|
locales.add(0, Locale(language))
|
||||||
|
Log.w(TAG, "Attempting to use unknown language tag '$language'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLocaleIndex > 0) {
|
||||||
|
// Move preselected locale to the top
|
||||||
|
locales.add(0, locales.removeAt(currentLocaleIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLanguageSpinner(initialLanguage: String) {
|
||||||
|
val locales = mutableListOf<Locale>()
|
||||||
|
mergeLocaleListCompat(locales, AppCompatDelegate.getApplicationLocales()) // configured app languages first
|
||||||
|
mergeLocaleListCompat(locales, LocaleListCompat.getDefault()) // then configured system languages
|
||||||
|
locales.addAll( // finally, other languages
|
||||||
|
// Only "base" languages, "en" but not "en_DK"
|
||||||
|
Locale.getAvailableLocales().filter {
|
||||||
|
it.country.isNullOrEmpty() &&
|
||||||
|
it.script.isNullOrEmpty() &&
|
||||||
|
it.variant.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ensureLanguageIsFirst(locales, initialLanguage)
|
||||||
|
|
||||||
|
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||||
|
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>) {
|
||||||
|
parent.setSelection(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.composePostLanguageButton.apply {
|
||||||
|
adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, locales)
|
||||||
|
setSelection(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInitialLanguage(language: String? = null): String {
|
||||||
|
return if (language.isNullOrEmpty()) {
|
||||||
|
// Account-specific language set on the server
|
||||||
|
if (accountManager.activeAccount?.defaultPostLanguage?.isNotEmpty() == true) {
|
||||||
|
accountManager.activeAccount?.defaultPostLanguage!!
|
||||||
|
} else {
|
||||||
|
// Setting the application ui preference sets the default locale
|
||||||
|
AppCompatDelegate.getApplicationLocales()[0]?.language
|
||||||
|
?: Locale.getDefault().language
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
language
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupActionBar() {
|
private fun setupActionBar() {
|
||||||
|
@ -555,6 +708,9 @@ class ComposeActivity :
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putParcelable(PHOTO_UPLOAD_URI_KEY, photoUploadUri)
|
outState.putParcelable(PHOTO_UPLOAD_URI_KEY, photoUploadUri)
|
||||||
|
outState.putSerializable(VISIBILITY_KEY, viewModel.statusVisibility.value)
|
||||||
|
outState.putBoolean(CONTENT_WARNING_VISIBLE_KEY, viewModel.showContentWarning.value)
|
||||||
|
outState.putString(SCHEDULED_TIME_KEY, viewModel.scheduledAt.value)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -581,12 +737,12 @@ class ComposeActivity :
|
||||||
@ColorInt val color = if (contentWarningShown) {
|
@ColorInt val color = if (contentWarningShown) {
|
||||||
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
||||||
binding.composeHideMediaButton.isClickable = false
|
binding.composeHideMediaButton.isClickable = false
|
||||||
ContextCompat.getColor(this, R.color.transparent_chinwag_green)
|
getColor(R.color.transparent_chinwag_green)
|
||||||
} else {
|
} else {
|
||||||
binding.composeHideMediaButton.isClickable = true
|
binding.composeHideMediaButton.isClickable = true
|
||||||
if (markMediaSensitive) {
|
if (markMediaSensitive) {
|
||||||
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
||||||
ContextCompat.getColor(this, R.color.chinwag_green)
|
getColor(R.color.chinwag_green)
|
||||||
} else {
|
} else {
|
||||||
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
|
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
|
||||||
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||||
|
@ -600,7 +756,7 @@ class ComposeActivity :
|
||||||
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
||||||
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||||
} else {
|
} else {
|
||||||
ContextCompat.getColor(this, R.color.chinwag_green)
|
getColor(R.color.chinwag_green)
|
||||||
}
|
}
|
||||||
binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||||
}
|
}
|
||||||
|
@ -693,7 +849,7 @@ class ComposeActivity :
|
||||||
// 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) {
|
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
addMediaBehavior.removeBottomSheetCallback(this)
|
addMediaBehavior.removeBottomSheetCallback(this)
|
||||||
if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||||
ActivityCompat.requestPermissions(
|
ActivityCompat.requestPermissions(
|
||||||
this@ComposeActivity,
|
this@ComposeActivity,
|
||||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||||
|
@ -711,13 +867,17 @@ class ComposeActivity :
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openPollDialog() {
|
private fun openPollDialog() = lifecycleScope.launch {
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
val instanceParams = viewModel.instanceInfo.value!!
|
val instanceParams = viewModel.instanceInfo.first()
|
||||||
showAddPollDialog(
|
showAddPollDialog(
|
||||||
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
context = this@ComposeActivity,
|
||||||
instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration,
|
poll = viewModel.poll.value,
|
||||||
viewModel::updatePoll
|
maxOptionCount = instanceParams.pollMaxOptions,
|
||||||
|
maxOptionLength = instanceParams.pollMaxLength,
|
||||||
|
minDuration = instanceParams.pollMinDuration,
|
||||||
|
maxDuration = instanceParams.pollMaxDuration,
|
||||||
|
onUpdatePoll = viewModel::updatePoll
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,18 +928,22 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var length = binding.composeEditField.length() - offset
|
var length = binding.composeEditField.length() - offset
|
||||||
if (viewModel.showContentWarning.value!!) {
|
if (viewModel.showContentWarning.value) {
|
||||||
length += binding.composeContentWarningField.length()
|
length += binding.composeContentWarningField.length()
|
||||||
}
|
}
|
||||||
return length
|
return length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
val selectedLanguage: String?
|
||||||
|
get() = viewModel.postLanguage
|
||||||
|
|
||||||
private fun updateVisibleCharactersLeft() {
|
private fun updateVisibleCharactersLeft() {
|
||||||
val remainingLength = maximumTootCharacters - calculateTextLength()
|
val remainingLength = maximumTootCharacters - calculateTextLength()
|
||||||
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
||||||
|
|
||||||
val textColor = if (remainingLength < 0) {
|
val textColor = if (remainingLength < 0) {
|
||||||
ContextCompat.getColor(this, R.color.tusky_red)
|
getColor(R.color.tusky_red)
|
||||||
} else {
|
} else {
|
||||||
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||||
}
|
}
|
||||||
|
@ -822,7 +986,7 @@ class ComposeActivity :
|
||||||
enableButtons(false)
|
enableButtons(false)
|
||||||
val contentText = binding.composeEditField.text.toString()
|
val contentText = binding.composeEditField.text.toString()
|
||||||
var spoilerText = ""
|
var spoilerText = ""
|
||||||
if (viewModel.showContentWarning.value!!) {
|
if (viewModel.showContentWarning.value) {
|
||||||
spoilerText = binding.composeContentWarningField.text.toString()
|
spoilerText = binding.composeContentWarningField.text.toString()
|
||||||
}
|
}
|
||||||
val characterCount = calculateTextLength()
|
val characterCount = calculateTextLength()
|
||||||
|
@ -837,9 +1001,8 @@ class ComposeActivity :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.sendStatus(contentText, spoilerText).observe(
|
lifecycleScope.launch {
|
||||||
this
|
viewModel.sendStatus(contentText, spoilerText)
|
||||||
) {
|
|
||||||
finishingUploadDialog?.dismiss()
|
finishingUploadDialog?.dismiss()
|
||||||
deleteDraftAndFinish()
|
deleteDraftAndFinish()
|
||||||
}
|
}
|
||||||
|
@ -935,13 +1098,17 @@ class ComposeActivity :
|
||||||
private fun pickMedia(uri: Uri) {
|
private fun pickMedia(uri: Uri) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.pickMedia(uri).onFailure { throwable ->
|
viewModel.pickMedia(uri).onFailure { throwable ->
|
||||||
val errorId = when (throwable) {
|
val errorString = when (throwable) {
|
||||||
is VideoSizeException -> R.string.error_video_upload_size
|
is FileSizeException -> {
|
||||||
is AudioSizeException -> R.string.error_audio_upload_size
|
val decimalFormat = DecimalFormat("0.##")
|
||||||
is VideoOrImageException -> R.string.error_media_upload_image_or_video
|
val allowedSizeInMb = throwable.allowedSizeInBytes.toDouble() / (1024 * 1024)
|
||||||
else -> R.string.error_media_upload_opening
|
val formattedSize = decimalFormat.format(allowedSizeInMb)
|
||||||
|
getString(R.string.error_multimedia_size_limit, formattedSize)
|
||||||
|
}
|
||||||
|
is VideoOrImageException -> getString(R.string.error_media_upload_image_or_video)
|
||||||
|
else -> getString(R.string.error_media_upload_opening)
|
||||||
}
|
}
|
||||||
displayTransientError(errorId)
|
displayTransientError(errorString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -952,7 +1119,7 @@ class ComposeActivity :
|
||||||
binding.composeContentWarningBar.show()
|
binding.composeContentWarningBar.show()
|
||||||
binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length)
|
binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length)
|
||||||
binding.composeContentWarningField.requestFocus()
|
binding.composeContentWarningField.requestFocus()
|
||||||
ContextCompat.getColor(this, R.color.chinwag_green)
|
getColor(R.color.chinwag_green)
|
||||||
} else {
|
} else {
|
||||||
binding.composeContentWarningBar.hide()
|
binding.composeContentWarningBar.hide()
|
||||||
binding.composeEditField.requestFocus()
|
binding.composeEditField.requestFocus()
|
||||||
|
@ -970,23 +1137,6 @@ class ComposeActivity :
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
// Acting like a teen: deliberately ignoring parent.
|
|
||||||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
) {
|
|
||||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCloseButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
Log.d(TAG, event.toString())
|
Log.d(TAG, event.toString())
|
||||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||||
|
@ -999,7 +1149,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1010,8 +1160,15 @@ class ComposeActivity :
|
||||||
val contentText = binding.composeEditField.text.toString()
|
val contentText = binding.composeEditField.text.toString()
|
||||||
val contentWarning = binding.composeContentWarningField.text.toString()
|
val contentWarning = binding.composeContentWarningField.text.toString()
|
||||||
if (viewModel.didChange(contentText, contentWarning)) {
|
if (viewModel.didChange(contentText, contentWarning)) {
|
||||||
|
|
||||||
|
val warning = if (!viewModel.media.value.isEmpty()) {
|
||||||
|
R.string.compose_save_draft_loses_media
|
||||||
|
} else {
|
||||||
|
R.string.compose_save_draft
|
||||||
|
}
|
||||||
|
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setMessage(R.string.compose_save_draft)
|
.setMessage(warning)
|
||||||
.setPositiveButton(R.string.action_save) { _, _ ->
|
.setPositiveButton(R.string.action_save) { _, _ ->
|
||||||
saveDraftAndFinish(contentText, contentWarning)
|
saveDraftAndFinish(contentText, contentWarning)
|
||||||
}
|
}
|
||||||
|
@ -1065,7 +1222,8 @@ class ComposeActivity :
|
||||||
val mediaSize: Long,
|
val mediaSize: Long,
|
||||||
val uploadPercent: Int = 0,
|
val uploadPercent: Int = 0,
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val description: String? = null
|
val description: String? = null,
|
||||||
|
val focus: Attachment.Focus? = null
|
||||||
) {
|
) {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
IMAGE, VIDEO, AUDIO;
|
IMAGE, VIDEO, AUDIO;
|
||||||
|
@ -1086,6 +1244,14 @@ class ComposeActivity :
|
||||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUpdateDescription(localId: Int, description: String) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (!viewModel.updateDescription(localId, description)) {
|
||||||
|
Toast.makeText(this@ComposeActivity, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ComposeOptions(
|
data class ComposeOptions(
|
||||||
// Let's keep fields var until all consumers are Kotlin
|
// Let's keep fields var until all consumers are Kotlin
|
||||||
|
@ -1106,7 +1272,8 @@ class ComposeActivity :
|
||||||
var scheduledAt: String? = null,
|
var scheduledAt: String? = null,
|
||||||
var sensitive: Boolean? = null,
|
var sensitive: Boolean? = null,
|
||||||
var poll: NewPoll? = null,
|
var poll: NewPoll? = null,
|
||||||
var modifiedInitialState: Boolean? = null
|
var modifiedInitialState: Boolean? = null,
|
||||||
|
var language: String? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -1117,6 +1284,9 @@ class ComposeActivity :
|
||||||
private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_ID"
|
private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_ID"
|
||||||
private const val ACCOUNT_ID_EXTRA = "ACCOUNT_ID"
|
private const val ACCOUNT_ID_EXTRA = "ACCOUNT_ID"
|
||||||
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
||||||
|
private const val VISIBILITY_KEY = "VISIBILITY"
|
||||||
|
private const val SCHEDULED_TIME_KEY = "SCHEDULE"
|
||||||
|
private const val CONTENT_WARNING_VISIBLE_KEY = "CONTENT_WARNING_VISIBLE"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param options ComposeOptions to configure the ComposeActivity
|
* @param options ComposeOptions to configure the ComposeActivity
|
||||||
|
|
|
@ -97,11 +97,11 @@ class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
||||||
return if (i > 0 && text[i - 1] == ' ') {
|
return if (i > 0 && text[i - 1] == ' ') {
|
||||||
text
|
text
|
||||||
} else if (text is Spanned) {
|
} else if (text is Spanned) {
|
||||||
val s = SpannableString(text.toString() + " ")
|
val s = SpannableString("$text ")
|
||||||
TextUtils.copySpansFrom(text, 0, text.length, Object::class.java, s, 0)
|
TextUtils.copySpansFrom(text, 0, text.length, Object::class.java, s, 0)
|
||||||
s
|
s
|
||||||
} else {
|
} else {
|
||||||
text.toString() + " "
|
"$text "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,7 @@ package com.keylesspalace.tusky.components.compose
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
|
@ -38,35 +35,40 @@ import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.StatusToSend
|
import com.keylesspalace.tusky.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.combineLiveData
|
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import com.keylesspalace.tusky.util.toLiveData
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.flow.updateAndGet
|
import kotlinx.coroutines.flow.updateAndGet
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.rxSingle
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
class ComposeViewModel @Inject constructor(
|
class ComposeViewModel @Inject constructor(
|
||||||
private val api: MastodonApi,
|
private val api: MastodonApi,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val mediaUploader: MediaUploader,
|
private val mediaUploader: MediaUploader,
|
||||||
private val serviceClient: ServiceClient,
|
private val serviceClient: ServiceClient,
|
||||||
private val draftHelper: DraftHelper,
|
private val draftHelper: DraftHelper,
|
||||||
private val instanceInfoRepo: InstanceInfoRepository
|
instanceInfoRepo: InstanceInfoRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private var replyingStatusAuthor: String? = null
|
private var replyingStatusAuthor: String? = null
|
||||||
private var replyingStatusContent: String? = null
|
private var replyingStatusContent: String? = null
|
||||||
internal var startingText: String? = null
|
internal var startingText: String? = null
|
||||||
|
internal var postLanguage: String? = null
|
||||||
private var draftId: Int = 0
|
private var draftId: Int = 0
|
||||||
private var scheduledTootId: String? = null
|
private var scheduledTootId: String? = null
|
||||||
private var startingContentWarning: String = ""
|
private var startingContentWarning: String = ""
|
||||||
|
@ -75,41 +77,35 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
private var contentWarningStateChanged: Boolean = false
|
private var contentWarningStateChanged: Boolean = false
|
||||||
private var modifiedInitialState: Boolean = false
|
private var modifiedInitialState: Boolean = false
|
||||||
|
private var hasScheduledTimeChanged: Boolean = false
|
||||||
|
|
||||||
val instanceInfo: MutableLiveData<InstanceInfo> = MutableLiveData()
|
val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
|
||||||
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
|
||||||
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
|
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
|
||||||
val markMediaAsSensitive =
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||||
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
|
||||||
|
|
||||||
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
|
val markMediaAsSensitive: MutableStateFlow<Boolean> =
|
||||||
val showContentWarning = mutableLiveData(false)
|
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||||
val setupComplete = mutableLiveData(false)
|
|
||||||
val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
|
val statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
|
||||||
val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
|
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
|
||||||
|
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||||
|
|
||||||
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||||
val uploadError = MutableLiveData<Throwable>()
|
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
|
||||||
private val mediaToJob = mutableMapOf<Int, Job>()
|
private val mediaToJob = mutableMapOf<Int, Job>()
|
||||||
|
|
||||||
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
|
||||||
|
|
||||||
// Used in ComposeActivity to pass state to result function when cropImage contract inflight
|
// Used in ComposeActivity to pass state to result function when cropImage contract inflight
|
||||||
var cropImageItemOld: QueuedMedia? = null
|
var cropImageItemOld: QueuedMedia? = null
|
||||||
|
|
||||||
init {
|
private var setupComplete = false
|
||||||
viewModelScope.launch {
|
|
||||||
emoji.postValue(instanceInfoRepo.getEmojis())
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
instanceInfo.postValue(instanceInfoRepo.getInstanceInfo())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
|
suspend fun pickMedia(mediaUri: Uri, description: String? = null, focus: Attachment.Focus? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri)
|
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
|
||||||
val mediaItems = media.value
|
val mediaItems = media.value
|
||||||
if (type != QueuedMedia.Type.IMAGE &&
|
if (type != QueuedMedia.Type.IMAGE &&
|
||||||
mediaItems.isNotEmpty() &&
|
mediaItems.isNotEmpty() &&
|
||||||
|
@ -117,7 +113,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
Result.failure(VideoOrImageException())
|
Result.failure(VideoOrImageException())
|
||||||
} else {
|
} else {
|
||||||
val queuedMedia = addMediaToQueue(type, uri, size, description)
|
val queuedMedia = addMediaToQueue(type, uri, size, description, focus)
|
||||||
Result.success(queuedMedia)
|
Result.success(queuedMedia)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -130,6 +126,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mediaSize: Long,
|
mediaSize: Long,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
|
focus: Attachment.Focus? = null,
|
||||||
replaceItem: QueuedMedia? = null
|
replaceItem: QueuedMedia? = null
|
||||||
): QueuedMedia {
|
): QueuedMedia {
|
||||||
var stashMediaItem: QueuedMedia? = null
|
var stashMediaItem: QueuedMedia? = null
|
||||||
|
@ -140,7 +137,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
type = type,
|
type = type,
|
||||||
mediaSize = mediaSize,
|
mediaSize = mediaSize,
|
||||||
description = description
|
description = description,
|
||||||
|
focus = focus
|
||||||
)
|
)
|
||||||
stashMediaItem = mediaItem
|
stashMediaItem = mediaItem
|
||||||
|
|
||||||
|
@ -157,10 +155,10 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
mediaToJob[mediaItem.localId] = viewModelScope.launch {
|
mediaToJob[mediaItem.localId] = viewModelScope.launch {
|
||||||
mediaUploader
|
mediaUploader
|
||||||
.uploadMedia(mediaItem)
|
.uploadMedia(mediaItem, instanceInfo.first())
|
||||||
.catch { error ->
|
.catch { error ->
|
||||||
media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } }
|
media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } }
|
||||||
uploadError.postValue(error)
|
uploadError.emit(error)
|
||||||
}
|
}
|
||||||
.collect { event ->
|
.collect { event ->
|
||||||
val item = media.value.find { it.localId == mediaItem.localId }
|
val item = media.value.find { it.localId == mediaItem.localId }
|
||||||
|
@ -185,7 +183,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
return mediaItem
|
return mediaItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) {
|
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) {
|
||||||
media.update { mediaValue ->
|
media.update { mediaValue ->
|
||||||
val mediaItem = QueuedMedia(
|
val mediaItem = QueuedMedia(
|
||||||
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
||||||
|
@ -194,7 +192,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaSize = 0,
|
mediaSize = 0,
|
||||||
uploadPercent = -1,
|
uploadPercent = -1,
|
||||||
id = id,
|
id = id,
|
||||||
description = description
|
description = description,
|
||||||
|
focus = focus
|
||||||
)
|
)
|
||||||
mediaValue + mediaItem
|
mediaValue + mediaItem
|
||||||
}
|
}
|
||||||
|
@ -216,13 +215,14 @@ class ComposeViewModel @Inject constructor(
|
||||||
startingText?.startsWith(content.toString()) ?: false
|
startingText?.startsWith(content.toString()) ?: false
|
||||||
)
|
)
|
||||||
|
|
||||||
val contentWarningChanged = showContentWarning.value!! &&
|
val contentWarningChanged = showContentWarning.value &&
|
||||||
!contentWarning.isNullOrEmpty() &&
|
!contentWarning.isNullOrEmpty() &&
|
||||||
!startingContentWarning.startsWith(contentWarning.toString())
|
!startingContentWarning.startsWith(contentWarning.toString())
|
||||||
val mediaChanged = media.value.isNotEmpty()
|
val mediaChanged = media.value.isNotEmpty()
|
||||||
val pollChanged = poll.value != null
|
val pollChanged = poll.value != null
|
||||||
|
val didScheduledTimeChange = hasScheduledTimeChanged
|
||||||
|
|
||||||
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged
|
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contentWarningChanged(value: Boolean) {
|
fun contentWarningChanged(value: Boolean) {
|
||||||
|
@ -248,9 +248,11 @@ class ComposeViewModel @Inject constructor(
|
||||||
suspend fun saveDraft(content: String, contentWarning: String) {
|
suspend fun saveDraft(content: String, contentWarning: String) {
|
||||||
val mediaUris: MutableList<String> = mutableListOf()
|
val mediaUris: MutableList<String> = mutableListOf()
|
||||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||||
|
val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf()
|
||||||
media.value.forEach { item ->
|
media.value.forEach { item ->
|
||||||
mediaUris.add(item.uri.toString())
|
mediaUris.add(item.uri.toString())
|
||||||
mediaDescriptions.add(item.description)
|
mediaDescriptions.add(item.description)
|
||||||
|
mediaFocus.add(item.focus)
|
||||||
}
|
}
|
||||||
|
|
||||||
draftHelper.saveDraft(
|
draftHelper.saveDraft(
|
||||||
|
@ -259,53 +261,55 @@ class ComposeViewModel @Inject constructor(
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
content = content,
|
content = content,
|
||||||
contentWarning = contentWarning,
|
contentWarning = contentWarning,
|
||||||
sensitive = markMediaAsSensitive.value!!,
|
sensitive = markMediaAsSensitive.value,
|
||||||
visibility = statusVisibility.value!!,
|
visibility = statusVisibility.value,
|
||||||
mediaUris = mediaUris,
|
mediaUris = mediaUris,
|
||||||
mediaDescriptions = mediaDescriptions,
|
mediaDescriptions = mediaDescriptions,
|
||||||
|
mediaFocus = mediaFocus,
|
||||||
poll = poll.value,
|
poll = poll.value,
|
||||||
failedToSend = false
|
failedToSend = false,
|
||||||
|
scheduledAt = scheduledAt.value,
|
||||||
|
language = postLanguage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send status to the server.
|
* Send status to the server.
|
||||||
* Uses current state plus provided arguments.
|
* Uses current state plus provided arguments.
|
||||||
* @return LiveData which will signal once the screen can be closed or null if there are errors
|
|
||||||
*/
|
*/
|
||||||
fun sendStatus(
|
suspend fun sendStatus(
|
||||||
content: String,
|
content: String,
|
||||||
spoilerText: String
|
spoilerText: String
|
||||||
): LiveData<Unit> {
|
) {
|
||||||
|
|
||||||
val deletionObservable = if (isEditingScheduledToot) {
|
if (!scheduledTootId.isNullOrEmpty()) {
|
||||||
rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { }
|
api.deleteScheduledStatus(scheduledTootId!!)
|
||||||
} else {
|
}
|
||||||
Observable.just(Unit)
|
|
||||||
}.toLiveData()
|
|
||||||
|
|
||||||
val sendFlow = media
|
media
|
||||||
.filter { items -> items.all { it.uploadPercent == -1 } }
|
.filter { items -> items.all { it.uploadPercent == -1 } }
|
||||||
.map {
|
.first {
|
||||||
val mediaIds: MutableList<String> = mutableListOf()
|
val mediaIds: MutableList<String> = mutableListOf()
|
||||||
val mediaUris: MutableList<Uri> = mutableListOf()
|
val mediaUris: MutableList<Uri> = mutableListOf()
|
||||||
val mediaDescriptions: MutableList<String> = mutableListOf()
|
val mediaDescriptions: MutableList<String> = mutableListOf()
|
||||||
|
val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf()
|
||||||
val mediaProcessed: MutableList<Boolean> = mutableListOf()
|
val mediaProcessed: MutableList<Boolean> = mutableListOf()
|
||||||
for (item in media.value) {
|
media.value.forEach { item ->
|
||||||
mediaIds.add(item.id!!)
|
mediaIds.add(item.id!!)
|
||||||
mediaUris.add(item.uri)
|
mediaUris.add(item.uri)
|
||||||
mediaDescriptions.add(item.description ?: "")
|
mediaDescriptions.add(item.description ?: "")
|
||||||
|
mediaFocus.add(item.focus)
|
||||||
mediaProcessed.add(false)
|
mediaProcessed.add(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tootToSend = StatusToSend(
|
val tootToSend = StatusToSend(
|
||||||
text = content,
|
text = content,
|
||||||
warningText = spoilerText,
|
warningText = spoilerText,
|
||||||
visibility = statusVisibility.value!!.serverString(),
|
visibility = statusVisibility.value.serverString(),
|
||||||
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
|
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value),
|
||||||
mediaIds = mediaIds,
|
mediaIds = mediaIds,
|
||||||
mediaUris = mediaUris.map { it.toString() },
|
mediaUris = mediaUris.map { it.toString() },
|
||||||
mediaDescriptions = mediaDescriptions,
|
mediaDescriptions = mediaDescriptions,
|
||||||
|
mediaFocus = mediaFocus,
|
||||||
scheduledAt = scheduledAt.value,
|
scheduledAt = scheduledAt.value,
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
poll = poll.value,
|
poll = poll.value,
|
||||||
|
@ -315,20 +319,21 @@ class ComposeViewModel @Inject constructor(
|
||||||
draftId = draftId,
|
draftId = draftId,
|
||||||
idempotencyKey = randomAlphanumericString(16),
|
idempotencyKey = randomAlphanumericString(16),
|
||||||
retries = 0,
|
retries = 0,
|
||||||
mediaProcessed = mediaProcessed
|
mediaProcessed = mediaProcessed,
|
||||||
|
language = postLanguage,
|
||||||
)
|
)
|
||||||
|
|
||||||
serviceClient.sendToot(tootToSend)
|
serviceClient.sendToot(tootToSend)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineLiveData(deletionObservable, sendFlow.asLiveData()) { _, _ -> }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateDescription(localId: Int, description: String): Boolean {
|
// Updates a QueuedMedia item arbitrarily, then sends description and focus to server
|
||||||
|
private suspend fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia): Boolean {
|
||||||
val newMediaList = media.updateAndGet { mediaValue ->
|
val newMediaList = media.updateAndGet { mediaValue ->
|
||||||
mediaValue.map { mediaItem ->
|
mediaValue.map { mediaItem ->
|
||||||
if (mediaItem.localId == localId) {
|
if (mediaItem.localId == localId) {
|
||||||
mediaItem.copy(description = description)
|
mutator(mediaItem)
|
||||||
} else {
|
} else {
|
||||||
mediaItem
|
mediaItem
|
||||||
}
|
}
|
||||||
|
@ -337,7 +342,9 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
val updatedItem = newMediaList.find { it.localId == localId }
|
val updatedItem = newMediaList.find { it.localId == localId }
|
||||||
if (updatedItem?.id != null) {
|
if (updatedItem?.id != null) {
|
||||||
return api.updateMedia(updatedItem.id, description)
|
val focus = updatedItem.focus
|
||||||
|
val focusString = if (focus != null) "${focus.x},${focus.y}" else null
|
||||||
|
return api.updateMedia(updatedItem.id, updatedItem.description, focusString)
|
||||||
.fold({
|
.fold({
|
||||||
true
|
true
|
||||||
}, { throwable ->
|
}, { throwable ->
|
||||||
|
@ -348,6 +355,18 @@ class ComposeViewModel @Inject constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateDescription(localId: Int, description: String): Boolean {
|
||||||
|
return updateMediaItem(localId, { mediaItem ->
|
||||||
|
mediaItem.copy(description = description)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateFocus(localId: Int, focus: Attachment.Focus): Boolean {
|
||||||
|
return updateMediaItem(localId, { mediaItem ->
|
||||||
|
mediaItem.copy(focus = focus)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
||||||
when (token[0]) {
|
when (token[0]) {
|
||||||
'@' -> {
|
'@' -> {
|
||||||
|
@ -369,7 +388,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
':' -> {
|
':' -> {
|
||||||
val emojiList = emoji.value ?: return emptyList()
|
val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList()
|
||||||
val incomplete = token.substring(1)
|
val incomplete = token.substring(1)
|
||||||
|
|
||||||
return emojiList.filter { emoji ->
|
return emojiList.filter { emoji ->
|
||||||
|
@ -389,7 +408,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
|
fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
|
||||||
|
|
||||||
if (setupComplete.value == true) {
|
if (setupComplete) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -418,7 +437,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
// when coming from DraftActivity
|
// when coming from DraftActivity
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
draftAttachments.forEach { attachment ->
|
draftAttachments.forEach { attachment ->
|
||||||
pickMedia(attachment.uri, attachment.description)
|
pickMedia(attachment.uri, attachment.description, attachment.focus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else composeOptions?.mediaAttachments?.forEach { a ->
|
} else composeOptions?.mediaAttachments?.forEach { a ->
|
||||||
|
@ -428,12 +447,13 @@ class ComposeViewModel @Inject constructor(
|
||||||
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
|
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
|
||||||
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
|
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
|
||||||
}
|
}
|
||||||
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description, a.meta?.focus)
|
||||||
}
|
}
|
||||||
|
|
||||||
draftId = composeOptions?.draftId ?: 0
|
draftId = composeOptions?.draftId ?: 0
|
||||||
scheduledTootId = composeOptions?.scheduledTootId
|
scheduledTootId = composeOptions?.scheduledTootId
|
||||||
startingText = composeOptions?.content
|
startingText = composeOptions?.content
|
||||||
|
postLanguage = composeOptions?.language
|
||||||
|
|
||||||
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
||||||
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
||||||
|
@ -461,6 +481,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
replyingStatusContent = composeOptions?.replyingStatusContent
|
replyingStatusContent = composeOptions?.replyingStatusContent
|
||||||
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
||||||
|
|
||||||
|
setupComplete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePoll(newPoll: NewPoll) {
|
fun updatePoll(newPoll: NewPoll) {
|
||||||
|
@ -468,6 +490,10 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateScheduledAt(newScheduledAt: String?) {
|
fun updateScheduledAt(newScheduledAt: String?) {
|
||||||
|
if (newScheduledAt != scheduledAt.value) {
|
||||||
|
hasScheduledTimeChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
scheduledAt.value = newScheduledAt
|
scheduledAt.value = newScheduledAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -476,8 +502,6 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when trying to add an image when video is already present or the other way around
|
* Thrown when trying to add an image when video is already present or the other way around
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -20,8 +20,8 @@ import android.graphics.Bitmap
|
||||||
import android.graphics.Bitmap.CompressFormat
|
import android.graphics.Bitmap.CompressFormat
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.keylesspalace.tusky.util.IOUtils
|
|
||||||
import com.keylesspalace.tusky.util.calculateInSampleSize
|
import com.keylesspalace.tusky.util.calculateInSampleSize
|
||||||
|
import com.keylesspalace.tusky.util.closeQuietly
|
||||||
import com.keylesspalace.tusky.util.getImageOrientation
|
import com.keylesspalace.tusky.util.getImageOrientation
|
||||||
import com.keylesspalace.tusky.util.reorientBitmap
|
import com.keylesspalace.tusky.util.reorientBitmap
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -51,7 +51,7 @@ fun downsizeImage(
|
||||||
val options = BitmapFactory.Options()
|
val options = BitmapFactory.Options()
|
||||||
options.inJustDecodeBounds = true
|
options.inJustDecodeBounds = true
|
||||||
BitmapFactory.decodeStream(decodeBoundsInputStream, null, options)
|
BitmapFactory.decodeStream(decodeBoundsInputStream, null, options)
|
||||||
IOUtils.closeQuietly(decodeBoundsInputStream)
|
decodeBoundsInputStream.closeQuietly()
|
||||||
// Get EXIF data, for orientation info.
|
// Get EXIF data, for orientation info.
|
||||||
val orientation = getImageOrientation(uri, contentResolver)
|
val orientation = getImageOrientation(uri, contentResolver)
|
||||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||||
|
@ -78,7 +78,7 @@ fun downsizeImage(
|
||||||
} catch (error: OutOfMemoryError) {
|
} catch (error: OutOfMemoryError) {
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.closeQuietly(decodeBitmapInputStream)
|
decodeBitmapInputStream.closeQuietly()
|
||||||
} ?: return false
|
} ?: return false
|
||||||
|
|
||||||
val reorientedBitmap = reorientBitmap(scaledBitmap, orientation)
|
val reorientedBitmap = reorientBitmap(scaledBitmap, orientation)
|
||||||
|
|
|
@ -32,6 +32,7 @@ import com.keylesspalace.tusky.components.compose.view.ProgressImageView
|
||||||
class MediaPreviewAdapter(
|
class MediaPreviewAdapter(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
|
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
|
||||||
|
private val onAddFocus: (ComposeActivity.QueuedMedia) -> Unit,
|
||||||
private val onEditImage: (ComposeActivity.QueuedMedia) -> Unit,
|
private val onEditImage: (ComposeActivity.QueuedMedia) -> Unit,
|
||||||
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
|
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
|
||||||
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
|
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
|
||||||
|
@ -44,15 +45,19 @@ class MediaPreviewAdapter(
|
||||||
val item = differ.currentList[position]
|
val item = differ.currentList[position]
|
||||||
val popup = PopupMenu(view.context, view)
|
val popup = PopupMenu(view.context, view)
|
||||||
val addCaptionId = 1
|
val addCaptionId = 1
|
||||||
val editImageId = 2
|
val addFocusId = 2
|
||||||
val removeId = 3
|
val editImageId = 3
|
||||||
|
val removeId = 4
|
||||||
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
||||||
if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE)
|
if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) {
|
||||||
|
popup.menu.add(0, addFocusId, 0, R.string.action_set_focus)
|
||||||
popup.menu.add(0, editImageId, 0, R.string.action_edit_image)
|
popup.menu.add(0, editImageId, 0, R.string.action_edit_image)
|
||||||
|
}
|
||||||
popup.menu.add(0, removeId, 0, R.string.action_remove)
|
popup.menu.add(0, removeId, 0, R.string.action_remove)
|
||||||
popup.setOnMenuItemClickListener { menuItem ->
|
popup.setOnMenuItemClickListener { menuItem ->
|
||||||
when (menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
addCaptionId -> onAddCaption(item)
|
addCaptionId -> onAddCaption(item)
|
||||||
|
addFocusId -> onAddFocus(item)
|
||||||
editImageId -> onEditImage(item)
|
editImageId -> onEditImage(item)
|
||||||
removeId -> onRemove(item)
|
removeId -> onRemove(item)
|
||||||
}
|
}
|
||||||
|
@ -78,11 +83,24 @@ class MediaPreviewAdapter(
|
||||||
// TODO: Fancy waveform display?
|
// TODO: Fancy waveform display?
|
||||||
holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(holder.itemView.context)
|
val imageView = holder.progressImageView
|
||||||
|
val focus = item.focus
|
||||||
|
|
||||||
|
if (focus != null)
|
||||||
|
imageView.setFocalPoint(focus)
|
||||||
|
else
|
||||||
|
imageView.removeFocalPoint() // Probably unnecessary since we have no UI for removal once added.
|
||||||
|
|
||||||
|
var glide = Glide.with(holder.itemView.context)
|
||||||
.load(item.uri)
|
.load(item.uri)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.into(holder.progressImageView)
|
.centerInside()
|
||||||
|
|
||||||
|
if (focus != null)
|
||||||
|
glide = glide.addListener(imageView)
|
||||||
|
|
||||||
|
glide.into(imageView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
||||||
import com.keylesspalace.tusky.network.MediaUploadApi
|
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||||
|
@ -70,8 +71,7 @@ fun createNewImageFile(context: Context, suffix: String = ".jpg"): File {
|
||||||
|
|
||||||
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
||||||
|
|
||||||
class AudioSizeException : Exception()
|
class FileSizeException(val allowedSizeInBytes: Int) : Exception()
|
||||||
class VideoSizeException : Exception()
|
|
||||||
class MediaTypeException : Exception()
|
class MediaTypeException : Exception()
|
||||||
class CouldNotOpenFileException : Exception()
|
class CouldNotOpenFileException : Exception()
|
||||||
class UploadServerError(val errorMessage: String) : Exception()
|
class UploadServerError(val errorMessage: String) : Exception()
|
||||||
|
@ -82,10 +82,10 @@ class MediaUploader @Inject constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun uploadMedia(media: QueuedMedia): Flow<UploadEvent> {
|
fun uploadMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Flow<UploadEvent> {
|
||||||
return flow {
|
return flow {
|
||||||
if (shouldResizeMedia(media)) {
|
if (shouldResizeMedia(media, instanceInfo)) {
|
||||||
emit(downsize(media))
|
emit(downsize(media, instanceInfo))
|
||||||
} else {
|
} else {
|
||||||
emit(media)
|
emit(media)
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ class MediaUploader @Inject constructor(
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepareMedia(inUri: Uri): PreparedMedia {
|
fun prepareMedia(inUri: Uri, instanceInfo: InstanceInfo): PreparedMedia {
|
||||||
var mediaSize = MEDIA_SIZE_UNKNOWN
|
var mediaSize = MEDIA_SIZE_UNKNOWN
|
||||||
var uri = inUri
|
var uri = inUri
|
||||||
val mimeType: String?
|
val mimeType: String?
|
||||||
|
@ -164,8 +164,8 @@ class MediaUploader @Inject constructor(
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
return when (mimeType.substring(0, mimeType.indexOf('/'))) {
|
return when (mimeType.substring(0, mimeType.indexOf('/'))) {
|
||||||
"video" -> {
|
"video" -> {
|
||||||
if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) {
|
if (mediaSize > instanceInfo.videoSizeLimit) {
|
||||||
throw VideoSizeException()
|
throw FileSizeException(instanceInfo.videoSizeLimit)
|
||||||
}
|
}
|
||||||
PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
|
PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
|
||||||
}
|
}
|
||||||
|
@ -173,8 +173,8 @@ class MediaUploader @Inject constructor(
|
||||||
PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
|
PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
|
||||||
}
|
}
|
||||||
"audio" -> {
|
"audio" -> {
|
||||||
if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) {
|
if (mediaSize > instanceInfo.videoSizeLimit) {
|
||||||
throw AudioSizeException()
|
throw FileSizeException(instanceInfo.videoSizeLimit)
|
||||||
}
|
}
|
||||||
PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
|
PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
|
||||||
}
|
}
|
||||||
|
@ -225,7 +225,13 @@ class MediaUploader @Inject constructor(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaUploadApi.uploadMedia(body, description).fold({ result ->
|
val focus = if (media.focus != null) {
|
||||||
|
MultipartBody.Part.createFormData("focus", "${media.focus.x},${media.focus.y}")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaUploadApi.uploadMedia(body, description, focus).fold({ result ->
|
||||||
send(UploadEvent.FinishedEvent(result.id))
|
send(UploadEvent.FinishedEvent(result.id))
|
||||||
}, { throwable ->
|
}, { throwable ->
|
||||||
val errorMessage = throwable.getServerErrorMessage()
|
val errorMessage = throwable.getServerErrorMessage()
|
||||||
|
@ -239,22 +245,18 @@ class MediaUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downsize(media: QueuedMedia): QueuedMedia {
|
private fun downsize(media: QueuedMedia, instanceInfo: InstanceInfo): QueuedMedia {
|
||||||
val file = createNewImageFile(context)
|
val file = createNewImageFile(context)
|
||||||
downsizeImage(media.uri, STATUS_IMAGE_SIZE_LIMIT, contentResolver, file)
|
downsizeImage(media.uri, instanceInfo.imageSizeLimit, contentResolver, file)
|
||||||
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldResizeMedia(media: QueuedMedia): Boolean {
|
private fun shouldResizeMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Boolean {
|
||||||
return media.type == QueuedMedia.Type.IMAGE &&
|
return media.type == QueuedMedia.Type.IMAGE &&
|
||||||
(media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
(media.mediaSize > instanceInfo.imageSizeLimit || getImageSquarePixels(context.contentResolver, media.uri) > instanceInfo.imageMatrixLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val TAG = "MediaUploader"
|
private const val TAG = "MediaUploader"
|
||||||
private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
|
|
||||||
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
|
|
||||||
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
|
|
||||||
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ fun showAddPollDialog(
|
||||||
}
|
}
|
||||||
|
|
||||||
val pollDurationId = durations.indexOfLast {
|
val pollDurationId = durations.indexOfLast {
|
||||||
it <= poll?.expiresIn ?: 0
|
it <= (poll?.expiresIn ?: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.pollDurationSpinner.setSelection(pollDurationId)
|
binding.pollDurationSpinner.setSelection(pollDurationId)
|
||||||
|
|
|
@ -15,19 +15,22 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.compose.dialog
|
package com.keylesspalace.tusky.components.compose.dialog
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.core.os.bundleOf
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.fragment.app.DialogFragment
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||||
|
@ -35,84 +38,123 @@ import com.bumptech.glide.request.target.CustomTarget
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import com.github.chrisbanes.photoview.PhotoView
|
import com.github.chrisbanes.photoview.PhotoView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
|
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
|
||||||
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
||||||
|
|
||||||
fun <T> T.makeCaptionDialog(
|
class CaptionDialog : DialogFragment() {
|
||||||
existingDescription: String?,
|
|
||||||
previewUri: Uri,
|
|
||||||
onUpdateDescription: suspend (String) -> Boolean
|
|
||||||
) where T : Activity, T : LifecycleOwner {
|
|
||||||
val dialogLayout = LinearLayout(this)
|
|
||||||
val padding = Utils.dpToPx(this, 8)
|
|
||||||
dialogLayout.setPadding(padding, padding, padding, padding)
|
|
||||||
|
|
||||||
dialogLayout.orientation = LinearLayout.VERTICAL
|
private lateinit var listener: Listener
|
||||||
val imageView = PhotoView(this).apply {
|
private lateinit var input: EditText
|
||||||
maximumScale = 6f
|
|
||||||
}
|
|
||||||
|
|
||||||
val margin = Utils.dpToPx(this, 4)
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
dialogLayout.addView(imageView)
|
val context = requireContext()
|
||||||
(imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f
|
val dialogLayout = LinearLayout(context)
|
||||||
imageView.layoutParams.height = 0
|
val padding = Utils.dpToPx(context, 8)
|
||||||
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
|
dialogLayout.setPadding(padding, padding, padding, padding)
|
||||||
|
|
||||||
val input = EditText(this)
|
dialogLayout.orientation = LinearLayout.VERTICAL
|
||||||
input.hint = resources.getQuantityString(
|
val imageView = PhotoView(context).apply {
|
||||||
R.plurals.hint_describe_for_visually_impaired,
|
maximumScale = 6f
|
||||||
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
|
|
||||||
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
||||||
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
||||||
)
|
|
||||||
input.setText(existingDescription)
|
|
||||||
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
|
||||||
|
|
||||||
val okListener = { dialog: DialogInterface, _: Int ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (!onUpdateDescription(input.text.toString())) {
|
|
||||||
showFailedCaptionMessage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
|
||||||
|
val margin = Utils.dpToPx(context, 4)
|
||||||
|
dialogLayout.addView(imageView)
|
||||||
|
(imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f
|
||||||
|
imageView.layoutParams.height = 0
|
||||||
|
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
|
||||||
|
|
||||||
|
input = EditText(context)
|
||||||
|
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
|
||||||
|
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||||
|
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||||
|
)
|
||||||
|
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
||||||
|
input.setText(arguments?.getString(EXISTING_DESCRIPTION_ARG))
|
||||||
|
|
||||||
|
val localId = arguments?.getInt(LOCAL_ID_ARG) ?: error("Missing localId")
|
||||||
|
val dialog = AlertDialog.Builder(context)
|
||||||
|
.setView(dialogLayout)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
listener.onUpdateDescription(localId, input.text.toString())
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
val window = dialog.window
|
||||||
|
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||||
|
|
||||||
|
val previewUri = arguments?.getParcelable<Uri>(PREVIEW_URI_ARG) ?: error("Preview Uri is null")
|
||||||
|
// Load the image and manually set it into the ImageView because it doesn't have a fixed size.
|
||||||
|
Glide.with(this)
|
||||||
|
.load(previewUri)
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(this)
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
.setView(dialogLayout)
|
outState.putString(DESCRIPTION_KEY, input.text.toString())
|
||||||
.setPositiveButton(android.R.string.ok, okListener)
|
super.onSaveInstanceState(outState)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
}
|
||||||
.create()
|
|
||||||
|
|
||||||
val window = dialog.window
|
override fun onCreateView(
|
||||||
window?.setSoftInputMode(
|
inflater: LayoutInflater,
|
||||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
container: ViewGroup?,
|
||||||
)
|
savedInstanceState: Bundle?,
|
||||||
|
): View? {
|
||||||
|
savedInstanceState?.getString(DESCRIPTION_KEY)?.let {
|
||||||
|
input.setText(it)
|
||||||
|
}
|
||||||
|
return super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
dialog.show()
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
listener = context as? Listener ?: error("Activity is not ComposeCaptionDialog.Listener")
|
||||||
|
}
|
||||||
|
|
||||||
// Load the image and manually set it into the ImageView because it doesn't have a fixed size.
|
interface Listener {
|
||||||
Glide.with(this)
|
fun onUpdateDescription(localId: Int, description: String)
|
||||||
.load(previewUri)
|
}
|
||||||
.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>?) {
|
companion object {
|
||||||
imageView.setImageDrawable(resource)
|
fun newInstance(
|
||||||
}
|
localId: Int,
|
||||||
})
|
existingDescription: String?,
|
||||||
}
|
previewUri: Uri,
|
||||||
|
) = CaptionDialog().apply {
|
||||||
private fun Activity.showFailedCaptionMessage() {
|
arguments = bundleOf(
|
||||||
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
|
LOCAL_ID_ARG to localId,
|
||||||
|
EXISTING_DESCRIPTION_ARG to existingDescription,
|
||||||
|
PREVIEW_URI_ARG to previewUri,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DESCRIPTION_KEY = "description"
|
||||||
|
private const val EXISTING_DESCRIPTION_ARG = "existing_description"
|
||||||
|
private const val PREVIEW_URI_ARG = "preview_uri"
|
||||||
|
private const val LOCAL_ID_ARG = "local_id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
/* Copyright 2019 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.compose.dialog
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||||
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.DialogFocusBinding
|
||||||
|
import com.keylesspalace.tusky.entity.Attachment.Focus
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
fun <T> T.makeFocusDialog(
|
||||||
|
existingFocus: Focus?,
|
||||||
|
previewUri: Uri,
|
||||||
|
onUpdateFocus: suspend (Focus) -> Boolean
|
||||||
|
) where T : Activity, T : LifecycleOwner {
|
||||||
|
val focus = existingFocus ?: Focus(0.0f, 0.0f) // Default to center
|
||||||
|
|
||||||
|
val dialogBinding = DialogFocusBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
dialogBinding.focusIndicator.setFocus(focus)
|
||||||
|
|
||||||
|
Glide.with(this)
|
||||||
|
.load(previewUri)
|
||||||
|
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||||
|
.listener(object : RequestListener<Drawable> {
|
||||||
|
override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target<Drawable?>?, p3: Boolean): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable?>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||||
|
val width = resource!!.intrinsicWidth
|
||||||
|
val height = resource.intrinsicHeight
|
||||||
|
|
||||||
|
dialogBinding.focusIndicator.setImageSize(width, height)
|
||||||
|
|
||||||
|
// We want the dialog to be a little taller than the image, so you can slide your thumb past the image border,
|
||||||
|
// but if it's *too* much taller that looks weird. See if a threshold has been crossed:
|
||||||
|
if (width > height) {
|
||||||
|
val maxHeight = dialogBinding.focusIndicator.maxAttractiveHeight()
|
||||||
|
|
||||||
|
if (dialogBinding.imageView.height > maxHeight) {
|
||||||
|
val verticalShrinkLayout = FrameLayout.LayoutParams(width, maxHeight)
|
||||||
|
dialogBinding.imageView.layoutParams = verticalShrinkLayout
|
||||||
|
dialogBinding.focusIndicator.layoutParams = verticalShrinkLayout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false // Pass through
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(dialogBinding.imageView)
|
||||||
|
|
||||||
|
val okListener = { dialog: DialogInterface, _: Int ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (!onUpdateFocus(dialogBinding.focusIndicator.getFocus())) {
|
||||||
|
showFailedFocusMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setView(dialogBinding.root)
|
||||||
|
.setPositiveButton(android.R.string.ok, okListener)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
val window = dialog.window
|
||||||
|
window?.setSoftInputMode(
|
||||||
|
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Activity.showFailedFocusMessage() {
|
||||||
|
Toast.makeText(this, R.string.error_failed_set_focus, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package com.keylesspalace.tusky.components.compose.view
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class FocusIndicatorView
|
||||||
|
@JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
private var focus: Attachment.Focus? = null
|
||||||
|
private var imageSize: Point? = null
|
||||||
|
private var circleRadius: Float? = null
|
||||||
|
|
||||||
|
fun setImageSize(width: Int, height: Int) {
|
||||||
|
this.imageSize = Point(width, height)
|
||||||
|
if (focus != null)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFocus(focus: Attachment.Focus) {
|
||||||
|
this.focus = focus
|
||||||
|
if (imageSize != null)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes setFocus called first
|
||||||
|
fun getFocus(): Attachment.Focus {
|
||||||
|
return focus!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// This needs to be consistent every time it is consulted over the lifetime of the object,
|
||||||
|
// so base it on the view width/height whenever the first access occurs.
|
||||||
|
private fun getCircleRadius(): Float {
|
||||||
|
val circleRadius = this.circleRadius
|
||||||
|
if (circleRadius != null)
|
||||||
|
return circleRadius
|
||||||
|
val newCircleRadius = min(this.width, this.height).toFloat() / 4.0f
|
||||||
|
this.circleRadius = newCircleRadius
|
||||||
|
return newCircleRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember focus uses -1..1 y-down coordinates (so focus value should be negated for y)
|
||||||
|
private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int): Float {
|
||||||
|
val offset = (outerLimit - innerLimit) / 2 // Assume image is centered in widget frame
|
||||||
|
val result = (value - offset) / innerLimit.toFloat() * 2.0f - 1.0f // To range -1..1
|
||||||
|
return min(1.0f, max(-1.0f, result)) // Clamp
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun axisFromFocus(value: Float, innerLimit: Int, outerLimit: Int): Float {
|
||||||
|
val offset = (outerLimit - innerLimit) / 2
|
||||||
|
return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility") // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget.
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
if (event.actionMasked == MotionEvent.ACTION_CANCEL)
|
||||||
|
return false
|
||||||
|
|
||||||
|
val imageSize = this.imageSize
|
||||||
|
if (imageSize == null)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Convert touch xy to point inside image
|
||||||
|
focus = Attachment.Focus(axisToFocus(event.x, imageSize.x, this.width), -axisToFocus(event.y, imageSize.y, this.height))
|
||||||
|
invalidate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val transparentDarkGray = 0x40000000
|
||||||
|
private val strokeWidth = 4.0f * this.resources.displayMetrics.density
|
||||||
|
|
||||||
|
private val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
|
private val curtainPath = Path()
|
||||||
|
|
||||||
|
init {
|
||||||
|
curtainPaint.color = transparentDarkGray
|
||||||
|
curtainPaint.style = Paint.Style.FILL
|
||||||
|
|
||||||
|
strokePaint.style = Paint.Style.STROKE
|
||||||
|
strokePaint.strokeWidth = strokeWidth
|
||||||
|
strokePaint.color = Color.WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
val imageSize = this.imageSize
|
||||||
|
val focus = this.focus
|
||||||
|
|
||||||
|
if (imageSize != null && focus != null) {
|
||||||
|
val x = axisFromFocus(focus.x, imageSize.x, this.width)
|
||||||
|
val y = axisFromFocus(-focus.y, imageSize.y, this.height)
|
||||||
|
val circleRadius = getCircleRadius()
|
||||||
|
|
||||||
|
curtainPath.reset() // Draw a flood fill with a hole cut out of it
|
||||||
|
curtainPath.fillType = Path.FillType.WINDING
|
||||||
|
curtainPath.addRect(0.0f, 0.0f, this.width.toFloat(), this.height.toFloat(), Path.Direction.CW)
|
||||||
|
curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW)
|
||||||
|
canvas.drawPath(curtainPath, curtainPaint)
|
||||||
|
|
||||||
|
canvas.drawCircle(x, y, circleRadius, strokePaint) // Draw white circle
|
||||||
|
canvas.drawCircle(x, y, strokeWidth / 2.0f, strokePaint) // Draw white dot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give a "safe" height based on currently set image size. Assume imageSize is set and height>width already checked
|
||||||
|
fun maxAttractiveHeight(): Int {
|
||||||
|
val height = this.imageSize!!.y
|
||||||
|
val circleRadius = getCircleRadius()
|
||||||
|
|
||||||
|
// Give us enough space for the image, plus on each side half a focus indicator circle, plus a strokeWidth
|
||||||
|
return ceil(height.toFloat() + circleRadius * 2.0f + strokeWidth).toInt()
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,9 +30,10 @@ import androidx.appcompat.widget.AppCompatImageView;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.view.MediaPreviewImageView;
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
import at.connyduck.sparkbutton.helpers.Utils;
|
||||||
|
|
||||||
public final class ProgressImageView extends AppCompatImageView {
|
public final class ProgressImageView extends MediaPreviewImageView {
|
||||||
|
|
||||||
private int progress = -1;
|
private int progress = -1;
|
||||||
private final RectF progressRect = new RectF();
|
private final RectF progressRect = new RectF();
|
||||||
|
@ -58,15 +59,14 @@ public final class ProgressImageView extends AppCompatImageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init() {
|
private void init() {
|
||||||
circlePaint.setColor(ContextCompat.getColor(getContext(), R.color.chinwag_green));
|
circlePaint.setColor(getContext().getColor(R.color.chinwag_green));
|
||||||
circlePaint.setStrokeWidth(Utils.dpToPx(getContext(), 4));
|
circlePaint.setStrokeWidth(Utils.dpToPx(getContext(), 4));
|
||||||
circlePaint.setStyle(Paint.Style.STROKE);
|
circlePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
|
||||||
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
|
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
|
||||||
|
|
||||||
markBgPaint.setStyle(Paint.Style.FILL);
|
markBgPaint.setStyle(Paint.Style.FILL);
|
||||||
markBgPaint.setColor(ContextCompat.getColor(getContext(),
|
markBgPaint.setColor(getContext().getColor(R.color.tusky_grey_10));
|
||||||
R.color.tusky_grey_10));
|
|
||||||
captionDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.spellcheck);
|
captionDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.spellcheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,8 +81,7 @@ public final class ProgressImageView extends AppCompatImageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setChecked(boolean checked) {
|
public void setChecked(boolean checked) {
|
||||||
this.markBgPaint.setColor(ContextCompat.getColor(getContext(),
|
this.markBgPaint.setColor(getContext().getColor(checked ? R.color.chinwag_green : R.color.tusky_grey_10));
|
||||||
checked ? R.color.chinwag_green : R.color.tusky_grey_10));
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,8 @@ data class ConversationStatusEntity(
|
||||||
val expanded: Boolean,
|
val expanded: Boolean,
|
||||||
val collapsed: Boolean,
|
val collapsed: Boolean,
|
||||||
val muted: Boolean,
|
val muted: Boolean,
|
||||||
val poll: Poll?
|
val poll: Poll?,
|
||||||
|
val language: String?,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toViewData(): StatusViewData.Concrete {
|
fun toViewData(): StatusViewData.Concrete {
|
||||||
|
@ -125,7 +126,8 @@ data class ConversationStatusEntity(
|
||||||
pinned = false,
|
pinned = false,
|
||||||
muted = muted,
|
muted = muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = null
|
card = null,
|
||||||
|
language = language,
|
||||||
),
|
),
|
||||||
isExpanded = expanded,
|
isExpanded = expanded,
|
||||||
isShowingContent = showingHiddenContent,
|
isShowingContent = showingHiddenContent,
|
||||||
|
@ -144,7 +146,11 @@ fun TimelineAccount.toEntity() =
|
||||||
emojis = emojis ?: emptyList()
|
emojis = emojis ?: emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Status.toEntity() =
|
fun Status.toEntity(
|
||||||
|
expanded: Boolean,
|
||||||
|
contentShowing: Boolean,
|
||||||
|
contentCollapsed: Boolean
|
||||||
|
) =
|
||||||
ConversationStatusEntity(
|
ConversationStatusEntity(
|
||||||
id = id,
|
id = id,
|
||||||
url = url,
|
url = url,
|
||||||
|
@ -163,19 +169,30 @@ fun Status.toEntity() =
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
showingHiddenContent = false,
|
showingHiddenContent = contentShowing,
|
||||||
expanded = false,
|
expanded = expanded,
|
||||||
collapsed = true,
|
collapsed = contentCollapsed,
|
||||||
muted = muted ?: false,
|
muted = muted ?: false,
|
||||||
poll = poll
|
poll = poll,
|
||||||
|
language = language,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Conversation.toEntity(accountId: Long, order: Int) =
|
fun Conversation.toEntity(
|
||||||
|
accountId: Long,
|
||||||
|
order: Int,
|
||||||
|
expanded: Boolean,
|
||||||
|
contentShowing: Boolean,
|
||||||
|
contentCollapsed: Boolean
|
||||||
|
) =
|
||||||
ConversationEntity(
|
ConversationEntity(
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
id = id,
|
id = id,
|
||||||
order = order,
|
order = order,
|
||||||
accounts = accounts.map { it.toEntity() },
|
accounts = accounts.map { it.toEntity() },
|
||||||
unread = unread,
|
unread = unread,
|
||||||
lastStatus = lastStatus!!.toEntity()
|
lastStatus = lastStatus!!.toEntity(
|
||||||
|
expanded = expanded,
|
||||||
|
contentShowing = contentShowing,
|
||||||
|
contentCollapsed = contentCollapsed
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -85,6 +85,7 @@ fun StatusViewData.Concrete.toConversationStatusEntity(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
collapsed = collapsed,
|
collapsed = collapsed,
|
||||||
muted = muted,
|
muted = muted,
|
||||||
poll = poll
|
poll = poll,
|
||||||
|
language = status.language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.paging.LoadType
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.RemoteMediator
|
import androidx.paging.RemoteMediator
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.HttpHeaderLink
|
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||||
|
@ -12,15 +13,17 @@ import retrofit2.HttpException
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
class ConversationsRemoteMediator(
|
class ConversationsRemoteMediator(
|
||||||
private val accountId: Long,
|
|
||||||
private val api: MastodonApi,
|
private val api: MastodonApi,
|
||||||
private val db: AppDatabase
|
private val db: AppDatabase,
|
||||||
|
accountManager: AccountManager,
|
||||||
) : RemoteMediator<Int, ConversationEntity>() {
|
) : RemoteMediator<Int, ConversationEntity>() {
|
||||||
|
|
||||||
private var nextKey: String? = null
|
private var nextKey: String? = null
|
||||||
|
|
||||||
private var order: Int = 0
|
private var order: Int = 0
|
||||||
|
|
||||||
|
private val activeAccount = accountManager.activeAccount!!
|
||||||
|
|
||||||
override suspend fun load(
|
override suspend fun load(
|
||||||
loadType: LoadType,
|
loadType: LoadType,
|
||||||
state: PagingState<Int, ConversationEntity>
|
state: PagingState<Int, ConversationEntity>
|
||||||
|
@ -46,7 +49,7 @@ class ConversationsRemoteMediator(
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
|
|
||||||
if (loadType == LoadType.REFRESH) {
|
if (loadType == LoadType.REFRESH) {
|
||||||
db.conversationDao().deleteForAccount(accountId)
|
db.conversationDao().deleteForAccount(activeAccount.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
val linkHeader = conversationsResponse.headers()["Link"]
|
val linkHeader = conversationsResponse.headers()["Link"]
|
||||||
|
@ -56,8 +59,19 @@ class ConversationsRemoteMediator(
|
||||||
db.conversationDao().insert(
|
db.conversationDao().insert(
|
||||||
conversations
|
conversations
|
||||||
.filterNot { it.lastStatus == null }
|
.filterNot { it.lastStatus == null }
|
||||||
.map {
|
.map { conversation ->
|
||||||
it.toEntity(accountId, order++)
|
|
||||||
|
val expanded = activeAccount.alwaysOpenSpoiler
|
||||||
|
val contentShowing = activeAccount.alwaysShowSensitiveMedia || !conversation.lastStatus!!.sensitive
|
||||||
|
val contentCollapsed = true
|
||||||
|
|
||||||
|
conversation.toEntity(
|
||||||
|
accountId = activeAccount.id,
|
||||||
|
order = order++,
|
||||||
|
expanded = expanded,
|
||||||
|
contentShowing = contentShowing,
|
||||||
|
contentCollapsed = contentCollapsed
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.usecase.TimelineCases
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
|
import com.keylesspalace.tusky.util.EmptyPagingSource
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
@ -42,8 +43,15 @@ class ConversationsViewModel @Inject constructor(
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
val conversationFlow = Pager(
|
val conversationFlow = Pager(
|
||||||
config = PagingConfig(pageSize = 30),
|
config = PagingConfig(pageSize = 30),
|
||||||
remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database),
|
remoteMediator = ConversationsRemoteMediator(api, database, accountManager),
|
||||||
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
pagingSourceFactory = {
|
||||||
|
val activeAccount = accountManager.activeAccount
|
||||||
|
if (activeAccount == null) {
|
||||||
|
EmptyPagingSource()
|
||||||
|
} else {
|
||||||
|
database.conversationDao().conversationsForAccount(activeAccount.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.flow
|
.flow
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
|
|
|
@ -25,9 +25,10 @@ import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.DraftAttachment
|
import com.keylesspalace.tusky.db.DraftAttachment
|
||||||
import com.keylesspalace.tusky.db.DraftEntity
|
import com.keylesspalace.tusky.db.DraftEntity
|
||||||
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.util.IOUtils
|
import com.keylesspalace.tusky.util.copyToFile
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -59,8 +60,11 @@ class DraftHelper @Inject constructor(
|
||||||
visibility: Status.Visibility,
|
visibility: Status.Visibility,
|
||||||
mediaUris: List<String>,
|
mediaUris: List<String>,
|
||||||
mediaDescriptions: List<String?>,
|
mediaDescriptions: List<String?>,
|
||||||
|
mediaFocus: List<Attachment.Focus?>,
|
||||||
poll: NewPoll?,
|
poll: NewPoll?,
|
||||||
failedToSend: Boolean
|
failedToSend: Boolean,
|
||||||
|
scheduledAt: String?,
|
||||||
|
language: String?,
|
||||||
) = withContext(Dispatchers.IO) {
|
) = withContext(Dispatchers.IO) {
|
||||||
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
||||||
|
|
||||||
|
@ -77,11 +81,11 @@ class DraftHelper @Inject constructor(
|
||||||
|
|
||||||
val uris = mediaUris.map { uriString ->
|
val uris = mediaUris.map { uriString ->
|
||||||
uriString.toUri()
|
uriString.toUri()
|
||||||
}.mapNotNull { uri ->
|
}.mapIndexedNotNull { index, uri ->
|
||||||
if (uri.isInFolder(draftDirectory)) {
|
if (uri.isInFolder(draftDirectory)) {
|
||||||
uri
|
uri
|
||||||
} else {
|
} else {
|
||||||
uri.copyToFolder(draftDirectory)
|
uri.copyToFolder(draftDirectory, index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +105,7 @@ class DraftHelper @Inject constructor(
|
||||||
DraftAttachment(
|
DraftAttachment(
|
||||||
uriString = uris[i].toString(),
|
uriString = uris[i].toString(),
|
||||||
description = mediaDescriptions[i],
|
description = mediaDescriptions[i],
|
||||||
|
focus = mediaFocus[i],
|
||||||
type = types[i]
|
type = types[i]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -116,7 +121,9 @@ class DraftHelper @Inject constructor(
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
failedToSend = failedToSend
|
failedToSend = failedToSend,
|
||||||
|
scheduledAt = scheduledAt,
|
||||||
|
language = language,
|
||||||
)
|
)
|
||||||
|
|
||||||
draftDao.insertOrReplace(draft)
|
draftDao.insertOrReplace(draft)
|
||||||
|
@ -153,7 +160,7 @@ class DraftHelper @Inject constructor(
|
||||||
return File(filePath).parentFile == folder
|
return File(filePath).parentFile == folder
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Uri.copyToFolder(folder: File): Uri? {
|
private fun Uri.copyToFolder(folder: File, index: Int): Uri? {
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||||
|
|
||||||
|
@ -165,7 +172,7 @@ class DraftHelper @Inject constructor(
|
||||||
map.getExtensionFromMimeType(mimeType)
|
map.getExtensionFromMimeType(mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension)
|
val filename = String.format("Tusky_Draft_Media_%s_%d.%s", timeStamp, index, fileExtension)
|
||||||
val file = File(folder, filename)
|
val file = File(folder, filename)
|
||||||
|
|
||||||
if (scheme == "https") {
|
if (scheme == "https") {
|
||||||
|
@ -187,7 +194,7 @@ class DraftHelper @Inject constructor(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOUtils.copyToFile(contentResolver, this, file)
|
this.copyToFile(contentResolver, file)
|
||||||
}
|
}
|
||||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ package com.keylesspalace.tusky.components.drafts
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
@ -26,6 +25,7 @@ import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.db.DraftAttachment
|
import com.keylesspalace.tusky.db.DraftAttachment
|
||||||
|
import com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||||
|
|
||||||
class DraftMediaAdapter(
|
class DraftMediaAdapter(
|
||||||
private val attachmentClick: () -> Unit
|
private val attachmentClick: () -> Unit
|
||||||
|
@ -42,24 +42,34 @@ class DraftMediaAdapter(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder {
|
||||||
return DraftMediaViewHolder(AppCompatImageView(parent.context))
|
return DraftMediaViewHolder(MediaPreviewImageView(parent.context))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) {
|
||||||
getItem(position)?.let { attachment ->
|
getItem(position)?.let { attachment ->
|
||||||
if (attachment.type == DraftAttachment.Type.AUDIO) {
|
if (attachment.type == DraftAttachment.Type.AUDIO) {
|
||||||
|
holder.imageView.clearFocus()
|
||||||
holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(holder.itemView.context)
|
if (attachment.focus != null)
|
||||||
|
holder.imageView.setFocalPoint(attachment.focus)
|
||||||
|
else
|
||||||
|
holder.imageView.clearFocus()
|
||||||
|
var glide = Glide.with(holder.itemView.context)
|
||||||
.load(attachment.uri)
|
.load(attachment.uri)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.into(holder.imageView)
|
.centerInside()
|
||||||
|
|
||||||
|
if (attachment.focus != null)
|
||||||
|
glide = glide.addListener(holder.imageView)
|
||||||
|
|
||||||
|
glide.into(holder.imageView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class DraftMediaViewHolder(val imageView: ImageView) :
|
inner class DraftMediaViewHolder(val imageView: MediaPreviewImageView) :
|
||||||
RecyclerView.ViewHolder(imageView) {
|
RecyclerView.ViewHolder(imageView) {
|
||||||
init {
|
init {
|
||||||
val thumbnailViewSize =
|
val thumbnailViewSize =
|
||||||
|
|
|
@ -106,7 +106,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
draftAttachments = draft.attachments,
|
draftAttachments = draft.attachments,
|
||||||
poll = draft.poll,
|
poll = draft.poll,
|
||||||
sensitive = draft.sensitive,
|
sensitive = draft.sensitive,
|
||||||
visibility = draft.visibility
|
visibility = draft.visibility,
|
||||||
|
scheduledAt = draft.scheduledAt,
|
||||||
|
language = draft.language,
|
||||||
)
|
)
|
||||||
|
|
||||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
@ -143,7 +145,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
draftAttachments = draft.attachments,
|
draftAttachments = draft.attachments,
|
||||||
poll = draft.poll,
|
poll = draft.poll,
|
||||||
sensitive = draft.sensitive,
|
sensitive = draft.sensitive,
|
||||||
visibility = draft.visibility
|
visibility = draft.visibility,
|
||||||
|
scheduledAt = draft.scheduledAt,
|
||||||
|
language = draft.language,
|
||||||
)
|
)
|
||||||
|
|
||||||
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
||||||
|
|
|
@ -21,5 +21,12 @@ data class InstanceInfo(
|
||||||
val pollMaxLength: Int,
|
val pollMaxLength: Int,
|
||||||
val pollMinDuration: Int,
|
val pollMinDuration: Int,
|
||||||
val pollMaxDuration: Int,
|
val pollMaxDuration: Int,
|
||||||
val charactersReservedPerUrl: Int
|
val charactersReservedPerUrl: Int,
|
||||||
|
val videoSizeLimit: Int,
|
||||||
|
val imageSizeLimit: Int,
|
||||||
|
val imageMatrixLimit: Int,
|
||||||
|
val maxMediaAttachments: Int,
|
||||||
|
val maxFields: Int,
|
||||||
|
val maxFieldNameLength: Int?,
|
||||||
|
val maxFieldValueLength: Int?
|
||||||
)
|
)
|
||||||
|
|
|
@ -45,7 +45,7 @@ class InstanceInfoRepository @Inject constructor(
|
||||||
*/
|
*/
|
||||||
suspend fun getEmojis(): List<Emoji> = withContext(Dispatchers.IO) {
|
suspend fun getEmojis(): List<Emoji> = withContext(Dispatchers.IO) {
|
||||||
api.getCustomEmojis()
|
api.getCustomEmojis()
|
||||||
.onSuccess { emojiList -> dao.insertOrReplace(EmojisEntity(instanceName, emojiList)) }
|
.onSuccess { emojiList -> dao.upsert(EmojisEntity(instanceName, emojiList)) }
|
||||||
.getOrElse { throwable ->
|
.getOrElse { throwable ->
|
||||||
Log.w(TAG, "failed to load custom emojis, falling back to cache", throwable)
|
Log.w(TAG, "failed to load custom emojis, falling back to cache", throwable)
|
||||||
dao.getEmojiInfo(instanceName)?.emojiList.orEmpty()
|
dao.getEmojiInfo(instanceName)?.emojiList.orEmpty()
|
||||||
|
@ -69,9 +69,16 @@ class InstanceInfoRepository @Inject constructor(
|
||||||
minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
|
minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
|
||||||
maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
|
maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
|
||||||
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
|
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
|
||||||
version = instance.version
|
version = instance.version,
|
||||||
|
videoSizeLimit = instance.configuration?.mediaAttachments?.videoSizeLimit ?: instance.uploadLimit,
|
||||||
|
imageSizeLimit = instance.configuration?.mediaAttachments?.imageSizeLimit ?: instance.uploadLimit,
|
||||||
|
imageMatrixLimit = instance.configuration?.mediaAttachments?.imageMatrixLimit,
|
||||||
|
maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments,
|
||||||
|
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
|
||||||
|
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
|
||||||
|
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength
|
||||||
)
|
)
|
||||||
dao.insertOrReplace(instanceEntity)
|
dao.upsert(instanceEntity)
|
||||||
instanceEntity
|
instanceEntity
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable ->
|
||||||
|
@ -85,7 +92,14 @@ class InstanceInfoRepository @Inject constructor(
|
||||||
pollMaxLength = instanceInfo?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
pollMaxLength = instanceInfo?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
||||||
pollMinDuration = instanceInfo?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
|
pollMinDuration = instanceInfo?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
|
||||||
pollMaxDuration = instanceInfo?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
|
pollMaxDuration = instanceInfo?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
|
||||||
charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL
|
charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL,
|
||||||
|
videoSizeLimit = instanceInfo?.videoSizeLimit ?: DEFAULT_VIDEO_SIZE_LIMIT,
|
||||||
|
imageSizeLimit = instanceInfo?.imageSizeLimit ?: DEFAULT_IMAGE_SIZE_LIMIT,
|
||||||
|
imageMatrixLimit = instanceInfo?.imageMatrixLimit ?: DEFAULT_IMAGE_MATRIX_LIMIT,
|
||||||
|
maxMediaAttachments = instanceInfo?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS,
|
||||||
|
maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS,
|
||||||
|
maxFieldNameLength = instanceInfo?.maxFieldNameLength,
|
||||||
|
maxFieldValueLength = instanceInfo?.maxFieldValueLength
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +113,14 @@ class InstanceInfoRepository @Inject constructor(
|
||||||
private const val DEFAULT_MIN_POLL_DURATION = 300
|
private const val DEFAULT_MIN_POLL_DURATION = 300
|
||||||
private const val DEFAULT_MAX_POLL_DURATION = 604800
|
private const val DEFAULT_MAX_POLL_DURATION = 604800
|
||||||
|
|
||||||
|
private const val DEFAULT_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
|
||||||
|
private const val DEFAULT_IMAGE_SIZE_LIMIT = 10485760 // 10MiB
|
||||||
|
private const val DEFAULT_IMAGE_MATRIX_LIMIT = 16777216 // 4096^2 Pixels
|
||||||
|
|
||||||
// Mastodon only counts URLs as this long in terms of status character limits
|
// Mastodon only counts URLs as this long in terms of status character limits
|
||||||
const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23
|
const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23
|
||||||
|
|
||||||
|
const val DEFAULT_MAX_MEDIA_ATTACHMENTS = 4
|
||||||
|
const val DEFAULT_MAX_ACCOUNT_FIELDS = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,7 +230,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
.addQueryParameter("response_type", "code")
|
.addQueryParameter("response_type", "code")
|
||||||
.addQueryParameter("scope", OAUTH_SCOPES)
|
.addQueryParameter("scope", OAUTH_SCOPES)
|
||||||
.build()
|
.build()
|
||||||
doWebViewAuth.launch(LoginData(url.toString().toUri(), oauthRedirectUri.toUri()))
|
doWebViewAuth.launch(LoginData(domain, url.toString().toUri(), oauthRedirectUri.toUri()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
@ -1,3 +1,18 @@
|
||||||
|
/* Copyright 2022 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.login
|
package com.keylesspalace.tusky.components.login
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
@ -16,15 +31,22 @@ import android.webkit.WebStorage
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLoginWebviewBinding
|
import com.keylesspalace.tusky.databinding.ActivityLoginWebviewBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/** Contract for starting [LoginWebViewActivity]. */
|
/** Contract for starting [LoginWebViewActivity]. */
|
||||||
class OauthLogin : ActivityResultContract<LoginData, LoginResult>() {
|
class OauthLogin : ActivityResultContract<LoginData, LoginResult>() {
|
||||||
|
@ -61,6 +83,7 @@ class OauthLogin : ActivityResultContract<LoginData, LoginResult>() {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LoginData(
|
data class LoginData(
|
||||||
|
val domain: String,
|
||||||
val url: Uri,
|
val url: Uri,
|
||||||
val oauthRedirectUrl: Uri,
|
val oauthRedirectUrl: Uri,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
@ -80,6 +103,11 @@ sealed class LoginResult : Parcelable {
|
||||||
class LoginWebViewActivity : BaseActivity(), Injectable {
|
class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
private val binding by viewBinding(ActivityLoginWebviewBinding::inflate)
|
private val binding by viewBinding(ActivityLoginWebviewBinding::inflate)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
private val viewModel: LoginWebViewViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -103,7 +131,7 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
webView.settings.databaseEnabled = false
|
webView.settings.databaseEnabled = false
|
||||||
webView.settings.displayZoomControls = false
|
webView.settings.displayZoomControls = false
|
||||||
webView.settings.javaScriptCanOpenWindowsAutomatically = false
|
webView.settings.javaScriptCanOpenWindowsAutomatically = false
|
||||||
// Javascript needs to be enabled because otherwise 2FA does not work in some instances
|
// JavaScript needs to be enabled because otherwise 2FA does not work in some instances
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
webView.settings.javaScriptEnabled = true
|
webView.settings.javaScriptEnabled = true
|
||||||
webView.settings.userAgentString += " Tusky/${BuildConfig.VERSION_NAME}"
|
webView.settings.userAgentString += " Tusky/${BuildConfig.VERSION_NAME}"
|
||||||
|
@ -161,6 +189,25 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
} else {
|
} else {
|
||||||
webView.restoreState(savedInstanceState)
|
webView.restoreState(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.loginRules.text = getString(R.string.instance_rule_info, data.domain)
|
||||||
|
|
||||||
|
viewModel.init(data.domain)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.instanceRules.collect { instanceRules ->
|
||||||
|
binding.loginRules.visible(instanceRules.isNotEmpty())
|
||||||
|
binding.loginRules.setOnClickListener {
|
||||||
|
AlertDialog.Builder(this@LoginWebViewActivity)
|
||||||
|
.setTitle(getString(R.string.instance_rule_title, data.domain))
|
||||||
|
.setMessage(
|
||||||
|
instanceRules.joinToString(separator = "\n\n") { "• $it" }
|
||||||
|
)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/* Copyright 2022 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.login
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LoginWebViewViewModel @Inject constructor(
|
||||||
|
private val api: MastodonApi
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val instanceRules: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
private var domain: String? = null
|
||||||
|
|
||||||
|
fun init(domain: String) {
|
||||||
|
if (this.domain == null) {
|
||||||
|
this.domain = domain
|
||||||
|
viewModelScope.launch {
|
||||||
|
api.getInstance(domain).fold({ instance ->
|
||||||
|
instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty()
|
||||||
|
}, { throwable ->
|
||||||
|
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,6 @@ import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.core.app.RemoteInput;
|
import androidx.core.app.RemoteInput;
|
||||||
import androidx.core.app.TaskStackBuilder;
|
import androidx.core.app.TaskStackBuilder;
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.work.Constraints;
|
import androidx.work.Constraints;
|
||||||
import androidx.work.NetworkType;
|
import androidx.work.NetworkType;
|
||||||
import androidx.work.PeriodicWorkRequest;
|
import androidx.work.PeriodicWorkRequest;
|
||||||
|
@ -57,7 +56,6 @@ import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Poll;
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
import com.keylesspalace.tusky.entity.PollOption;
|
import com.keylesspalace.tusky.entity.PollOption;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
|
||||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
||||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
||||||
import com.keylesspalace.tusky.util.StringUtils;
|
import com.keylesspalace.tusky.util.StringUtils;
|
||||||
|
@ -86,6 +84,8 @@ public class NotificationHelper {
|
||||||
*/
|
*/
|
||||||
public static final String ACCOUNT_ID = "account_id";
|
public static final String ACCOUNT_ID = "account_id";
|
||||||
|
|
||||||
|
public static final String TYPE = "type";
|
||||||
|
|
||||||
private static final String TAG = "NotificationHelper";
|
private static final String TAG = "NotificationHelper";
|
||||||
|
|
||||||
public static final String REPLY_ACTION = "REPLY_ACTION";
|
public static final String REPLY_ACTION = "REPLY_ACTION";
|
||||||
|
@ -270,6 +270,7 @@ public class NotificationHelper {
|
||||||
private static NotificationCompat.Builder newNotification(Context context, Notification body, AccountEntity account, boolean summary) {
|
private static NotificationCompat.Builder newNotification(Context context, Notification body, AccountEntity account, boolean summary) {
|
||||||
Intent summaryResultIntent = new Intent(context, MainActivity.class);
|
Intent summaryResultIntent = new Intent(context, MainActivity.class);
|
||||||
summaryResultIntent.putExtra(ACCOUNT_ID, account.getId());
|
summaryResultIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||||
|
summaryResultIntent.putExtra(TYPE, body.getType().name());
|
||||||
TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context);
|
TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context);
|
||||||
summaryStackBuilder.addParentStack(MainActivity.class);
|
summaryStackBuilder.addParentStack(MainActivity.class);
|
||||||
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
||||||
|
@ -280,6 +281,7 @@ public class NotificationHelper {
|
||||||
// we have to switch account here
|
// we have to switch account here
|
||||||
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
||||||
eventResultIntent.putExtra(ACCOUNT_ID, account.getId());
|
eventResultIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||||
|
eventResultIntent.putExtra(TYPE, body.getType().name());
|
||||||
TaskStackBuilder eventStackBuilder = TaskStackBuilder.create(context);
|
TaskStackBuilder eventStackBuilder = TaskStackBuilder.create(context);
|
||||||
eventStackBuilder.addParentStack(MainActivity.class);
|
eventStackBuilder.addParentStack(MainActivity.class);
|
||||||
eventStackBuilder.addNextIntent(eventResultIntent);
|
eventStackBuilder.addNextIntent(eventResultIntent);
|
||||||
|
@ -296,7 +298,7 @@ public class NotificationHelper {
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
||||||
.setDeleteIntent(deletePendingIntent)
|
.setDeleteIntent(deletePendingIntent)
|
||||||
.setColor(ContextCompat.getColor(context, R.color.notification_color))
|
.setColor(context.getColor(R.color.notification_color))
|
||||||
.setGroup(account.getAccountId())
|
.setGroup(account.getAccountId())
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setShortcutId(Long.toString(account.getId()))
|
.setShortcutId(Long.toString(account.getId()))
|
||||||
|
@ -367,6 +369,7 @@ public class NotificationHelper {
|
||||||
composeOptions.setReplyingStatusContent(citedText);
|
composeOptions.setReplyingStatusContent(citedText);
|
||||||
composeOptions.setMentionedUsernames(mentionedUsernames);
|
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||||
composeOptions.setModifiedInitialState(true);
|
composeOptions.setModifiedInitialState(true);
|
||||||
|
composeOptions.setLanguage(actionableStatus.getLanguage());
|
||||||
|
|
||||||
Intent composeIntent = ComposeActivity.startIntent(
|
Intent composeIntent = ComposeActivity.startIntent(
|
||||||
context,
|
context,
|
||||||
|
|
|
@ -154,6 +154,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO language
|
||||||
preferenceCategory(R.string.pref_publishing) {
|
preferenceCategory(R.string.pref_publishing) {
|
||||||
listPreference {
|
listPreference {
|
||||||
setTitle(R.string.pref_default_post_privacy)
|
setTitle(R.string.pref_default_post_privacy)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
@ -47,7 +48,17 @@ class PreferencesActivity :
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
||||||
|
|
||||||
private var restartActivitiesOnExit: Boolean = false
|
private val restartActivitiesOnBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
/* Switching themes won't actually change the theme of activities on the back stack.
|
||||||
|
* Either the back stack activities need to all be recreated, or do the easier thing, which
|
||||||
|
* is hijack the back button press and use it to launch a new MainActivity and clear the
|
||||||
|
* back stack. */
|
||||||
|
val intent = Intent(this@PreferencesActivity, MainActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
startActivityWithSlideInAnimation(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -61,30 +72,17 @@ class PreferencesActivity :
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE"
|
val preferenceType = intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)
|
||||||
|
|
||||||
|
val fragmentTag = "preference_fragment_$preferenceType"
|
||||||
|
|
||||||
val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
|
val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
|
||||||
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
|
?: when (preferenceType) {
|
||||||
GENERAL_PREFERENCES -> {
|
GENERAL_PREFERENCES -> PreferencesFragment.newInstance()
|
||||||
setTitle(R.string.action_view_preferences)
|
ACCOUNT_PREFERENCES -> AccountPreferencesFragment.newInstance()
|
||||||
PreferencesFragment.newInstance()
|
NOTIFICATION_PREFERENCES -> NotificationPreferencesFragment.newInstance()
|
||||||
}
|
TAB_FILTER_PREFERENCES -> TabFilterPreferencesFragment.newInstance()
|
||||||
ACCOUNT_PREFERENCES -> {
|
PROXY_PREFERENCES -> ProxyPreferencesFragment.newInstance()
|
||||||
setTitle(R.string.action_view_account_preferences)
|
|
||||||
AccountPreferencesFragment.newInstance()
|
|
||||||
}
|
|
||||||
NOTIFICATION_PREFERENCES -> {
|
|
||||||
setTitle(R.string.pref_title_edit_notification_settings)
|
|
||||||
NotificationPreferencesFragment.newInstance()
|
|
||||||
}
|
|
||||||
TAB_FILTER_PREFERENCES -> {
|
|
||||||
setTitle(R.string.pref_title_post_tabs)
|
|
||||||
TabFilterPreferencesFragment.newInstance()
|
|
||||||
}
|
|
||||||
PROXY_PREFERENCES -> {
|
|
||||||
setTitle(R.string.pref_title_http_proxy_settings)
|
|
||||||
ProxyPreferencesFragment.newInstance()
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException("preferenceType not known")
|
else -> throw IllegalArgumentException("preferenceType not known")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +90,16 @@ class PreferencesActivity :
|
||||||
replace(R.id.fragment_container, fragment, fragmentTag)
|
replace(R.id.fragment_container, fragment, fragmentTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
restartActivitiesOnExit = intent.getBooleanExtra("restart", false)
|
when (preferenceType) {
|
||||||
|
GENERAL_PREFERENCES -> setTitle(R.string.action_view_preferences)
|
||||||
|
ACCOUNT_PREFERENCES -> setTitle(R.string.action_view_account_preferences)
|
||||||
|
NOTIFICATION_PREFERENCES -> setTitle(R.string.pref_title_edit_notification_settings)
|
||||||
|
TAB_FILTER_PREFERENCES -> setTitle(R.string.pref_title_post_tabs)
|
||||||
|
PROXY_PREFERENCES -> setTitle(R.string.pref_title_http_proxy_settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, restartActivitiesOnBackPressedCallback)
|
||||||
|
restartActivitiesOnBackPressedCallback.isEnabled = savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -106,11 +113,11 @@ class PreferencesActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveInstanceState(outState: Bundle) {
|
private fun saveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean("restart", restartActivitiesOnExit)
|
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean("restart", restartActivitiesOnExit)
|
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,17 +128,13 @@ class PreferencesActivity :
|
||||||
Log.d("activeTheme", theme)
|
Log.d("activeTheme", theme)
|
||||||
ThemeUtils.setAppNightMode(theme)
|
ThemeUtils.setAppNightMode(theme)
|
||||||
|
|
||||||
restartActivitiesOnExit = true
|
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||||
this.restartCurrentActivity()
|
this.restartCurrentActivity()
|
||||||
}
|
}
|
||||||
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "useBlurhash",
|
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "useBlurhash",
|
||||||
"showCardsInTimelines", "confirmReblogs", "confirmFavourites",
|
"showSelfUsername", "showCardsInTimelines", "confirmReblogs", "confirmFavourites",
|
||||||
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
|
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
|
||||||
restartActivitiesOnExit = true
|
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||||
}
|
|
||||||
"language" -> {
|
|
||||||
restartActivitiesOnExit = true
|
|
||||||
this.restartCurrentActivity()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,20 +151,6 @@ class PreferencesActivity :
|
||||||
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
|
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
/* Switching themes won't actually change the theme of activities on the back stack.
|
|
||||||
* Either the back stack activities need to all be recreated, or do the easier thing, which
|
|
||||||
* is hijack the back button press and use it to launch a new MainActivity and clear the
|
|
||||||
* back stack. */
|
|
||||||
if (restartActivitiesOnExit) {
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
startActivityWithSlideInAnimation(intent)
|
|
||||||
} else {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
override fun androidInjector() = androidInjector
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -172,6 +161,7 @@ class PreferencesActivity :
|
||||||
const val TAB_FILTER_PREFERENCES = 3
|
const val TAB_FILTER_PREFERENCES = 3
|
||||||
const val PROXY_PREFERENCES = 4
|
const val PROXY_PREFERENCES = 4
|
||||||
private const val EXTRA_PREFERENCE_TYPE = "EXTRA_PREFERENCE_TYPE"
|
private const val EXTRA_PREFERENCE_TYPE = "EXTRA_PREFERENCE_TYPE"
|
||||||
|
private const val EXTRA_RESTART_ON_BACK = "restart"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun newIntent(context: Context, preferenceType: Int): Intent {
|
fun newIntent(context: Context, preferenceType: Int): Intent {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.keylesspalace.tusky.settings.makePreferenceScreen
|
||||||
import com.keylesspalace.tusky.settings.preference
|
import com.keylesspalace.tusky.settings.preference
|
||||||
import com.keylesspalace.tusky.settings.preferenceCategory
|
import com.keylesspalace.tusky.settings.preferenceCategory
|
||||||
import com.keylesspalace.tusky.settings.switchPreference
|
import com.keylesspalace.tusky.settings.switchPreference
|
||||||
|
import com.keylesspalace.tusky.util.LocaleManager
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.deserialize
|
import com.keylesspalace.tusky.util.deserialize
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
|
@ -46,6 +47,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var accountManager: AccountManager
|
lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var localeManager: LocaleManager
|
||||||
|
|
||||||
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
|
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
|
||||||
private var httpProxyPref: Preference? = null
|
private var httpProxyPref: Preference? = null
|
||||||
|
|
||||||
|
@ -71,10 +75,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
setDefaultValue("default")
|
setDefaultValue("default")
|
||||||
setEntries(R.array.language_entries)
|
setEntries(R.array.language_entries)
|
||||||
setEntryValues(R.array.language_values)
|
setEntryValues(R.array.language_values)
|
||||||
key = PrefKeys.LANGUAGE
|
key = PrefKeys.LANGUAGE + "_" // deliberately not the actual key, the real handling happens in LocaleManager
|
||||||
setSummaryProvider { entry }
|
setSummaryProvider { entry }
|
||||||
setTitle(R.string.pref_title_language)
|
setTitle(R.string.pref_title_language)
|
||||||
icon = makeIcon(GoogleMaterial.Icon.gmd_translate)
|
icon = makeIcon(GoogleMaterial.Icon.gmd_translate)
|
||||||
|
preferenceDataStore = localeManager
|
||||||
}
|
}
|
||||||
|
|
||||||
listPreference {
|
listPreference {
|
||||||
|
@ -96,6 +101,16 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
setTitle(R.string.pref_main_nav_position)
|
setTitle(R.string.pref_main_nav_position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listPreference {
|
||||||
|
setDefaultValue("disambiguate")
|
||||||
|
setEntries(R.array.pref_show_self_username_names)
|
||||||
|
setEntryValues(R.array.pref_show_self_username_values)
|
||||||
|
key = PrefKeys.SHOW_SELF_USERNAME
|
||||||
|
setSummaryProvider { entry }
|
||||||
|
setTitle(R.string.pref_title_show_self_username)
|
||||||
|
isSingleLineTitle = false
|
||||||
|
}
|
||||||
|
|
||||||
switchPreference {
|
switchPreference {
|
||||||
setDefaultValue(false)
|
setDefaultValue(false)
|
||||||
key = PrefKeys.HIDE_TOP_TOOLBAR
|
key = PrefKeys.HIDE_TOP_TOOLBAR
|
||||||
|
|
|
@ -19,6 +19,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
@ -134,7 +135,13 @@ class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, I
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(item: ScheduledStatus) {
|
override fun delete(item: ScheduledStatus) {
|
||||||
viewModel.deleteScheduledStatus(item)
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.delete_scheduled_post_warning)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
viewModel.deleteScheduledStatus(item)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -57,8 +57,7 @@ class ScheduledStatusAdapter(
|
||||||
v.isEnabled = false
|
v.isEnabled = false
|
||||||
listener.edit(item)
|
listener.edit(item)
|
||||||
}
|
}
|
||||||
holder.binding.delete.setOnClickListener { v: View ->
|
holder.binding.delete.setOnClickListener {
|
||||||
v.isEnabled = false
|
|
||||||
listener.delete(item)
|
listener.delete(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import com.keylesspalace.tusky.components.search.adapter.SearchPagingSourceFacto
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.DeletedStatus
|
import com.keylesspalace.tusky.entity.DeletedStatus
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.usecase.TimelineCases
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
|
@ -113,11 +112,7 @@ class SearchViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) {
|
fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) {
|
||||||
val idx = loadedStatuses.indexOf(statusViewData)
|
updateStatusViewData(statusViewData.copy(isExpanded = expanded))
|
||||||
if (idx >= 0) {
|
|
||||||
loadedStatuses[idx] = statusViewData.copy(isExpanded = expanded)
|
|
||||||
statusesPagingSourceFactory.invalidate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
||||||
|
@ -131,51 +126,34 @@ class SearchViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setRebloggedForStatus(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
private fun setRebloggedForStatus(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
||||||
statusViewData.status.reblogged = reblog
|
updateStatus(
|
||||||
statusViewData.status.reblog?.reblogged = reblog
|
statusViewData.status.copy(
|
||||||
statusesPagingSourceFactory.invalidate()
|
reblogged = reblog,
|
||||||
|
reblog = statusViewData.status.reblog?.copy(reblogged = reblog)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) {
|
fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) {
|
||||||
val idx = loadedStatuses.indexOf(statusViewData)
|
updateStatusViewData(statusViewData.copy(isShowingContent = isShowing))
|
||||||
if (idx >= 0) {
|
|
||||||
loadedStatuses[idx] = statusViewData.copy(isShowingContent = isShowing)
|
|
||||||
statusesPagingSourceFactory.invalidate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) {
|
fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) {
|
||||||
val idx = loadedStatuses.indexOf(statusViewData)
|
updateStatusViewData(statusViewData.copy(isCollapsed = collapsed))
|
||||||
if (idx >= 0) {
|
|
||||||
loadedStatuses[idx] = statusViewData.copy(isCollapsed = collapsed)
|
|
||||||
statusesPagingSourceFactory.invalidate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList<Int>) {
|
fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList<Int>) {
|
||||||
val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices)
|
val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices)
|
||||||
updateStatus(statusViewData, votedPoll)
|
updateStatus(statusViewData.status.copy(poll = votedPoll))
|
||||||
timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices)
|
timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.doOnError { t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) }
|
||||||
{ newPoll -> updateStatus(statusViewData, newPoll) },
|
.subscribe()
|
||||||
{ t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) }
|
|
||||||
)
|
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStatus(statusViewData: StatusViewData.Concrete, newPoll: Poll) {
|
|
||||||
val idx = loadedStatuses.indexOf(statusViewData)
|
|
||||||
if (idx >= 0) {
|
|
||||||
val newStatus = statusViewData.status.copy(poll = newPoll)
|
|
||||||
loadedStatuses[idx] = statusViewData.copy(status = newStatus)
|
|
||||||
statusesPagingSourceFactory.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) {
|
fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) {
|
||||||
statusViewData.status.favourited = isFavorited
|
updateStatus(statusViewData.status.copy(favourited = isFavorited))
|
||||||
statusesPagingSourceFactory.invalidate()
|
|
||||||
timelineCases.favourite(statusViewData.id, isFavorited)
|
timelineCases.favourite(statusViewData.id, isFavorited)
|
||||||
.onErrorReturnItem(statusViewData.status)
|
.onErrorReturnItem(statusViewData.status)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
@ -183,18 +161,13 @@ class SearchViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) {
|
fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) {
|
||||||
statusViewData.status.bookmarked = isBookmarked
|
updateStatus(statusViewData.status.copy(bookmarked = isBookmarked))
|
||||||
statusesPagingSourceFactory.invalidate()
|
|
||||||
timelineCases.bookmark(statusViewData.id, isBookmarked)
|
timelineCases.bookmark(statusViewData.id, isBookmarked)
|
||||||
.onErrorReturnItem(statusViewData.status)
|
.onErrorReturnItem(statusViewData.status)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllAccountsOrderedByActive(): List<AccountEntity> {
|
|
||||||
return accountManager.getAllAccountsOrderedByActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) {
|
fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) {
|
||||||
timelineCases.mute(accountId, notifications, duration)
|
timelineCases.mute(accountId, notifications, duration)
|
||||||
}
|
}
|
||||||
|
@ -212,18 +185,28 @@ class SearchViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) {
|
fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) {
|
||||||
val idx = loadedStatuses.indexOf(statusViewData)
|
updateStatus(statusViewData.status.copy(muted = mute))
|
||||||
if (idx >= 0) {
|
|
||||||
val newStatus = statusViewData.status.copy(muted = mute)
|
|
||||||
loadedStatuses[idx] = statusViewData.copy(status = newStatus)
|
|
||||||
statusesPagingSourceFactory.invalidate()
|
|
||||||
}
|
|
||||||
timelineCases.muteConversation(statusViewData.id, mute)
|
timelineCases.muteConversation(statusViewData.id, mute)
|
||||||
.onErrorReturnItem(statusViewData.status)
|
.onErrorReturnItem(statusViewData.status)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateStatusViewData(newStatusViewData: StatusViewData.Concrete) {
|
||||||
|
val idx = loadedStatuses.indexOfFirst { it.id == newStatusViewData.id }
|
||||||
|
if (idx >= 0) {
|
||||||
|
loadedStatuses[idx] = newStatusViewData
|
||||||
|
statusesPagingSourceFactory.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStatus(newStatus: Status) {
|
||||||
|
val statusViewData = loadedStatuses.find { it.id == newStatus.id }
|
||||||
|
if (statusViewData != null) {
|
||||||
|
updateStatusViewData(statusViewData.copy(status = newStatus))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SearchViewModel"
|
private const val TAG = "SearchViewModel"
|
||||||
private const val DEFAULT_LOAD_SIZE = 20
|
private const val DEFAULT_LOAD_SIZE = 20
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -216,7 +217,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
contentWarning = actionableStatus.spoilerText,
|
contentWarning = actionableStatus.spoilerText,
|
||||||
mentionedUsernames = mentionedUsernames,
|
mentionedUsernames = mentionedUsernames,
|
||||||
replyingStatusAuthor = actionableStatus.account.localUsername,
|
replyingStatusAuthor = actionableStatus.account.localUsername,
|
||||||
replyingStatusContent = status.content.toString()
|
replyingStatusContent = status.content.toString(),
|
||||||
|
language = actionableStatus.language,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
bottomSheetActivity?.startActivityWithSlideInAnimation(intent)
|
bottomSheetActivity?.startActivityWithSlideInAnimation(intent)
|
||||||
|
@ -288,7 +290,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
|
|
||||||
val stringToShare = statusToShare.account.username +
|
val stringToShare = statusToShare.account.username +
|
||||||
" - " +
|
" - " +
|
||||||
statusToShare.content.toString()
|
statusToShare.content
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
|
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
|
||||||
sendIntent.type = "text/plain"
|
sendIntent.type = "text/plain"
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_post_content_to)))
|
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_post_content_to)))
|
||||||
|
@ -382,7 +384,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
} != null
|
} != null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) {
|
private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence?) {
|
||||||
bottomSheetActivity?.showAccountChooserDialog(
|
bottomSheetActivity?.showAccountChooserDialog(
|
||||||
dialogTitle, false,
|
dialogTitle, false,
|
||||||
object : AccountSelectionListener {
|
object : AccountSelectionListener {
|
||||||
|
@ -407,13 +409,21 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestDownloadAllMedia(status: Status) {
|
private fun requestDownloadAllMedia(status: Status) {
|
||||||
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
(activity as BaseActivity).requestPermissions(permissions) { _, grantResults ->
|
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
(activity as BaseActivity).requestPermissions(permissions) { _, grantResults ->
|
||||||
downloadAllMedia(status)
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
} else {
|
downloadAllMedia(status)
|
||||||
Toast.makeText(context, R.string.error_media_download_permission, Toast.LENGTH_SHORT).show()
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
R.string.error_media_download_permission,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
downloadAllMedia(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,7 +471,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
contentWarning = redraftStatus.spoilerText,
|
contentWarning = redraftStatus.spoilerText,
|
||||||
mediaAttachments = redraftStatus.attachments,
|
mediaAttachments = redraftStatus.attachments,
|
||||||
sensitive = redraftStatus.sensitive,
|
sensitive = redraftStatus.sensitive,
|
||||||
poll = redraftStatus.poll?.toNewPoll(status.createdAt)
|
poll = redraftStatus.poll?.toNewPoll(status.createdAt),
|
||||||
|
language = redraftStatus.language,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|
|
@ -99,7 +99,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
||||||
contentShowing = false,
|
contentShowing = false,
|
||||||
pinned = false,
|
pinned = false,
|
||||||
card = null,
|
card = null,
|
||||||
repliesCount = 0
|
repliesCount = 0,
|
||||||
|
language = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +142,8 @@ fun Status.toEntity(
|
||||||
contentCollapsed = contentCollapsed,
|
contentCollapsed = contentCollapsed,
|
||||||
pinned = actionableStatus.pinned == true,
|
pinned = actionableStatus.pinned == true,
|
||||||
card = actionableStatus.card?.let(gson::toJson),
|
card = actionableStatus.card?.let(gson::toJson),
|
||||||
repliesCount = actionableStatus.repliesCount
|
repliesCount = actionableStatus.repliesCount,
|
||||||
|
language = actionableStatus.language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +187,8 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = card,
|
card = card,
|
||||||
repliesCount = status.repliesCount
|
repliesCount = status.repliesCount,
|
||||||
|
language = status.language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val status = if (reblog != null) {
|
val status = if (reblog != null) {
|
||||||
|
@ -216,6 +219,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
poll = null,
|
poll = null,
|
||||||
card = null,
|
card = null,
|
||||||
repliesCount = status.repliesCount,
|
repliesCount = status.repliesCount,
|
||||||
|
language = status.language,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Status(
|
Status(
|
||||||
|
@ -245,6 +249,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = card,
|
card = card,
|
||||||
repliesCount = status.repliesCount,
|
repliesCount = status.repliesCount,
|
||||||
|
language = status.language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return StatusViewData.Concrete(
|
return StatusViewData.Concrete(
|
||||||
|
|
|
@ -30,7 +30,6 @@ import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import kotlinx.coroutines.rx3.await
|
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
@ -71,7 +70,7 @@ class CachedTimelineRemoteMediator(
|
||||||
maxId = cachedTopId,
|
maxId = cachedTopId,
|
||||||
sinceId = topPlaceholderId, // so already existing placeholders don't get accidentally overwritten
|
sinceId = topPlaceholderId, // so already existing placeholders don't get accidentally overwritten
|
||||||
limit = state.config.pageSize
|
limit = state.config.pageSize
|
||||||
).await()
|
)
|
||||||
|
|
||||||
val statuses = statusResponse.body()
|
val statuses = statusResponse.body()
|
||||||
if (statusResponse.isSuccessful && statuses != null) {
|
if (statusResponse.isSuccessful && statuses != null) {
|
||||||
|
@ -86,14 +85,14 @@ class CachedTimelineRemoteMediator(
|
||||||
|
|
||||||
val statusResponse = when (loadType) {
|
val statusResponse = when (loadType) {
|
||||||
LoadType.REFRESH -> {
|
LoadType.REFRESH -> {
|
||||||
api.homeTimeline(sinceId = topPlaceholderId, limit = state.config.pageSize).await()
|
api.homeTimeline(sinceId = topPlaceholderId, limit = state.config.pageSize)
|
||||||
}
|
}
|
||||||
LoadType.PREPEND -> {
|
LoadType.PREPEND -> {
|
||||||
return MediatorResult.Success(endOfPaginationReached = true)
|
return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
}
|
}
|
||||||
LoadType.APPEND -> {
|
LoadType.APPEND -> {
|
||||||
val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.status?.serverId
|
val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.status?.serverId
|
||||||
api.homeTimeline(maxId = maxId, limit = state.config.pageSize).await()
|
api.homeTimeline(maxId = maxId, limit = state.config.pageSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.usecase.TimelineCases
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
|
import com.keylesspalace.tusky.util.EmptyPagingSource
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
|
@ -50,7 +51,6 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
@ -86,7 +86,7 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
pagingSourceFactory = {
|
pagingSourceFactory = {
|
||||||
val activeAccount = accountManager.activeAccount
|
val activeAccount = accountManager.activeAccount
|
||||||
if (activeAccount == null) {
|
if (activeAccount == null) {
|
||||||
EmptyTimelinePagingSource()
|
EmptyPagingSource()
|
||||||
} else {
|
} else {
|
||||||
db.timelineDao().getStatuses(activeAccount.id)
|
db.timelineDao().getStatuses(activeAccount.id)
|
||||||
}.also { newPagingSource ->
|
}.also { newPagingSource ->
|
||||||
|
@ -176,7 +176,7 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
sinceId = nextPlaceholderId,
|
sinceId = nextPlaceholderId,
|
||||||
limit = LOAD_AT_ONCE
|
limit = LOAD_AT_ONCE
|
||||||
)
|
)
|
||||||
}.await()
|
}
|
||||||
|
|
||||||
val statuses = response.body()
|
val statuses = response.body()
|
||||||
if (!response.isSuccessful || statuses == null) {
|
if (!response.isSuccessful || statuses == null) {
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package com.keylesspalace.tusky.components.timeline.viewmodel
|
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
|
||||||
import androidx.paging.PagingState
|
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
|
||||||
|
|
||||||
class EmptyTimelinePagingSource : PagingSource<Int, TimelineStatusWithAccount>() {
|
|
||||||
override fun getRefreshKey(state: PagingState<Int, TimelineStatusWithAccount>): Int? = null
|
|
||||||
|
|
||||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TimelineStatusWithAccount> = LoadResult.Page(emptyList(), null, null)
|
|
||||||
}
|
|
|
@ -45,7 +45,6 @@ import kotlinx.coroutines.asExecutor
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -298,7 +297,7 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
Kind.FAVOURITES -> api.favourites(fromId, uptoId, limit)
|
Kind.FAVOURITES -> api.favourites(fromId, uptoId, limit)
|
||||||
Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, limit)
|
Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, limit)
|
||||||
Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, limit)
|
Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, limit)
|
||||||
}.await()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun StatusViewData.Concrete.update() {
|
private fun StatusViewData.Concrete.update() {
|
||||||
|
|
|
@ -13,16 +13,16 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view
|
package com.keylesspalace.tusky.components.viewthread
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.forEach
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.adapter.ThreadAdapter
|
|
||||||
|
|
||||||
class ConversationLineItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
|
class ConversationLineItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
@ -32,29 +32,25 @@ class ConversationLineItemDecoration(private val context: Context) : RecyclerVie
|
||||||
val dividerStart = parent.paddingStart + context.resources.getDimensionPixelSize(R.dimen.status_line_margin_start)
|
val dividerStart = parent.paddingStart + context.resources.getDimensionPixelSize(R.dimen.status_line_margin_start)
|
||||||
val dividerEnd = dividerStart + divider.intrinsicWidth
|
val dividerEnd = dividerStart + divider.intrinsicWidth
|
||||||
|
|
||||||
val childCount = parent.childCount
|
|
||||||
val avatarMargin = context.resources.getDimensionPixelSize(R.dimen.account_avatar_margin)
|
val avatarMargin = context.resources.getDimensionPixelSize(R.dimen.account_avatar_margin)
|
||||||
|
|
||||||
for (i in 0 until childCount) {
|
val items = (parent.adapter as ThreadAdapter).currentList
|
||||||
val child = parent.getChildAt(i)
|
|
||||||
|
parent.forEach { child ->
|
||||||
|
|
||||||
val position = parent.getChildAdapterPosition(child)
|
val position = parent.getChildAdapterPosition(child)
|
||||||
val adapter = parent.adapter as ThreadAdapter
|
|
||||||
|
|
||||||
val current = adapter.getItem(position)
|
val current = items.getOrNull(position)
|
||||||
val dividerTop: Int
|
|
||||||
val dividerBottom: Int
|
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
val above = adapter.getItem(position - 1)
|
val above = items.getOrNull(position - 1)
|
||||||
dividerTop = if (above != null && above.id == current.status.inReplyToId) {
|
val dividerTop = if (above != null && above.id == current.status.inReplyToId) {
|
||||||
child.top
|
child.top
|
||||||
} else {
|
} else {
|
||||||
child.top + avatarMargin
|
child.top + avatarMargin
|
||||||
}
|
}
|
||||||
val below = adapter.getItem(position + 1)
|
val below = items.getOrNull(position + 1)
|
||||||
dividerBottom = if (below != null && current.id == below.status.inReplyToId &&
|
val dividerBottom = if (below != null && current.id == below.status.inReplyToId && !current.isDetailed) {
|
||||||
adapter.detailedStatusPosition != position
|
|
||||||
) {
|
|
||||||
child.bottom
|
child.bottom
|
||||||
} else {
|
} else {
|
||||||
child.top + avatarMargin
|
child.top + avatarMargin
|
|
@ -0,0 +1,95 @@
|
||||||
|
/* 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.viewthread
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
||||||
|
import com.keylesspalace.tusky.adapter.StatusDetailedViewHolder
|
||||||
|
import com.keylesspalace.tusky.adapter.StatusViewHolder
|
||||||
|
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
|
||||||
|
) : ListAdapter<StatusViewData.Concrete, StatusBaseViewHolder>(ThreadDifferCallback) {
|
||||||
|
|
||||||
|
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 = getItem(position)
|
||||||
|
viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return if (getItem(position).isDetailed) {
|
||||||
|
VIEW_TYPE_STATUS_DETAILED
|
||||||
|
} else {
|
||||||
|
VIEW_TYPE_STATUS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VIEW_TYPE_STATUS = 0
|
||||||
|
private const val VIEW_TYPE_STATUS_DETAILED = 1
|
||||||
|
|
||||||
|
val ThreadDifferCallback = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: StatusViewData.Concrete,
|
||||||
|
newItem: StatusViewData.Concrete
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: StatusViewData.Concrete,
|
||||||
|
newItem: StatusViewData.Concrete
|
||||||
|
): Boolean {
|
||||||
|
return false // Items are different always. It allows to refresh timestamp on every view holder update
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(
|
||||||
|
oldItem: StatusViewData.Concrete,
|
||||||
|
newItem: StatusViewData.Concrete
|
||||||
|
): Any? {
|
||||||
|
return if (oldItem == newItem) {
|
||||||
|
// If items are equal - update timestamp only
|
||||||
|
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
||||||
|
} else // If items are different - update the whole view holder
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/* Copyright 2022 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.viewthread
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.keylesspalace.tusky.BottomSheetActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import dagger.android.DispatchingAndroidInjector
|
||||||
|
import dagger.android.HasAndroidInjector
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ViewThreadActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_view_thread)
|
||||||
|
val id = intent.getStringExtra(ID_EXTRA)!!
|
||||||
|
val url = intent.getStringExtra(URL_EXTRA)!!
|
||||||
|
val fragment =
|
||||||
|
supportFragmentManager.findFragmentByTag(FRAGMENT_TAG + id) as ViewThreadFragment?
|
||||||
|
?: ViewThreadFragment.newInstance(id, url)
|
||||||
|
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.fragment_container, fragment, FRAGMENT_TAG + id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun androidInjector() = dispatchingAndroidInjector
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun startIntent(context: Context, id: String, url: String): Intent {
|
||||||
|
val intent = Intent(context, ViewThreadActivity::class.java)
|
||||||
|
intent.putExtra(ID_EXTRA, id)
|
||||||
|
intent.putExtra(URL_EXTRA, url)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val ID_EXTRA = "id"
|
||||||
|
private const val URL_EXTRA = "url"
|
||||||
|
private const val FRAGMENT_TAG = "ViewThreadFragment_"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,337 @@
|
||||||
|
/* Copyright 2022 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.viewthread
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.keylesspalace.tusky.AccountListActivity
|
||||||
|
import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent
|
||||||
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.FragmentViewThreadBinding
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
import com.keylesspalace.tusky.fragment.SFragment
|
||||||
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
import com.keylesspalace.tusky.util.CardViewMode
|
||||||
|
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
|
||||||
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.openLink
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData.Companion.list
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
private val viewModel: ViewThreadViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
|
private val binding by viewBinding(FragmentViewThreadBinding::bind)
|
||||||
|
|
||||||
|
private lateinit var adapter: ThreadAdapter
|
||||||
|
private lateinit var thisThreadsStatusId: String
|
||||||
|
|
||||||
|
private var alwaysShowSensitiveMedia = false
|
||||||
|
private var alwaysOpenSpoiler = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
thisThreadsStatusId = requireArguments().getString(ID_EXTRA)!!
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
|
||||||
|
val statusDisplayOptions = StatusDisplayOptions(
|
||||||
|
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||||
|
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
||||||
|
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
|
||||||
|
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
|
||||||
|
useBlurhash = preferences.getBoolean("useBlurhash", true),
|
||||||
|
cardViewMode = if (preferences.getBoolean("showCardsInTimelines", false)) {
|
||||||
|
CardViewMode.INDENTED
|
||||||
|
} else {
|
||||||
|
CardViewMode.NONE
|
||||||
|
},
|
||||||
|
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
|
||||||
|
confirmFavourites = preferences.getBoolean("confirmFavourites", false),
|
||||||
|
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
||||||
|
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
|
)
|
||||||
|
adapter = ThreadAdapter(statusDisplayOptions, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_view_thread, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
binding.toolbar.setNavigationOnClickListener {
|
||||||
|
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||||
|
}
|
||||||
|
binding.toolbar.setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.action_reveal -> {
|
||||||
|
viewModel.toggleRevealButton()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_open_in_web -> {
|
||||||
|
context?.openLink(requireArguments().getString(URL_EXTRA)!!)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
||||||
|
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||||
|
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
|
binding.recyclerView.setAccessibilityDelegateCompat(
|
||||||
|
ListStatusAccessibilityDelegate(
|
||||||
|
binding.recyclerView,
|
||||||
|
this
|
||||||
|
) { index -> adapter.currentList.getOrNull(index) }
|
||||||
|
)
|
||||||
|
val divider = DividerItemDecoration(context, LinearLayout.VERTICAL)
|
||||||
|
binding.recyclerView.addItemDecoration(divider)
|
||||||
|
binding.recyclerView.addItemDecoration(ConversationLineItemDecoration(requireContext()))
|
||||||
|
alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia
|
||||||
|
alwaysOpenSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
|
||||||
|
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is ThreadUiState.Loading -> {
|
||||||
|
updateRevealButton(RevealButtonState.NO_BUTTON)
|
||||||
|
binding.recyclerView.hide()
|
||||||
|
binding.statusView.hide()
|
||||||
|
binding.progressBar.show()
|
||||||
|
}
|
||||||
|
is ThreadUiState.Error -> {
|
||||||
|
Log.w(TAG, "failed to load status", uiState.throwable)
|
||||||
|
|
||||||
|
updateRevealButton(RevealButtonState.NO_BUTTON)
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
|
||||||
|
binding.recyclerView.hide()
|
||||||
|
binding.statusView.show()
|
||||||
|
binding.progressBar.hide()
|
||||||
|
|
||||||
|
if (uiState.throwable is IOException) {
|
||||||
|
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
||||||
|
viewModel.retry(thisThreadsStatusId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||||
|
viewModel.retry(thisThreadsStatusId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ThreadUiState.Success -> {
|
||||||
|
adapter.submitList(uiState.statuses) {
|
||||||
|
if (viewModel.isInitialLoad) {
|
||||||
|
viewModel.isInitialLoad = false
|
||||||
|
val detailedPosition = adapter.currentList.indexOfFirst { viewData ->
|
||||||
|
viewData.isDetailed
|
||||||
|
}
|
||||||
|
binding.recyclerView.scrollToPosition(detailedPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRevealButton(uiState.revealButton)
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = uiState.refreshing
|
||||||
|
|
||||||
|
binding.recyclerView.show()
|
||||||
|
binding.statusView.hide()
|
||||||
|
binding.progressBar.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.errors.collect { throwable ->
|
||||||
|
Log.w(TAG, "failed to load status context", throwable)
|
||||||
|
Snackbar.make(binding.root, R.string.error_generic, Snackbar.LENGTH_SHORT)
|
||||||
|
.setAction(R.string.action_retry) {
|
||||||
|
viewModel.retry(thisThreadsStatusId)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.loadThread(thisThreadsStatusId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRevealButton(state: RevealButtonState) {
|
||||||
|
val menuItem = binding.toolbar.menu.findItem(R.id.action_reveal)
|
||||||
|
|
||||||
|
menuItem.isVisible = state != RevealButtonState.NO_BUTTON
|
||||||
|
menuItem.setIcon(if (state == RevealButtonState.REVEAL) R.drawable.ic_eye_24dp else R.drawable.ic_hide_media_24dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRefresh() {
|
||||||
|
viewModel.refresh(thisThreadsStatusId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReply(position: Int) {
|
||||||
|
super.reply(adapter.currentList[position].status)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReblog(reblog: Boolean, position: Int) {
|
||||||
|
val status = adapter.currentList[position]
|
||||||
|
viewModel.reblog(reblog, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||||
|
val status = adapter.currentList[position]
|
||||||
|
viewModel.favorite(favourite, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBookmark(bookmark: Boolean, position: Int) {
|
||||||
|
val status = adapter.currentList[position]
|
||||||
|
viewModel.bookmark(bookmark, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMore(view: View, position: Int) {
|
||||||
|
super.more(adapter.currentList[position].status, view, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||||
|
val status = adapter.currentList[position].status
|
||||||
|
super.viewMedia(attachmentIndex, list(status), view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewThread(position: Int) {
|
||||||
|
val status = adapter.currentList[position]
|
||||||
|
if (thisThreadsStatusId == status.id) {
|
||||||
|
// If already viewing this thread, don't reopen it.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.viewThread(status.actionableId, status.actionable.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewUrl(url: String) {
|
||||||
|
val status: StatusViewData.Concrete? = viewModel.detailedStatus()
|
||||||
|
if (status != null && status.status.url == url) {
|
||||||
|
// already viewing the status with this url
|
||||||
|
// probably just a preview federated and the user is clicking again to view more -> open the browser
|
||||||
|
// this can happen with some friendica statuses
|
||||||
|
requireContext().openLink(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.onViewUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpenReblog(position: Int) {
|
||||||
|
// there are no reblogs in threads
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||||
|
viewModel.changeExpanded(expanded, adapter.currentList[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||||
|
viewModel.changeContentShowing(isShowing, adapter.currentList[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadMore(position: Int) {
|
||||||
|
// only used in timelines
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowReblogs(position: Int) {
|
||||||
|
val statusId = adapter.currentList[position].id
|
||||||
|
val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId)
|
||||||
|
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowFavs(position: Int) {
|
||||||
|
val statusId = adapter.currentList[position].id
|
||||||
|
val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId)
|
||||||
|
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||||
|
viewModel.changeContentCollapsed(isCollapsed, adapter.currentList[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewTag(tag: String) {
|
||||||
|
super.viewTag(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewAccount(id: String) {
|
||||||
|
super.viewAccount(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun removeItem(position: Int) {
|
||||||
|
val status = adapter.currentList[position]
|
||||||
|
if (status.isDetailed) {
|
||||||
|
// the main status we are viewing is being removed, finish the activity
|
||||||
|
activity?.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModel.removeStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVoteInPoll(position: Int, choices: List<Int>) {
|
||||||
|
val status = adapter.currentList[position]
|
||||||
|
viewModel.voteInPoll(choices, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ViewThreadFragment"
|
||||||
|
|
||||||
|
private const val ID_EXTRA = "id"
|
||||||
|
private const val URL_EXTRA = "url"
|
||||||
|
|
||||||
|
fun newInstance(id: String, url: String): ViewThreadFragment {
|
||||||
|
val arguments = Bundle(2)
|
||||||
|
val fragment = ViewThreadFragment()
|
||||||
|
arguments.putString(ID_EXTRA, id)
|
||||||
|
arguments.putString(URL_EXTRA, url)
|
||||||
|
fragment.arguments = arguments
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,427 @@
|
||||||
|
/* Copyright 2022 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.viewthread
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
import at.connyduck.calladapter.networkresult.getOrElse
|
||||||
|
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.BookmarkEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.FavoriteEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.PinEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusDeletedEvent
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.rx3.asFlow
|
||||||
|
import kotlinx.coroutines.rx3.await
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ViewThreadViewModel @Inject constructor(
|
||||||
|
private val api: MastodonApi,
|
||||||
|
private val filterModel: FilterModel,
|
||||||
|
private val timelineCases: TimelineCases,
|
||||||
|
eventHub: EventHub,
|
||||||
|
accountManager: AccountManager
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState: MutableStateFlow<ThreadUiState> = MutableStateFlow(ThreadUiState.Loading)
|
||||||
|
val uiState: Flow<ThreadUiState>
|
||||||
|
get() = _uiState
|
||||||
|
|
||||||
|
private val _errors = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
val errors: Flow<Throwable>
|
||||||
|
get() = _errors
|
||||||
|
|
||||||
|
var isInitialLoad: Boolean = true
|
||||||
|
|
||||||
|
private val alwaysShowSensitiveMedia: Boolean
|
||||||
|
private val alwaysOpenSpoiler: Boolean
|
||||||
|
|
||||||
|
init {
|
||||||
|
val activeAccount = accountManager.activeAccount
|
||||||
|
alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false
|
||||||
|
alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
eventHub.events
|
||||||
|
.asFlow()
|
||||||
|
.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is FavoriteEvent -> handleFavEvent(event)
|
||||||
|
is ReblogEvent -> handleReblogEvent(event)
|
||||||
|
is BookmarkEvent -> handleBookmarkEvent(event)
|
||||||
|
is PinEvent -> handlePinEvent(event)
|
||||||
|
is BlockEvent -> removeAllByAccountId(event.accountId)
|
||||||
|
is StatusComposedEvent -> handleStatusComposedEvent(event)
|
||||||
|
is StatusDeletedEvent -> handleStatusDeletedEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadThread(id: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val contextCall = async { api.statusContext(id) }
|
||||||
|
val statusCall = async { api.statusAsync(id) }
|
||||||
|
|
||||||
|
val contextResult = contextCall.await()
|
||||||
|
val statusResult = statusCall.await()
|
||||||
|
|
||||||
|
val status = statusResult.getOrElse { exception ->
|
||||||
|
_uiState.value = ThreadUiState.Error(exception)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
contextResult.fold({ statusContext ->
|
||||||
|
|
||||||
|
val ancestors = statusContext.ancestors.map { status -> status.toViewData() }.filter()
|
||||||
|
val detailedStatus = status.toViewData(true)
|
||||||
|
val descendants = statusContext.descendants.map { status -> status.toViewData() }.filter()
|
||||||
|
val statuses = ancestors + detailedStatus + descendants
|
||||||
|
|
||||||
|
_uiState.value = ThreadUiState.Success(
|
||||||
|
statuses = statuses,
|
||||||
|
revealButton = statuses.getRevealButtonState(),
|
||||||
|
refreshing = false
|
||||||
|
)
|
||||||
|
}, { throwable ->
|
||||||
|
_errors.emit(throwable)
|
||||||
|
_uiState.value = ThreadUiState.Success(
|
||||||
|
statuses = listOf(status.toViewData(true)),
|
||||||
|
revealButton = RevealButtonState.NO_BUTTON,
|
||||||
|
refreshing = false
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retry(id: String) {
|
||||||
|
_uiState.value = ThreadUiState.Loading
|
||||||
|
loadThread(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh(id: String) {
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
uiState.copy(refreshing = true)
|
||||||
|
}
|
||||||
|
loadThread(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun detailedStatus(): StatusViewData.Concrete? {
|
||||||
|
return (_uiState.value as ThreadUiState.Success?)?.statuses?.find { status ->
|
||||||
|
status.isDetailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reblog(reblog: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
timelineCases.reblog(status.actionableId, reblog).await()
|
||||||
|
} catch (t: Exception) {
|
||||||
|
ifExpected(t) {
|
||||||
|
Log.d(TAG, "Failed to reblog status " + status.actionableId, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun favorite(favorite: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
timelineCases.favourite(status.actionableId, favorite).await()
|
||||||
|
} catch (t: Exception) {
|
||||||
|
ifExpected(t) {
|
||||||
|
Log.d(TAG, "Failed to favourite status " + status.actionableId, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bookmark(bookmark: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
timelineCases.bookmark(status.actionableId, bookmark).await()
|
||||||
|
} catch (t: Exception) {
|
||||||
|
ifExpected(t) {
|
||||||
|
Log.d(TAG, "Failed to favourite status " + status.actionableId, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun voteInPoll(choices: List<Int>, status: StatusViewData.Concrete): Job = viewModelScope.launch {
|
||||||
|
val poll = status.status.actionableStatus.poll ?: run {
|
||||||
|
Log.w(TAG, "No poll on status ${status.id}")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val votedPoll = poll.votedCopy(choices)
|
||||||
|
updateStatus(status.id) { status ->
|
||||||
|
status.copy(poll = votedPoll)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
timelineCases.voteInPoll(status.actionableId, poll.id, choices).await()
|
||||||
|
} catch (t: Exception) {
|
||||||
|
ifExpected(t) {
|
||||||
|
Log.d(TAG, "Failed to vote in poll: " + status.actionableId, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeStatus(statusToRemove: StatusViewData.Concrete) {
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
uiState.copy(
|
||||||
|
statuses = uiState.statuses.filterNot { status -> status == statusToRemove }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeExpanded(expanded: Boolean, status: StatusViewData.Concrete) {
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
val statuses = uiState.statuses.map { viewData ->
|
||||||
|
if (viewData.id == status.id) {
|
||||||
|
viewData.copy(isExpanded = expanded)
|
||||||
|
} else {
|
||||||
|
viewData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiState.copy(
|
||||||
|
statuses = statuses,
|
||||||
|
revealButton = statuses.getRevealButtonState()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) {
|
||||||
|
updateStatusViewData(status.id) { viewData ->
|
||||||
|
viewData.copy(isShowingContent = isShowing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) {
|
||||||
|
updateStatusViewData(status.id) { viewData ->
|
||||||
|
viewData.copy(isCollapsed = isCollapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFavEvent(event: FavoriteEvent) {
|
||||||
|
updateStatus(event.statusId) { status ->
|
||||||
|
status.copy(favourited = event.favourite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReblogEvent(event: ReblogEvent) {
|
||||||
|
updateStatus(event.statusId) { status ->
|
||||||
|
status.copy(reblogged = event.reblog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBookmarkEvent(event: BookmarkEvent) {
|
||||||
|
updateStatus(event.statusId) { status ->
|
||||||
|
status.copy(bookmarked = event.bookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePinEvent(event: PinEvent) {
|
||||||
|
updateStatus(event.statusId) { status ->
|
||||||
|
status.copy(pinned = event.pinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAllByAccountId(accountId: String) {
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
uiState.copy(
|
||||||
|
statuses = uiState.statuses.filter { viewData ->
|
||||||
|
viewData.status.account.id == accountId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleStatusComposedEvent(event: StatusComposedEvent) {
|
||||||
|
val eventStatus = event.status
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
val statuses = uiState.statuses
|
||||||
|
val detailedIndex = statuses.indexOfFirst { status -> status.isDetailed }
|
||||||
|
val repliedIndex = statuses.indexOfFirst { status -> eventStatus.inReplyToId == status.id }
|
||||||
|
if (detailedIndex != -1 && repliedIndex >= detailedIndex) {
|
||||||
|
// there is a new reply to the detailed status or below -> display it
|
||||||
|
val newStatuses = statuses.subList(0, repliedIndex + 1) +
|
||||||
|
eventStatus.toViewData() +
|
||||||
|
statuses.subList(repliedIndex + 1, statuses.size)
|
||||||
|
uiState.copy(statuses = newStatuses)
|
||||||
|
} else {
|
||||||
|
uiState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleStatusDeletedEvent(event: StatusDeletedEvent) {
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
uiState.copy(
|
||||||
|
statuses = uiState.statuses.filter { status ->
|
||||||
|
status.id != event.statusId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleRevealButton() {
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
when (uiState.revealButton) {
|
||||||
|
RevealButtonState.HIDE -> uiState.copy(
|
||||||
|
statuses = uiState.statuses.map { viewData ->
|
||||||
|
viewData.copy(isExpanded = false)
|
||||||
|
},
|
||||||
|
revealButton = RevealButtonState.REVEAL
|
||||||
|
)
|
||||||
|
RevealButtonState.REVEAL -> uiState.copy(
|
||||||
|
statuses = uiState.statuses.map { viewData ->
|
||||||
|
viewData.copy(isExpanded = true)
|
||||||
|
},
|
||||||
|
revealButton = RevealButtonState.HIDE
|
||||||
|
)
|
||||||
|
else -> uiState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<StatusViewData.Concrete>.getRevealButtonState(): RevealButtonState {
|
||||||
|
val hasWarnings = any { viewData ->
|
||||||
|
viewData.status.spoilerText.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (hasWarnings) {
|
||||||
|
val allExpanded = none { viewData ->
|
||||||
|
!viewData.isExpanded
|
||||||
|
}
|
||||||
|
if (allExpanded) {
|
||||||
|
RevealButtonState.HIDE
|
||||||
|
} else {
|
||||||
|
RevealButtonState.REVEAL
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RevealButtonState.NO_BUTTON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFilters() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val filters = try {
|
||||||
|
api.getFilters().await()
|
||||||
|
} catch (t: Exception) {
|
||||||
|
Log.w(TAG, "Failed to fetch filters", t)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
filterModel.initWithFilters(
|
||||||
|
filters.filter { filter ->
|
||||||
|
filter.context.contains(Filter.THREAD)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
val statuses = uiState.statuses.filter()
|
||||||
|
uiState.copy(
|
||||||
|
statuses = statuses,
|
||||||
|
revealButton = statuses.getRevealButtonState()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<StatusViewData.Concrete>.filter(): List<StatusViewData.Concrete> {
|
||||||
|
return filter { status ->
|
||||||
|
status.isDetailed || !filterModel.shouldFilterStatus(status.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Status.toViewData(detailed: Boolean = false): StatusViewData.Concrete {
|
||||||
|
val oldStatus = (_uiState.value as? ThreadUiState.Success)?.statuses?.find { it.id == this.id }
|
||||||
|
return toViewData(
|
||||||
|
isShowingContent = oldStatus?.isShowingContent ?: (alwaysShowSensitiveMedia || !actionableStatus.sensitive),
|
||||||
|
isExpanded = oldStatus?.isExpanded ?: alwaysOpenSpoiler,
|
||||||
|
isCollapsed = oldStatus?.isCollapsed ?: !detailed,
|
||||||
|
isDetailed = oldStatus?.isDetailed ?: detailed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun updateSuccess(updater: (ThreadUiState.Success) -> ThreadUiState.Success) {
|
||||||
|
_uiState.update { uiState ->
|
||||||
|
if (uiState is ThreadUiState.Success) {
|
||||||
|
updater(uiState)
|
||||||
|
} else {
|
||||||
|
uiState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStatusViewData(statusId: String, updater: (StatusViewData.Concrete) -> StatusViewData.Concrete) {
|
||||||
|
updateSuccess { uiState ->
|
||||||
|
uiState.copy(
|
||||||
|
statuses = uiState.statuses.map { viewData ->
|
||||||
|
if (viewData.id == statusId) {
|
||||||
|
updater(viewData)
|
||||||
|
} else {
|
||||||
|
viewData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStatus(statusId: String, updater: (Status) -> Status) {
|
||||||
|
updateStatusViewData(statusId) { viewData ->
|
||||||
|
viewData.copy(
|
||||||
|
status = updater(viewData.status)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ViewThreadViewModel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ThreadUiState {
|
||||||
|
object Loading : ThreadUiState
|
||||||
|
class Error(val throwable: Throwable) : ThreadUiState
|
||||||
|
data class Success(
|
||||||
|
val statuses: List<StatusViewData.Concrete>,
|
||||||
|
val revealButton: RevealButtonState,
|
||||||
|
val refreshing: Boolean
|
||||||
|
) : ThreadUiState
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class RevealButtonState {
|
||||||
|
NO_BUTTON, REVEAL, HIDE
|
||||||
|
}
|
|
@ -59,6 +59,7 @@ data class AccountEntity(
|
||||||
var notificationLight: Boolean = true,
|
var notificationLight: Boolean = true,
|
||||||
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
|
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
|
||||||
var defaultMediaSensitivity: Boolean = false,
|
var defaultMediaSensitivity: Boolean = false,
|
||||||
|
var defaultPostLanguage: String = "",
|
||||||
var alwaysShowSensitiveMedia: Boolean = false,
|
var alwaysShowSensitiveMedia: Boolean = false,
|
||||||
var alwaysOpenSpoiler: Boolean = false,
|
var alwaysOpenSpoiler: Boolean = false,
|
||||||
var mediaPreviewEnabled: Boolean = true,
|
var mediaPreviewEnabled: Boolean = true,
|
||||||
|
|
|
@ -15,9 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.db
|
package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -151,6 +154,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
it.displayName = account.name
|
it.displayName = account.name
|
||||||
it.profilePictureUrl = account.avatar
|
it.profilePictureUrl = account.avatar
|
||||||
it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC
|
it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC
|
||||||
|
it.defaultPostLanguage = account.source?.language ?: ""
|
||||||
it.defaultMediaSensitivity = account.source?.sensitive ?: false
|
it.defaultMediaSensitivity = account.source?.sensitive ?: false
|
||||||
it.emojis = account.emojis ?: emptyList()
|
it.emojis = account.emojis ?: emptyList()
|
||||||
|
|
||||||
|
@ -225,4 +229,18 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
identifier == it.identifier
|
identifier == it.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the name of the currently-selected account should be displayed in UIs
|
||||||
|
*/
|
||||||
|
fun shouldDisplaySelfUsername(context: Context): Boolean {
|
||||||
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val showUsernamePreference = sharedPreferences.getString(PrefKeys.SHOW_SELF_USERNAME, "disambiguate")
|
||||||
|
if (showUsernamePreference == "always")
|
||||||
|
return true
|
||||||
|
if (showUsernamePreference == "never")
|
||||||
|
return false
|
||||||
|
|
||||||
|
return accounts.size > 1 // "disambiguate"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
||||||
*/
|
*/
|
||||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 39)
|
}, version = 43)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
@ -581,4 +581,40 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT");
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_39_40 = new Migration(39, 40) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `videoSizeLimit` INTEGER");
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `imageSizeLimit` INTEGER");
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `imageMatrixLimit` INTEGER");
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxMediaAttachments` INTEGER");
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFields` INTEGER");
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldNameLength` INTEGER");
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldValueLength` INTEGER");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_40_41 = new Migration(40, 41) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `scheduledAt` TEXT");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_41_42 = new Migration(41, 42) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `language` TEXT");
|
||||||
|
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `language` TEXT");
|
||||||
|
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_language` TEXT");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_42_43 = new Migration(42, 43) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostLanguage` TEXT NOT NULL DEFAULT ''");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
@ -38,7 +39,9 @@ data class DraftEntity(
|
||||||
val visibility: Status.Visibility,
|
val visibility: Status.Visibility,
|
||||||
val attachments: List<DraftAttachment>,
|
val attachments: List<DraftAttachment>,
|
||||||
val poll: NewPoll?,
|
val poll: NewPoll?,
|
||||||
val failedToSend: Boolean
|
val failedToSend: Boolean,
|
||||||
|
val scheduledAt: String?,
|
||||||
|
val language: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,6 +53,7 @@ data class DraftEntity(
|
||||||
data class DraftAttachment(
|
data class DraftAttachment(
|
||||||
@SerializedName(value = "uriString", alternate = ["e", "i"]) val uriString: String,
|
@SerializedName(value = "uriString", alternate = ["e", "i"]) val uriString: String,
|
||||||
@SerializedName(value = "description", alternate = ["f", "j"]) val description: String?,
|
@SerializedName(value = "description", alternate = ["f", "j"]) val description: String?,
|
||||||
|
@SerializedName(value = "focus") val focus: Attachment.Focus?,
|
||||||
@SerializedName(value = "type", alternate = ["g", "k"]) val type: Type
|
@SerializedName(value = "type", alternate = ["g", "k"]) val type: Type
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
val uri: Uri
|
val uri: Uri
|
||||||
|
|
|
@ -20,15 +20,37 @@ import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.RewriteQueriesToDropUnusedColumns
|
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface InstanceDao {
|
interface InstanceDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class)
|
@Insert(onConflict = OnConflictStrategy.IGNORE, entity = InstanceEntity::class)
|
||||||
suspend fun insertOrReplace(instance: InstanceInfoEntity)
|
suspend fun insertOrIgnore(instance: InstanceInfoEntity): Long
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class)
|
@Update(onConflict = OnConflictStrategy.IGNORE, entity = InstanceEntity::class)
|
||||||
suspend fun insertOrReplace(emojis: EmojisEntity)
|
suspend fun updateOrIgnore(instance: InstanceInfoEntity)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun upsert(instance: InstanceInfoEntity) {
|
||||||
|
if (insertOrIgnore(instance) == -1L) {
|
||||||
|
updateOrIgnore(instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE, entity = InstanceEntity::class)
|
||||||
|
suspend fun insertOrIgnore(emojis: EmojisEntity): Long
|
||||||
|
|
||||||
|
@Update(onConflict = OnConflictStrategy.IGNORE, entity = InstanceEntity::class)
|
||||||
|
suspend fun updateOrIgnore(emojis: EmojisEntity)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun upsert(emojis: EmojisEntity) {
|
||||||
|
if (insertOrIgnore(emojis) == -1L) {
|
||||||
|
updateOrIgnore(emojis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
||||||
|
|
|
@ -31,7 +31,14 @@ data class InstanceEntity(
|
||||||
val minPollDuration: Int?,
|
val minPollDuration: Int?,
|
||||||
val maxPollDuration: Int?,
|
val maxPollDuration: Int?,
|
||||||
val charactersReservedPerUrl: Int?,
|
val charactersReservedPerUrl: Int?,
|
||||||
val version: String?
|
val version: String?,
|
||||||
|
val videoSizeLimit: Int?,
|
||||||
|
val imageSizeLimit: Int?,
|
||||||
|
val imageMatrixLimit: Int?,
|
||||||
|
val maxMediaAttachments: Int?,
|
||||||
|
val maxFields: Int?,
|
||||||
|
val maxFieldNameLength: Int?,
|
||||||
|
val maxFieldValueLength: Int?
|
||||||
)
|
)
|
||||||
|
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
@ -48,5 +55,12 @@ data class InstanceInfoEntity(
|
||||||
val minPollDuration: Int?,
|
val minPollDuration: Int?,
|
||||||
val maxPollDuration: Int?,
|
val maxPollDuration: Int?,
|
||||||
val charactersReservedPerUrl: Int?,
|
val charactersReservedPerUrl: Int?,
|
||||||
val version: String?
|
val version: String?,
|
||||||
|
val videoSizeLimit: Int?,
|
||||||
|
val imageSizeLimit: Int?,
|
||||||
|
val imageMatrixLimit: Int?,
|
||||||
|
val maxMediaAttachments: Int?,
|
||||||
|
val maxFields: Int?,
|
||||||
|
val maxFieldNameLength: Int?,
|
||||||
|
val maxFieldValueLength: Int?
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,7 +36,7 @@ SELECT s.serverId, s.url, s.timelineUserId,
|
||||||
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
|
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
|
||||||
s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
||||||
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
|
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
|
||||||
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
|
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, s.language,
|
||||||
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
|
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
|
||||||
a.localUsername as 'a_localUsername', a.username as 'a_username',
|
a.localUsername as 'a_localUsername', a.username as 'a_username',
|
||||||
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
|
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
|
||||||
|
|
|
@ -81,6 +81,7 @@ data class TimelineStatusEntity(
|
||||||
val contentShowing: Boolean,
|
val contentShowing: Boolean,
|
||||||
val pinned: Boolean,
|
val pinned: Boolean,
|
||||||
val card: String?,
|
val card: String?,
|
||||||
|
val language: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
|
|
|
@ -27,7 +27,6 @@ import com.keylesspalace.tusky.SplashActivity
|
||||||
import com.keylesspalace.tusky.StatusListActivity
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
import com.keylesspalace.tusky.ViewThreadActivity
|
|
||||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
@ -39,6 +38,7 @@ import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
|
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ abstract class ActivitiesModule {
|
||||||
abstract fun contributesStatusListActivity(): StatusListActivity
|
abstract fun contributesStatusListActivity(): StatusListActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesSearchAvtivity(): SearchActivity
|
abstract fun contributesSearchActivity(): SearchActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesAboutActivity(): AboutActivity
|
abstract fun contributesAboutActivity(): AboutActivity
|
||||||
|
|
|
@ -65,7 +65,8 @@ class AppModule {
|
||||||
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
||||||
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
||||||
AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38,
|
AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38,
|
||||||
AppDatabase.MIGRATION_38_39
|
AppDatabase.MIGRATION_38_39, AppDatabase.MIGRATION_39_40, AppDatabase.MIGRATION_40_41,
|
||||||
|
AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43,
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|