Merge tag 'v17.0' into develop
This commit is contained in:
commit
ea95fc2f4b
243 changed files with 8456 additions and 5710 deletions
|
@ -14,7 +14,7 @@ It is derived from [Tusky](https://tusky.app) and has been modified to reflect C
|
||||||
- Material Design
|
- Material Design
|
||||||
- Multi-Account support
|
- Multi-Account support
|
||||||
- Respect device preferences for light/dark theming
|
- Respect device preferences for light/dark theming
|
||||||
- Drafts - compose toots and save them for later
|
- Drafts - compose posts and save them for later
|
||||||
- Choose between different emoji styles
|
- Choose between different emoji styles
|
||||||
- Optimized for all screen sizes
|
- Optimized for all screen sizes
|
||||||
- Completely open-source - no non-free dependencies like Google services
|
- Completely open-source - no non-free dependencies like Google services
|
||||||
|
@ -22,7 +22,7 @@ It is derived from [Tusky](https://tusky.app) and has been modified to reflect C
|
||||||
### Support
|
### Support
|
||||||
|
|
||||||
Check out Tusky's [FAQs](https://github.com/tuskyapp/faq), your question may already be answered.
|
Check out Tusky's [FAQs](https://github.com/tuskyapp/faq), your question may already be answered.
|
||||||
If you have any bug reports, feature requests or questions please open an issue or send us a toot at [chinwagnews@chinwag.org](https://social.chinwag.org/@ChinwagNews)!
|
If you have any bug reports, feature requests or questions please open an issue or send us a message at [chinwagnews@chinwag.org](https://social.chinwag.org/@ChinwagNews)!
|
||||||
|
|
||||||
For translating Tusky into your language, visit https://weblate.tusky.app/
|
For translating Tusky into your language, visit https://weblate.tusky.app/
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,13 @@ def getGitSha = {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 31
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId APP_ID
|
applicationId APP_ID
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 30
|
targetSdkVersion 31
|
||||||
versionCode 87
|
versionCode 87
|
||||||
versionName "16.0-CW1"
|
versionName "17.0-CW1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
@ -90,12 +90,12 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.coroutinesVersion = "1.6.0"
|
ext.coroutinesVersion = "1.6.0"
|
||||||
ext.lifecycleVersion = "2.3.1"
|
ext.lifecycleVersion = "2.4.1"
|
||||||
ext.roomVersion = '2.3.0'
|
ext.roomVersion = '2.4.2'
|
||||||
ext.retrofitVersion = '2.9.0'
|
ext.retrofitVersion = '2.9.0'
|
||||||
ext.okhttpVersion = '4.9.3'
|
ext.okhttpVersion = '4.9.3'
|
||||||
ext.glideVersion = '4.12.0'
|
ext.glideVersion = '4.13.1'
|
||||||
ext.daggerVersion = '2.40.5'
|
ext.daggerVersion = '2.41'
|
||||||
ext.materialdrawerVersion = '8.4.5'
|
ext.materialdrawerVersion = '8.4.5'
|
||||||
|
|
||||||
// if libraries are changed here, they should also be changed in LicenseActivity
|
// if libraries are changed here, they should also be changed in LicenseActivity
|
||||||
|
@ -105,33 +105,35 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
||||||
|
|
||||||
implementation "androidx.core:core-ktx:1.5.0"
|
implementation "androidx.core:core-ktx:1.7.0"
|
||||||
implementation "androidx.appcompat:appcompat:1.3.0"
|
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||||
implementation "androidx.browser:browser:1.3.0"
|
implementation "androidx.browser:browser:1.4.0"
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
implementation "androidx.cardview:cardview:1.0.0"
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
|
||||||
implementation "androidx.emoji:emoji:1.1.0"
|
implementation "androidx.emoji:emoji:1.1.0"
|
||||||
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||||
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
|
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
|
||||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||||
implementation "androidx.work:work-runtime:2.5.0"
|
implementation "androidx.work:work-runtime:2.7.1"
|
||||||
implementation "androidx.room:room-ktx:$roomVersion"
|
implementation "androidx.room:room-ktx:$roomVersion"
|
||||||
|
implementation "androidx.room:room-paging:$roomVersion"
|
||||||
implementation "androidx.room:room-rxjava3:$roomVersion"
|
implementation "androidx.room:room-rxjava3:$roomVersion"
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
kapt "androidx.room:room-compiler:$roomVersion"
|
||||||
|
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.4.0"
|
implementation "com.google.android.material:material:1.5.0"
|
||||||
|
|
||||||
implementation "com.google.code.gson:gson:2.8.9"
|
implementation "com.google.code.gson:gson:2.9.0"
|
||||||
|
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||||
|
@ -146,14 +148,14 @@ dependencies {
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||||
|
|
||||||
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.17.0"
|
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.20.0"
|
||||||
|
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.0.12"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.3"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||||
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
|
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
|
||||||
|
|
||||||
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.0.0"
|
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"
|
||||||
implementation "com.uber.autodispose2:autodispose:2.0.0"
|
implementation "com.uber.autodispose2:autodispose:2.1.1"
|
||||||
|
|
||||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||||
|
@ -169,7 +171,7 @@ dependencies {
|
||||||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||||
|
|
||||||
implementation "com.github.CanHub:Android-Image-Cropper:3.1.0"
|
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
|
||||||
|
|
||||||
implementation "de.c1710:filemojicompat:1.0.18"
|
implementation "de.c1710:filemojicompat:1.0.18"
|
||||||
|
|
||||||
|
|
789
app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json
Normal file
789
app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json
Normal file
|
@ -0,0 +1,789 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 29,
|
||||||
|
"identityHash": "62c289344334da2db091ad4ba0a49c6a",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "InstanceEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "instance",
|
||||||
|
"columnName": "instance",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojiList",
|
||||||
|
"columnName": "emojiList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTootCharacters",
|
||||||
|
"columnName": "maximumTootCharacters",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptions",
|
||||||
|
"columnName": "maxPollOptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptionLength",
|
||||||
|
"columnName": "maxPollOptionLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `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, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.bookmarked",
|
||||||
|
"columnName": "s_bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.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.collapsible",
|
||||||
|
"columnName": "s_collapsible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.muted",
|
||||||
|
"columnName": "s_muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '62c289344334da2db091ad4ba0a49c6a')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
807
app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json
Normal file
807
app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json
Normal file
|
@ -0,0 +1,807 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 30,
|
||||||
|
"identityHash": "a75615171612bdfc9e3d4201ebf6071a",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "InstanceEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "instance",
|
||||||
|
"columnName": "instance",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojiList",
|
||||||
|
"columnName": "emojiList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTootCharacters",
|
||||||
|
"columnName": "maximumTootCharacters",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptions",
|
||||||
|
"columnName": "maxPollOptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptionLength",
|
||||||
|
"columnName": "maxPollOptionLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `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, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.bookmarked",
|
||||||
|
"columnName": "s_bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.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.collapsible",
|
||||||
|
"columnName": "s_collapsible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.muted",
|
||||||
|
"columnName": "s_muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a75615171612bdfc9e3d4201ebf6071a')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
809
app/schemas/com.keylesspalace.tusky.db.AppDatabase/31.json
Normal file
809
app/schemas/com.keylesspalace.tusky.db.AppDatabase/31.json
Normal file
|
@ -0,0 +1,809 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 31,
|
||||||
|
"identityHash": "a75615171612bdfc9e3d4201ebf6071a",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"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, 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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `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, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.bookmarked",
|
||||||
|
"columnName": "s_bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.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.collapsible",
|
||||||
|
"columnName": "s_collapsible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.muted",
|
||||||
|
"columnName": "s_muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a75615171612bdfc9e3d4201ebf6071a')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
6
app/src/green/res/values/flavor-colors.xml
Normal file
6
app/src/green/res/values/flavor-colors.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<color name="notification_color">#19A341</color>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -20,9 +20,12 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/TuskyTheme"
|
android:theme="@style/TuskyTheme"
|
||||||
android:usesCleartextTraffic="false">
|
android:usesCleartextTraffic="false">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SplashActivity"
|
android:name=".SplashActivity"
|
||||||
android:theme="@style/SplashTheme">
|
android:theme="@style/SplashTheme"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
@ -35,22 +38,15 @@
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".LoginActivity"
|
android:name=".components.login.LoginActivity"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="${applicationId}"
|
|
||||||
android:scheme="@string/oauth_scheme" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".components.login.LoginWebViewActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize">
|
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
|
@ -106,7 +102,6 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".ViewThreadActivity"
|
android:name=".ViewThreadActivity"
|
||||||
android:configChanges="orientation|screenSize" />
|
android:configChanges="orientation|screenSize" />
|
||||||
<activity android:name=".ViewTagActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ViewMediaActivity"
|
android:name=".ViewMediaActivity"
|
||||||
android:theme="@style/TuskyBaseTheme" />
|
android:theme="@style/TuskyBaseTheme" />
|
||||||
|
@ -124,7 +119,8 @@
|
||||||
android:theme="@style/Base.Theme.AppCompat" />
|
android:theme="@style/Base.Theme.AppCompat" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.search.SearchActivity"
|
android:name=".components.search.SearchActivity"
|
||||||
android:launchMode="singleTop">
|
android:launchMode="singleTop"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -134,18 +130,18 @@
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ListsActivity" />
|
<activity android:name=".ListsActivity" />
|
||||||
<activity android:name=".ModalTimelineActivity" />
|
|
||||||
<activity android:name=".LicenseActivity" />
|
<activity android:name=".LicenseActivity" />
|
||||||
<activity android:name=".FiltersActivity" />
|
<activity android:name=".FiltersActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.report.ReportActivity"
|
android:name=".components.report.ReportActivity"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||||
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
<activity android:name=".components.scheduled.ScheduledStatusActivity" />
|
||||||
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||||
<activity android:name=".components.drafts.DraftsActivity" />
|
<activity android:name=".components.drafts.DraftsActivity" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
<receiver android:name=".receiver.NotificationClearBroadcastReceiver"
|
||||||
|
android:exported="false" />
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receiver.SendStatusBroadcastReceiver"
|
android:name=".receiver.SendStatusBroadcastReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
@ -154,15 +150,17 @@
|
||||||
<service
|
<service
|
||||||
android:name=".service.TuskyTileService"
|
android:name=".service.TuskyTileService"
|
||||||
android:icon="@drawable/ic_chinwag_logo_simple"
|
android:icon="@drawable/ic_chinwag_logo_simple"
|
||||||
android:label="Compose Toot"
|
android:label="@string/tusky_compose_post_quicksetting_label"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
|
android:exported="true"
|
||||||
tools:targetApi="24">
|
tools:targetApi="24">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".service.SendTootService" />
|
<service android:name=".service.SendStatusService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
@ -176,10 +174,16 @@
|
||||||
|
|
||||||
<!-- disable automatic WorkManager initialization -->
|
<!-- disable automatic WorkManager initialization -->
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.workmanager-init"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
tools:node="remove" />
|
tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 70 KiB |
BIN
app/src/main/ic_launcher.png
Normal file
BIN
app/src/main/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
|
@ -1,488 +1,101 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
<svg version="1.2" width="180mm" height="180mm" viewBox="0 0 18000 18000" preserveAspectRatio="xMidYMid" fill-rule="evenodd" stroke-width="28.222" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" xmlns:ooo="http://xml.openoffice.org/svg/export" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:presentation="http://sun.com/xmlns/staroffice/presentation" xmlns:smil="http://www.w3.org/2001/SMIL20/" xmlns:anim="urn:oasis:names:tc:opendocument:xmlns:animation:1.0" xml:space="preserve">
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
<defs class="ClipPathGroup">
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
<clipPath id="presentation_clip_path" clipPathUnits="userSpaceOnUse">
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
<rect x="0" y="0" width="18000" height="18000"/>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</clipPath>
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
<clipPath id="presentation_clip_path_shrink" clipPathUnits="userSpaceOnUse">
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
<rect x="18" y="18" width="17964" height="17964"/>
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
</clipPath>
|
||||||
id="svg181"
|
</defs>
|
||||||
viewBox="0 0 135.46666 135.46666"
|
<defs class="TextShapeIndex">
|
||||||
version="1.1"
|
<g ooo:slide="id1" ooo:id-list="id3 id4 id5 id6 id7"/>
|
||||||
height="512"
|
</defs>
|
||||||
width="512"
|
<defs class="EmbeddedBulletChars">
|
||||||
sodipodi:docname="ic_launcher2.svg"
|
<g id="bullet-char-template-57356" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
inkscape:version="0.92.1 r15371">
|
<path d="M 580,1141 L 1163,571 580,0 -4,571 580,1141 Z"/>
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1147"
|
|
||||||
id="namedview4579"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:measure-start="140.54,184.424"
|
|
||||||
inkscape:measure-end="499.461,-175.02"
|
|
||||||
inkscape:zoom="1.9140625"
|
|
||||||
inkscape:cx="253.80846"
|
|
||||||
inkscape:cy="248.95369"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="g157" />
|
|
||||||
<metadata
|
|
||||||
id="metadata185">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs147">
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient4780">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#2588d0;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop4776" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#1967a3;stop-opacity:1"
|
|
||||||
offset="1"
|
|
||||||
id="stop4778" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="c">
|
|
||||||
<stop
|
|
||||||
id="stop2"
|
|
||||||
offset="0" />
|
|
||||||
<stop
|
|
||||||
id="stop4"
|
|
||||||
offset="1"
|
|
||||||
stop-opacity="0" />
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath
|
|
||||||
id="g">
|
|
||||||
<circle
|
|
||||||
id="circle7"
|
|
||||||
fill="#2588d0"
|
|
||||||
r="112.52"
|
|
||||||
cy="125"
|
|
||||||
cx="125" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
id="e">
|
|
||||||
<g
|
|
||||||
id="g132"
|
|
||||||
opacity=".05"
|
|
||||||
fill="url(#a)"
|
|
||||||
clip-path="url(#g)"
|
|
||||||
transform="matrix(.26458 0 0 .26458 7.7244 -29.561)">
|
|
||||||
<g
|
|
||||||
id="g128"
|
|
||||||
stroke-width=".5">
|
|
||||||
<path
|
|
||||||
id="path10"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m98.535 73.473c-1.8371-0.09054-2.2298 0.10241-1.635 0.69722l70.711 70.711c-0.59481-0.59481-0.2021-0.78775 1.635-0.69722z" />
|
|
||||||
<path
|
|
||||||
id="path12"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m96.9 74.17c0.70818 0.70818 2.8162 1.986 5.553 4.0332l70.711 70.711c-2.7368-2.0472-4.8448-3.3251-5.553-4.0332z" />
|
|
||||||
<path
|
|
||||||
id="path14"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m102.45 78.203c2.4807 1.8557 4.9237 4.0232 7.1498 6.2492l70.711 70.711c-2.226-2.226-4.6691-4.3935-7.1498-6.2492z" />
|
|
||||||
<path
|
|
||||||
id="path16"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m109.6 84.452c3.2825 3.2825 6.093 6.6922 7.8561 9.4168l70.711 70.711c-1.7631-2.7246-4.5736-6.1343-7.8561-9.4168z" />
|
|
||||||
<path
|
|
||||||
id="path18"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m117.46 93.869c2.9912 4.6225 5.6692 8.3037 12.262 28.254l70.711 70.711c-6.5925-19.95-9.2705-23.631-12.262-28.254z" />
|
|
||||||
<path
|
|
||||||
id="path20"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m129.72 122.12c2.4794 7.5032 5.7538 14.14 6.6211 16.113l70.711 70.711c-0.86733-1.9736-4.1417-8.6101-6.6211-16.113z" />
|
|
||||||
<path
|
|
||||||
id="path22"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m136.34 138.24 1.3476 3.0059 70.711 70.711-1.3476-3.0059z" />
|
|
||||||
<path
|
|
||||||
id="path24"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m137.69 141.24c-0.0653 0.0814-0.11927 0.14322-0.18554 0.22656l70.711 70.711c0.0663-0.0833 0.12024-0.14516 0.18554-0.22656z" />
|
|
||||||
<path
|
|
||||||
id="path26"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m137.5 141.47-3.4395 3.0098 70.711 70.711 3.4395-3.0098z" />
|
|
||||||
<path
|
|
||||||
id="path28"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m134.06 144.48c-5.5481 5.7637-16.033 11.691-24.285 13.729l70.711 70.711c8.2524-2.0372 18.737-7.9648 24.285-13.729z" />
|
|
||||||
<path
|
|
||||||
id="path30"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m109.78 158.21c-6.3614 1.5706-21.301 1.1651-27.809 0.0137l70.711 70.711c6.5077 1.1514 21.447 1.5569 27.809-0.0137z" />
|
|
||||||
<path
|
|
||||||
id="path32"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m81.971 158.22c-7.8527-1.3894-14.205-5.074-19.134-10.002l70.711 70.711c4.9282 4.9282 11.281 8.6128 19.134 10.002z" />
|
|
||||||
<path
|
|
||||||
id="path34"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m62.837 148.22c-6.1069-6.1069-10.026-14.123-11.902-22.049l70.711 70.711c1.8752 7.9251 5.7946 15.942 11.902 22.049z" />
|
|
||||||
<path
|
|
||||||
id="path36"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m50.936 126.17c-1.5987-9.5426 0.52116-16.959 1.6895-21.707l70.711 70.711c-1.1683 4.7477-3.2882 12.164-1.6894 21.707z" />
|
|
||||||
<path
|
|
||||||
id="path38"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m52.625 104.46c2.6637-10.825 9.7356-20.465 18.479-26.402l70.711 70.711c-8.7429 5.9372-15.815 15.578-18.479 26.402z" />
|
|
||||||
<path
|
|
||||||
id="path40"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m71.104 78.061c3.0009-2.0378 4.6792-2.7066 4.4597-2.9261l70.711 70.711c0.21947 0.21947-1.4588 0.88834-4.4597 2.9261z" />
|
|
||||||
<path
|
|
||||||
id="path42"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m75.563 75.134c-0.11026-0.11026-0.69947-0.10709-1.8406-0.10709l70.711 70.711c1.1411 0 1.7303-3e-3 1.8406 0.10709z" />
|
|
||||||
<path
|
|
||||||
id="path44"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m73.723 75.027c-3.9668 0-10.077 2.6389-14.275 6.0215l70.711 70.711c4.1987-3.3826 10.309-6.0215 14.275-6.0215z" />
|
|
||||||
<path
|
|
||||||
id="path46"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m59.447 81.049c-8.0199 6.461-14.768 15.598-18.961 26.336l70.711 70.711c4.1931-10.738 10.941-19.875 18.961-26.336z" />
|
|
||||||
<path
|
|
||||||
id="path48"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m40.486 107.38c-2.2433 6.6848-2.2747 5.5109-2.418 15.162l70.711 70.711c0.14324-9.6512 0.17463-8.4773 2.418-15.162z" />
|
|
||||||
<path
|
|
||||||
id="path50"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m38.068 122.55c0.37633 6.9829 1.0389 9.3071 3.0723 15.406l70.711 70.711c-2.0333-6.0991-2.6959-8.4234-3.0723-15.406z" />
|
|
||||||
<path
|
|
||||||
id="path52"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m41.141 137.95c2.3472 7.0404 6.2768 13.161 11.343 18.227l70.711 70.711c-5.0663-5.0663-8.9959-11.187-11.343-18.227z" />
|
|
||||||
<path
|
|
||||||
id="path54"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m52.484 156.18c2.9744 2.9744 6.3406 5.5855 10.008 7.806l70.711 70.711c-3.6678-2.2205-7.034-4.8316-10.008-7.806z" />
|
|
||||||
<path
|
|
||||||
id="path56"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m62.492 163.99c4.9985 3.0261 15.324 6.7023 22.098 7.8027l70.711 70.711c-6.7734-1.1005-17.099-4.7766-22.098-7.8027z" />
|
|
||||||
<path
|
|
||||||
id="path58"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m84.59 171.79c1.8904 0.30967 4.992 0.90865 7.8848 1.1875l70.711 70.711c-2.8928-0.27885-5.9944-0.87783-7.8848-1.1875z" />
|
|
||||||
<path
|
|
||||||
id="path60"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m92.475 172.98c12.195 0.94625 15.055-0.32666 24.506-1.8418l70.711 70.711c-9.4508 1.5151-12.311 2.788-24.506 1.8418z" />
|
|
||||||
<path
|
|
||||||
id="path62"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m116.98 171.13c8.5906-2.4983 16.678-7.275 22.934-12.367l70.711 70.711c-6.2561 5.0922-14.343 9.8689-22.934 12.367z" />
|
|
||||||
<path
|
|
||||||
id="path64"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m139.91 158.77 5.7871-4.7168 70.711 70.711-5.7871 4.7168z" />
|
|
||||||
<path
|
|
||||||
id="path66"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m145.7 154.05c0.0891 0.11275 0.1968 0.26762 0.28321 0.375l70.711 70.711c-0.0864-0.10738-0.19411-0.26225-0.28321-0.375z" />
|
|
||||||
<path
|
|
||||||
id="path68"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m145.98 154.43 1.9648 2.0644 70.711 70.711-1.9648-2.0644z" />
|
|
||||||
<path
|
|
||||||
id="path70"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m147.95 156.49c0.29948 0.31455 0.59902 0.62179 0.89884 0.92161l70.711 70.711c-0.29982-0.29982-0.59936-0.60705-0.89884-0.9216z" />
|
|
||||||
<path
|
|
||||||
id="path72"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m148.85 157.41c5.8411 5.8411 11.787 8.8672 19.377 8.303l70.711 70.711c-7.5898 0.56423-13.535-2.4619-19.377-8.303z" />
|
|
||||||
<path
|
|
||||||
id="path74"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m168.22 165.71c7.5551-0.56163 13.041-2.6761 17.49-6.7422l70.711 70.711c-4.4488 4.066-9.9351 6.1806-17.49 6.7422z" />
|
|
||||||
<path
|
|
||||||
id="path76"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m185.71 158.97c4.5132-4.1248 5.5354-6.236 7.6699-12.512l70.711 70.711c-2.1345 6.2758-3.1567 8.3869-7.6699 12.512z" />
|
|
||||||
<path
|
|
||||||
id="path78"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m193.38 146.46c2.9552-8.6889 4.3184-16.193 3.9922-29.721l70.711 70.711c0.32618 13.527-1.0369 21.032-3.9922 29.721z" />
|
|
||||||
<path
|
|
||||||
id="path80"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m197.38 116.74c-0.33294-13.818-1.6567-19.14-5.4434-23.453l70.711 70.711c3.7867 4.3128 5.1104 9.6355 5.4434 23.453z" />
|
|
||||||
<path
|
|
||||||
id="path82"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m191.93 93.287c-0.0959-0.10288-0.19218-0.20269-0.28888-0.2994l70.711 70.711c0.0967 0.0967 0.19301 0.19652 0.28888 0.2994z" />
|
|
||||||
<path
|
|
||||||
id="path84"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m191.64 92.988c-3.0781-3.0781-6.553-3.0044-8.5724 1.8287l70.711 70.711c2.0194-4.8331 5.4943-4.9068 8.5724-1.8287z" />
|
|
||||||
<path
|
|
||||||
id="path86"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m183.07 94.816c-0.79737 1.9089-0.78178 2.91 0.11132 6.918l70.711 70.711c-0.8931-4.008-0.90869-5.0091-0.11132-6.918z" />
|
|
||||||
<path
|
|
||||||
id="path88"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m183.18 101.73c1.6289 7.3102 1.7837 25.828 0.45313 32.072l70.711 70.711c1.3306-6.2445 1.1758-24.762-0.45313-32.072z" />
|
|
||||||
<path
|
|
||||||
id="path90"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m183.64 133.81c-2.7904 13.095-11.463 17.859-22.207 12.49l70.711 70.711c10.744 5.3688 19.417 0.60451 22.207-12.49z" />
|
|
||||||
<path
|
|
||||||
id="path92"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m161.43 146.3c-2.4274-1.213-3.8861-1.9048-5.748-3.5762l70.711 70.711c1.862 1.6714 3.3206 2.3632 5.748 3.5762z" />
|
|
||||||
<path
|
|
||||||
id="path94"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m155.68 142.72c-0.18578-0.19582-0.32768-0.35348-0.50391-0.54101l70.711 70.711c0.17623 0.18753 0.31813 0.34519 0.50391 0.54101z" />
|
|
||||||
<path
|
|
||||||
id="path96"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m155.18 142.18c1.047-1.7899 1.5781-3.0116 2.7754-4.9883l70.711 70.711c-1.1973 1.9767-1.7284 3.1984-2.7754 4.9883z" />
|
|
||||||
<path
|
|
||||||
id="path98"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m157.95 137.19c4.9715-8.2077 9.7196-21.436 11.604-32.795l70.711 70.711c-1.8839 11.359-6.632 24.587-11.604 32.795z" />
|
|
||||||
<path
|
|
||||||
id="path100"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m169.56 104.4c1.1273-6.797 0.81713-9.1729-0.83126-10.821l70.711 70.711c1.6484 1.6484 1.9586 4.0243 0.83126 10.821z" />
|
|
||||||
<path
|
|
||||||
id="path102"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m168.73 93.575c-0.32731-0.32731-0.70738-0.62594-1.1394-0.92479l70.711 70.711c0.43206 0.29886 0.81213 0.59748 1.1394 0.92479z" />
|
|
||||||
<path
|
|
||||||
id="path104"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m167.59 92.65c-2.0094-0.98322-2.8331-1.0491-4.9531-0.39844l70.711 70.711c2.12-0.65064 2.9438-0.58478 4.9531 0.39844z" />
|
|
||||||
<path
|
|
||||||
id="path106"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m162.63 92.252c-1.3988 0.42933-3.0221 1.3082-3.6055 1.9531l70.711 70.711c0.58337-0.64492 2.2067-1.5238 3.6055-1.9531z" />
|
|
||||||
<path
|
|
||||||
id="path108"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m159.03 94.205c-0.58363 0.64492-1.9734 4.6467-3.0879 8.8945l70.711 70.711c1.1145-4.2479 2.5043-8.2496 3.0879-8.8945z" />
|
|
||||||
<path
|
|
||||||
id="path110"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m155.94 103.1c-1.1144 4.2479-2.7555 9.2858-3.6465 11.193l70.711 70.711c0.89102-1.9076 2.532-6.9455 3.6465-11.193z" />
|
|
||||||
<path
|
|
||||||
id="path112"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m152.29 114.29c-0.89099 1.9076-1.8493 4.2376-2.1309 5.1777l70.711 70.711c0.28157-0.94013 1.2399-3.2702 2.1309-5.1777z" />
|
|
||||||
<path
|
|
||||||
id="path114"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m150.16 119.47c-0.28161 0.94013-1.2853 3.2472-2.2305 5.127l70.711 70.711c0.94515-1.8798 1.9489-4.1868 2.2305-5.127z" />
|
|
||||||
<path
|
|
||||||
id="path116"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m147.93 124.6-2 4.0059 70.711 70.711 2-4.0059z" />
|
|
||||||
<path
|
|
||||||
id="path118"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m145.93 128.6-1.1016-2.5469 70.711 70.711 1.1016 2.5469z" />
|
|
||||||
<path
|
|
||||||
id="path120"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m144.83 126.06c-0.50786-1.1277-2.6738-6.8816-5.1504-12.898l70.711 70.711c2.4766 6.0168 4.6425 11.771 5.1504 12.898z" />
|
|
||||||
<path
|
|
||||||
id="path122"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m139.68 113.16c-5.7036-13.857-11.141-23.408-17.401-29.668l70.711 70.711c6.2598 6.2598 11.697 15.811 17.401 29.668z" />
|
|
||||||
<path
|
|
||||||
id="path124"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m122.28 83.49c-6.0835-6.0835-12.944-9.058-21.58-9.8534l70.711 70.711c8.6358 0.79538 15.496 3.7699 21.58 9.8534z" />
|
|
||||||
<path
|
|
||||||
id="path126"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m100.7 73.637c-0.8718-0.08029-1.5892-0.13573-2.1641-0.16406l70.711 70.711c0.57491 0.0283 1.2923 0.0838 2.1641 0.16407z" />
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
id="path130"
|
|
||||||
fill="url(#a)"
|
|
||||||
d="m98.535 73.473c-4.0243-0.19833-1.1175 0.96369 3.918 4.7305 6.1387 4.592 12.047 11.094 15.006 15.666 2.9912 4.6225 5.6692 8.3037 12.262 28.254 2.4794 7.5032 5.7538 14.14 6.6211 16.113l1.3476 3.0059c-0.0653 0.0814-0.11927 0.14322-0.18554 0.22656l-3.4395 3.0098c-5.5481 5.7637-16.033 11.691-24.285 13.729-6.3614 1.5706-21.301 1.1651-27.809 0.0137-17.584-3.1111-27.647-17.73-31.035-32.051-1.5987-9.5426 0.52116-16.959 1.6895-21.707 2.6637-10.825 9.7356-20.465 18.479-26.402 4.5085-3.0615 6.0316-3.0332 2.6191-3.0332-3.9668 0-10.077 2.6389-14.275 6.0215-8.0199 6.461-14.768 15.598-18.961 26.336-2.2433 6.6848-2.2747 5.5109-2.418 15.162 0.37633 6.9829 1.0389 9.3071 3.0723 15.406 3.7252 11.174 11.436 20.03 21.352 26.033 4.9985 3.0261 15.324 6.7023 22.098 7.8027 1.8904 0.30967 4.992 0.90865 7.8848 1.1875 12.195 0.94625 15.055-0.32666 24.506-1.8418 8.5906-2.4983 16.678-7.275 22.934-12.367l5.7871-4.7168c0.0891 0.11275 0.1968 0.26762 0.28321 0.375l1.9648 2.0644c6.134 6.4426 12.296 9.8178 20.275 9.2246 7.5551-0.56163 13.041-2.6761 17.49-6.7422 4.5132-4.1248 5.5354-6.236 7.6699-12.512 2.9552-8.6889 4.3184-16.193 3.9922-29.721-0.33294-13.818-1.6567-19.14-5.4434-23.453-3.1472-3.3774-6.7785-3.4556-8.8613 1.5293-0.79737 1.9089-0.78178 2.91 0.11132 6.918 1.6289 7.3102 1.7837 25.828 0.45313 32.072-2.7904 13.095-11.463 17.859-22.207 12.49-2.4274-1.213-3.8861-1.9048-5.748-3.5762-0.18578-0.19582-0.32768-0.35348-0.50391-0.54101 1.047-1.7899 1.5781-3.0116 2.7754-4.9883 4.9715-8.2077 9.7196-21.436 11.604-32.795 1.3511-8.1466 0.63728-9.9421-1.9707-11.746-2.0094-0.98322-2.8331-1.0491-4.9531-0.39844-1.3988 0.42933-3.0221 1.3082-3.6055 1.9531-0.58363 0.64492-1.9734 4.6467-3.0879 8.8945-1.1144 4.2479-2.7555 9.2858-3.6465 11.193-0.89099 1.9076-1.8493 4.2376-2.1309 5.1777-0.28161 0.94013-1.2853 3.2472-2.2305 5.127l-2 4.0059-1.1016-2.5469c-0.50786-1.1277-2.6738-6.8816-5.1504-12.898-11.247-27.323-21.459-37.908-38.98-39.522-0.8718-0.080295-1.5892-0.13573-2.1641-0.16406z" />
|
|
||||||
</g>
|
|
||||||
</clipPath>
|
|
||||||
<linearGradient
|
|
||||||
xlink:href="#c"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="210.2"
|
|
||||||
y1="153.65"
|
|
||||||
x2="201.21"
|
|
||||||
x1="146.2"
|
|
||||||
id="a" />
|
|
||||||
<clipPath
|
|
||||||
id="f">
|
|
||||||
<circle
|
|
||||||
id="circle136"
|
|
||||||
opacity=".1"
|
|
||||||
clip-path="url(#e)"
|
|
||||||
r="29.772"
|
|
||||||
cy="2.6396"
|
|
||||||
cx="40.292"
|
|
||||||
transform="matrix(1.0037 0 0 1.0037 1.3836 -1.2671)" />
|
|
||||||
</clipPath>
|
|
||||||
<linearGradient
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="22.179"
|
|
||||||
y1="6.5707"
|
|
||||||
x2="61.309"
|
|
||||||
x1="45.701"
|
|
||||||
id="b">
|
|
||||||
<stop
|
|
||||||
id="stop139"
|
|
||||||
offset="0" />
|
|
||||||
<stop
|
|
||||||
id="stop141"
|
|
||||||
offset="1"
|
|
||||||
stop-opacity="0" />
|
|
||||||
</linearGradient>
|
|
||||||
<filter
|
|
||||||
color-interpolation-filters="sRGB"
|
|
||||||
height="1.048"
|
|
||||||
width="1.048"
|
|
||||||
y="-.024"
|
|
||||||
x="-.024"
|
|
||||||
id="d">
|
|
||||||
<feGaussianBlur
|
|
||||||
id="feGaussianBlur144"
|
|
||||||
stdDeviation="13.532652" />
|
|
||||||
</filter>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath4599">
|
|
||||||
<circle
|
|
||||||
cx="725.02002"
|
|
||||||
cy="-684.40002"
|
|
||||||
id="circle4601"
|
|
||||||
style="display:inline;fill:#2588d0;stroke-width:1"
|
|
||||||
r="666.01202" />
|
|
||||||
</clipPath>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#b"
|
|
||||||
id="linearGradient4609"
|
|
||||||
x1="932.39093"
|
|
||||||
y1="-448.59128"
|
|
||||||
x2="1103.4291"
|
|
||||||
y2="-292.29779"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
<radialGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient4780"
|
|
||||||
id="radialGradient4782"
|
|
||||||
cx="725.02002"
|
|
||||||
cy="-684.40002"
|
|
||||||
fx="725.02002"
|
|
||||||
fy="-684.40002"
|
|
||||||
r="666.01202"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
style="display:inline"
|
|
||||||
id="g177"
|
|
||||||
transform="translate(-8.0628,99.804)">
|
|
||||||
<g
|
|
||||||
style="display:inline"
|
|
||||||
id="g175"
|
|
||||||
transform="matrix(2,0,0,2,-5.3112,-36.986)">
|
|
||||||
<g
|
|
||||||
style="display:inline"
|
|
||||||
id="g173"
|
|
||||||
transform="matrix(0.046875,0,0,0.046875,6.3858,34.613)">
|
|
||||||
<g
|
|
||||||
style="display:inline"
|
|
||||||
id="g157">
|
|
||||||
<circle
|
|
||||||
style="display:inline;opacity:0.36000001;filter:url(#d)"
|
|
||||||
id="circle149"
|
|
||||||
r="676.63"
|
|
||||||
cy="-672.09003"
|
|
||||||
cx="710.14001"
|
|
||||||
transform="matrix(0.98504,0,0,0.98504,29.403,-5.6005)" />
|
|
||||||
<circle
|
|
||||||
r="666.01202"
|
|
||||||
style="display:inline;fill:url(#radialGradient4782);fill-opacity:1"
|
|
||||||
id="ellipse153"
|
|
||||||
cy="-684.40002"
|
|
||||||
cx="725.02002" />
|
|
||||||
<path
|
|
||||||
style="opacity:0.1;fill:url(#linearGradient4609);fill-opacity:1.0;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m 502.01302,-949.02227 384.24915,383.32798 -3.43645,-89.82106 -77.68618,-110.93471 -187.18224,-187.80303 137.91285,50.53567 165.36793,165.01042 87.04612,-110.32758 124.5448,123.76483 1.5739,-125.34778 679.4554,673.25412 -544.5472,559.35788 -866.24945,-865.97293 -82.57017,-258.03173 64.87656,-135.65098 z"
|
|
||||||
id="path4595"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
sodipodi:nodetypes="cccccccccccccccc"
|
|
||||||
clip-path="url(#clipPath4599)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
style="display:inline;stroke-width:0.052917"
|
|
||||||
id="g171"
|
|
||||||
transform="matrix(21.768,0,0,21.716,-148.18,-741.68)">
|
|
||||||
<g
|
|
||||||
style="display:inline;stroke:#efefef"
|
|
||||||
id="g167">
|
|
||||||
<g
|
|
||||||
style="display:inline;stroke:#efefef;stroke-width:0.052917"
|
|
||||||
id="g165">
|
|
||||||
<g
|
|
||||||
style="display:inline;stroke-width:0.052853"
|
|
||||||
id="g163"
|
|
||||||
transform="matrix(1.0012,0,0,1.0012,-0.2806,-0.13189)">
|
|
||||||
<path
|
|
||||||
style="display:inline;fill:#fefefe;stroke:#e9e9eb"
|
|
||||||
id="path159"
|
|
||||||
d="M 33.997,14.956 C 27.2465,14.30008 22.721,10.953 21.162,5.4628 c -0.97232,-3.4236 -0.40007,-6.6554 1.7689,-9.9897 1.963,-3.0177 4.5842,-4.9793 6.6538,-4.9793 h 0.55721 l -0.68124,0.45556 c -2.3327,1.5599 -4.1732,3.9523 -5.0456,6.5585 -0.51198,1.5295 -0.64528,4.5724 -0.27628,6.3067 0.63604,2.9894 2.4635,5.3876 5.0854,6.6734 1.7788,0.87238 3.409,1.2191 5.7324,1.2191 3.8422,0 6.9587,-1.203 9.6137,-3.711 l 0.90726,-0.85704 -0.45582,-1.0092 c -0.40414,-0.89475 -1.3238,-3.3221 -3.0074,-7.9377 -0.66266,-1.8167 -1.3902,-3.0367 -2.6217,-4.3966 -0.83567,-0.92274 -2.4857,-2.3558 -3.8598,-3.3523 -0.41933,-0.3041 -0.41152,-0.30624 0.78481,-0.21562 4.547,0.34444 6.8542,2.8969 10.192,11.276 0.58286,1.463 0.84878,2.0983 0.96426,2.3599 0.02953,0.0669 0.07669,0.21387 0.13247,0.16887 0.05579,-0.044998 0.2425,-0.39246 0.30776,-0.50578 0.06527,-0.11332 0.13756,-0.25964 0.22146,-0.43667 0.0839,-0.17703 0.1794,-0.38476 0.29108,-0.62088 0.81832,-1.7301 1.8142,-4.5739 2.1755,-6.2124 0.17685,-0.80191 0.31139,-1.0422 0.6944,-1.2403 1.2546,-0.64879 2.3739,-0.046158 2.3739,1.2782 0,2.3978 -1.562,7.2043 -3.3553,10.325 -0.11458,0.1994 -0.2299,0.32992 -0.28958,0.44825 -0.05969,0.11833 -0.22117,0.32942 -0.18689,0.38982 0.0262,0.046164 0.03631,0.058028 0.07857,0.1003 0.04494,0.044952 0.10546,0.1025 0.18606,0.16729 0.59444,0.47791 2.1261,1.0701 2.9132,1.178 1.0012,0.13722 2.1429,-0.34975 2.7808,-1.186 1.4436,-1.8927 1.7772,-5.2915 1.0067,-10.256 -0.28139,-1.8131 -0.2804,-1.8507 0.0612,-2.3095 0.8275,-1.1115 1.8285,-0.9019 2.5288,0.52951 1.4339,2.9307 1.2081,10.094 -0.43371,13.76 -0.9119,2.0362 -2.4949,3.1576 -5.135,3.6376 -2.1119,0.38401 -3.6271,-0.10683 -5.3738,-1.7407 l -1.0573,-0.98905 -0.49724,0.52519 c -1.2916,1.3642 -4.2775,3.0339 -6.4849,3.6264 -1.0871,0.29178 -3.7581,0.64007 -4.619,0.60228 -0.28227,-0.01239 -1.0906,-0.07862 -1.7963,-0.14719 z"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
style="display:inline;fill:#e9e9eb;stroke:#efefef"
|
|
||||||
id="path161"
|
|
||||||
d="M 49.244,6.7482 C 48.93619,6.36517 48.43741,5.6114 48.1356,5.0733 L 47.58686,4.09477 48.05882,3.2394 C 48.3184,2.76894 48.70519,1.8739 48.94989,1.2573 l 0.39948,-0.98883 1.3378,1.3563 c 0.8003,0.81132 1.266,1.4388 1.2301,1.6104 -0.08034,0.3845 -1.217,2.7582 -1.713,3.561 l -0.40066,0.6485 z"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
style="display:inline;fill:none;stroke:#e9e9eb"
|
|
||||||
id="path169"
|
|
||||||
d="m 45.251,7.0162 c 0,0 0.6208,1.4785 1.9195,3.2142"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
</g>
|
||||||
<style
|
<g id="bullet-char-template-57354" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
id="style179">.st0{fill:#e0e0e0}.st1{fill:#fff}.st2{clip-path:url(#SVGID_2_);fill:#fbbc05}.st3{clip-path:url(#SVGID_4_);fill:#ea4335}.st4{clip-path:url(#SVGID_6_);fill:#34a853}.st5{clip-path:url(#SVGID_8_);fill:#4285f4}</style>
|
<path d="M 8,1128 L 1137,1128 1137,0 8,0 8,1128 Z"/>
|
||||||
</svg>
|
</g>
|
||||||
|
<g id="bullet-char-template-10146" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
|
<path d="M 174,0 L 602,739 174,1481 1456,739 174,0 Z M 1358,739 L 309,1346 659,739 1358,739 Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="bullet-char-template-10132" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
|
<path d="M 2015,739 L 1276,0 717,0 1260,543 174,543 174,936 1260,936 717,1481 1274,1481 2015,739 Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="bullet-char-template-10007" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
|
<path d="M 0,-2 C -7,14 -16,27 -25,37 L 356,567 C 262,823 215,952 215,954 215,979 228,992 255,992 264,992 276,990 289,987 310,991 331,999 354,1012 L 381,999 492,748 772,1049 836,1024 860,1049 C 881,1039 901,1025 922,1006 886,937 835,863 770,784 769,783 710,716 594,584 L 774,223 C 774,196 753,168 711,139 L 727,119 C 717,90 699,76 672,76 641,76 570,178 457,381 L 164,-76 C 142,-110 111,-127 72,-127 30,-127 9,-110 8,-76 1,-67 -2,-52 -2,-32 -2,-23 -1,-13 0,-2 Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="bullet-char-template-10004" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
|
<path d="M 285,-33 C 182,-33 111,30 74,156 52,228 41,333 41,471 41,549 55,616 82,672 116,743 169,778 240,778 293,778 328,747 346,684 L 369,508 C 377,444 397,411 428,410 L 1163,1116 C 1174,1127 1196,1133 1229,1133 1271,1133 1292,1118 1292,1087 L 1292,965 C 1292,929 1282,901 1262,881 L 442,47 C 390,-6 338,-33 285,-33 Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="bullet-char-template-9679" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
|
<path d="M 813,0 C 632,0 489,54 383,161 276,268 223,411 223,592 223,773 276,916 383,1023 489,1130 632,1184 813,1184 992,1184 1136,1130 1245,1023 1353,916 1407,772 1407,592 1407,412 1353,268 1245,161 1136,54 992,0 813,0 Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="bullet-char-template-8226" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
|
<path d="M 346,457 C 273,457 209,483 155,535 101,586 74,649 74,723 74,796 101,859 155,911 209,963 273,989 346,989 419,989 480,963 531,910 582,859 608,796 608,723 608,648 583,586 532,535 482,483 420,457 346,457 Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="bullet-char-template-8211" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
|
<path d="M -4,459 L 1135,459 1135,606 -4,606 -4,459 Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="bullet-char-template-61548" transform="scale(0.00048828125,-0.00048828125)">
|
||||||
|
<path d="M 173,740 C 173,903 231,1043 346,1159 462,1274 601,1332 765,1332 928,1332 1067,1274 1183,1159 1299,1043 1357,903 1357,740 1357,577 1299,437 1183,322 1067,206 928,148 765,148 601,148 462,206 346,322 231,437 173,577 173,740 Z"/>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<defs class="TextEmbeddedBitmaps"/>
|
||||||
|
<g>
|
||||||
|
<g id="id2" class="Master_Slide">
|
||||||
|
<g id="bg-id2" class="Background"/>
|
||||||
|
<g id="bo-id2" class="BackgroundObjects"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g class="SlideGroup">
|
||||||
|
<g>
|
||||||
|
<g id="container-id1">
|
||||||
|
<g id="id1" class="Slide" clip-path="url(#presentation_clip_path)">
|
||||||
|
<g class="Page">
|
||||||
|
<g class="com.sun.star.drawing.CustomShape">
|
||||||
|
<g id="id3">
|
||||||
|
<rect class="BoundingBox" stroke="none" fill="none" x="-6" y="-6" width="18014" height="18014"/>
|
||||||
|
<path fill="rgb(56,142,60)" stroke="none" d="M 9000,100 C 14046,100 17900,3954 17900,9000 17900,14046 14046,17900 9000,17900 3954,17900 100,14046 100,9000 100,3954 3954,100 9000,100 Z"/>
|
||||||
|
<path fill="none" stroke="rgb(0,0,0)" stroke-width="212" stroke-linejoin="round" d="M 9000,100 C 14046,100 17900,3954 17900,9000 17900,14046 14046,17900 9000,17900 3954,17900 100,14046 100,9000 100,3954 3954,100 9000,100 Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g class="com.sun.star.drawing.CustomShape">
|
||||||
|
<g id="id4">
|
||||||
|
<rect class="BoundingBox" stroke="none" fill="none" x="1094" y="994" width="15813" height="15813"/>
|
||||||
|
<path fill="rgb(231,231,231)" stroke="none" d="M 9000,1100 C 13422,1100 16800,4478 16800,8900 16800,13322 13422,16700 9000,16700 4578,16700 1200,13322 1200,8900 1200,4478 4578,1100 9000,1100 Z"/>
|
||||||
|
<path fill="none" stroke="rgb(0,0,0)" stroke-width="212" stroke-linejoin="round" d="M 9000,1100 C 13422,1100 16800,4478 16800,8900 16800,13322 13422,16700 9000,16700 4578,16700 1200,13322 1200,8900 1200,4478 4578,1100 9000,1100 Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g class="Group">
|
||||||
|
<g class="com.sun.star.drawing.ClosedBezierShape">
|
||||||
|
<g id="id5">
|
||||||
|
<rect class="BoundingBox" stroke="none" fill="none" x="1265" y="1958" width="7446" height="12870"/>
|
||||||
|
<path fill="rgb(0,0,0)" stroke="none" d="M 5401,2000 C 5190,2137 4655,2216 4001,2800 3160,3551 2170,4881 1901,5500 625,8597 1313,13283 3601,14600 3643,14621 3768,14533 3801,14500 3834,14467 3801,14433 3801,14400 4501,14700 5001,14900 5801,14800 6316,14701 6847,14367 7101,14000 7601,13200 7254,13438 7401,13300 6701,13000 5501,12900 5201,12200 5027,11781 4897,11621 4801,11300 4727,11051 4683,10737 4601,10500 5268,10733 5990,11177 6601,11200 7212,11223 7949,11138 8101,10800 8253,10462 8032,10520 7801,10200 7570,9880 7068,9467 6701,9100 7168,9067 7744,9129 8101,9000 8458,8871 8614,8942 8701,8500 8788,8058 8197,7274 7801,6800 7405,6326 6868,6067 6401,5700 6468,5533 6567,5400 6601,5200 6635,5000 6424,4712 6601,4500 6778,4288 7300,4417 7501,4200 7702,3983 7736,3632 7701,3400 7666,3168 7647,3107 7301,2800 6955,2493 5776,1783 5401,2000 Z"/>
|
||||||
|
<path fill="none" stroke="rgb(0,0,0)" d="M 5401,2000 C 5190,2137 4655,2216 4001,2800 3160,3551 2170,4881 1901,5500 625,8597 1313,13283 3601,14600 3643,14621 3768,14533 3801,14500 3834,14467 3801,14433 3801,14400 4501,14700 5001,14900 5801,14800 6316,14701 6847,14367 7101,14000 7601,13200 7254,13438 7401,13300 6701,13000 5501,12900 5201,12200 5027,11781 4897,11621 4801,11300 4727,11051 4683,10737 4601,10500 5268,10733 5990,11177 6601,11200 7212,11223 7949,11138 8101,10800 8253,10462 8032,10520 7801,10200 7570,9880 7068,9467 6701,9100 7168,9067 7744,9129 8101,9000 8458,8871 8614,8942 8701,8500 8788,8058 8197,7274 7801,6800 7405,6326 6868,6067 6401,5700 6468,5533 6567,5400 6601,5200 6635,5000 6424,4712 6601,4500 6778,4288 7300,4417 7501,4200 7702,3983 7736,3632 7701,3400 7666,3168 7647,3107 7301,2800 6955,2493 5776,1783 5401,2000 Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g class="com.sun.star.drawing.ClosedBezierShape">
|
||||||
|
<g id="id6">
|
||||||
|
<rect class="BoundingBox" stroke="none" fill="none" x="10569" y="1898" width="5534" height="3904"/>
|
||||||
|
<path fill="rgb(0,0,0)" stroke="none" d="M 12400,1900 C 12077,1874 10510,2796 10601,3100 10469,3201 10771,3800 11301,4100 12890,4999 15894,5652 16101,5800 16090,5800 15987,5038 15100,4000 14410,3192 13119,1957 12400,1900 Z"/>
|
||||||
|
<path fill="none" stroke="rgb(0,0,0)" d="M 12400,1900 C 12077,1874 10510,2796 10601,3100 10469,3201 10771,3800 11301,4100 12890,4999 15894,5652 16101,5800 16090,5800 15987,5038 15100,4000 14410,3192 13119,1957 12400,1900 Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g class="com.sun.star.drawing.ClosedBezierShape">
|
||||||
|
<g id="id7">
|
||||||
|
<rect class="BoundingBox" stroke="none" fill="none" x="9094" y="4800" width="7691" height="10403"/>
|
||||||
|
<path fill="rgb(0,0,0)" stroke="none" d="M 11901,4801 C 16301,6501 16401,6501 16401,6501 16401,6501 17106,9347 16600,10900 16371,11648 15622,13159 15001,14001 14621,14516 13708,15222 13701,15201 13701,15201 12457,15112 11901,14801 11424,14534 10801,13601 10801,13601 10801,13601 13076,13181 13801,12401 14297,11868 14401,10401 14401,10401 14401,10401 13234,11134 12501,11201 11768,11268 10395,11342 10001,10701 9928,10584 10189,10442 10301,10301 10731,9757 11901,9101 11901,9101 11901,9101 10203,9101 9601,8901 9259,8787 9132,8735 9101,8501 9030,7968 9628,7388 10001,7001 10573,6409 12101,5701 12101,5701 12101,5701 11918,5337 11900,5201 11881,5036 11901,4801 11901,4801 Z"/>
|
||||||
|
<path fill="none" stroke="rgb(0,0,0)" d="M 11901,4801 C 16301,6501 16401,6501 16401,6501 16401,6501 17106,9347 16600,10900 16371,11648 15622,13159 15001,14001 14621,14516 13708,15222 13701,15201 13701,15201 12457,15112 11901,14801 11424,14534 10801,13601 10801,13601 10801,13601 13076,13181 13801,12401 14297,11868 14401,10401 14401,10401 14401,10401 13234,11134 12501,11201 11768,11268 10395,11342 10001,10701 9928,10584 10189,10442 10301,10301 10731,9757 11901,9101 11901,9101 11901,9101 10203,9101 9601,8901 9259,8787 9132,8735 9101,8501 9030,7968 9628,7388 10001,7001 10573,6409 12101,5701 12101,5701 12101,5701 11918,5337 11900,5201 11881,5036 11901,4801 11901,4801 Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 9.5 KiB |
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.Either
|
import com.keylesspalace.tusky.util.Either
|
||||||
|
@ -49,7 +49,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private typealias AccountInfo = Pair<Account, Boolean>
|
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
||||||
|
|
||||||
class AccountsInListFragment : DialogFragment(), Injectable {
|
class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
|
|
||||||
|
@ -168,21 +168,21 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
viewModel.deleteAccountFromList(listId, accountId)
|
viewModel.deleteAccountFromList(listId, accountId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAddToList(account: Account) {
|
private fun onAddToList(account: TimelineAccount) {
|
||||||
viewModel.addAccountToList(listId, account)
|
viewModel.addAccountToList(listId, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
private object AccountDiffer : DiffUtil.ItemCallback<Account>() {
|
private object AccountDiffer : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
|
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
|
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||||
return oldItem.deepEquals(newItem)
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Adapter : ListAdapter<Account, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
||||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
@ -209,12 +209,11 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
|
|
||||||
private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() {
|
private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() {
|
||||||
override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem.first.id == newItem.first.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||||
return oldItem.second == newItem.second &&
|
return oldItem == newItem
|
||||||
oldItem.first.deepEquals(newItem.first)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginActivity;
|
||||||
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.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
|
@ -197,6 +198,33 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @Nullable String getOpenAsText() {
|
||||||
|
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
|
||||||
|
switch (accounts.size()) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
return null;
|
||||||
|
case 2:
|
||||||
|
for (AccountEntity account : accounts) {
|
||||||
|
if (account != accountManager.getActiveAccount()) {
|
||||||
|
return String.format(getString(R.string.action_open_as), account.getFullName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return String.format(getString(R.string.action_open_as), "…");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openAsAccount(@NonNull String url, @NonNull AccountEntity account) {
|
||||||
|
accountManager.setActiveAccount(account);
|
||||||
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
|
intent.putExtra(MainActivity.REDIRECT_URL, url);
|
||||||
|
startActivity(intent);
|
||||||
|
finishWithoutSlideOutAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -27,7 +28,7 @@ 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.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
import com.keylesspalace.tusky.util.openLink
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
|
@ -157,9 +158,9 @@ abstract class BottomSheetActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
|
||||||
open fun openLink(url: String) {
|
open fun openLink(url: String) {
|
||||||
LinkHelper.openLink(url, this)
|
(this as Context).openLink(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showQuerySheet() {
|
private fun showQuerySheet() {
|
||||||
|
|
|
@ -15,28 +15,26 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
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.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
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.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
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.resource.bitmap.FitCenter
|
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.canhub.cropper.CropImage
|
import com.canhub.cropper.CropImageContract
|
||||||
|
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.databinding.ActivityEditProfileBinding
|
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
||||||
|
@ -44,9 +42,7 @@ import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
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.viewmodel.EditProfileViewModel
|
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||||
|
@ -63,12 +59,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
const val HEADER_WIDTH = 1500
|
const val HEADER_WIDTH = 1500
|
||||||
const val HEADER_HEIGHT = 500
|
const val HEADER_HEIGHT = 500
|
||||||
|
|
||||||
private const val AVATAR_PICK_RESULT = 1
|
|
||||||
private const val HEADER_PICK_RESULT = 2
|
|
||||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
|
||||||
private const val MAX_ACCOUNT_FIELDS = 4
|
private const val MAX_ACCOUNT_FIELDS = 4
|
||||||
|
|
||||||
private const val BUNDLE_CURRENTLY_PICKING = "BUNDLE_CURRENTLY_PICKING"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -78,23 +69,28 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
||||||
|
|
||||||
private var currentlyPicking: PickType = PickType.NOTHING
|
|
||||||
|
|
||||||
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
||||||
|
|
||||||
private enum class PickType {
|
private enum class PickType {
|
||||||
NOTHING,
|
|
||||||
AVATAR,
|
AVATAR,
|
||||||
HEADER
|
HEADER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val cropImage = registerForActivityResult(CropImageContract()) { result ->
|
||||||
|
if (result.isSuccessful) {
|
||||||
|
if (result.uriContent == viewModel.getAvatarUri()) {
|
||||||
|
viewModel.newAvatarPicked()
|
||||||
|
} else {
|
||||||
|
viewModel.newHeaderPicked()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onPickFailure(result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
savedInstanceState?.getString(BUNDLE_CURRENTLY_PICKING)?.let {
|
|
||||||
currentlyPicking = PickType.valueOf(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
|
@ -104,8 +100,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
|
binding.avatarButton.setOnClickListener { pickMedia(PickType.AVATAR) }
|
||||||
binding.headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
|
binding.headerButton.setOnClickListener { pickMedia(PickType.HEADER) }
|
||||||
|
|
||||||
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
||||||
binding.fieldList.adapter = accountFieldEditAdapter
|
binding.fieldList.adapter = accountFieldEditAdapter
|
||||||
|
@ -159,11 +155,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Error -> {
|
is Error -> {
|
||||||
val snackbar = Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||||
snackbar.setAction(R.string.action_retry) {
|
.setAction(R.string.action_retry) {
|
||||||
viewModel.obtainProfile()
|
viewModel.obtainProfile()
|
||||||
}
|
}
|
||||||
snackbar.show()
|
.show()
|
||||||
}
|
}
|
||||||
is Loading -> { }
|
is Loading -> { }
|
||||||
}
|
}
|
||||||
|
@ -179,30 +175,24 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
|
observeImage(viewModel.avatarData, binding.avatarPreview, true)
|
||||||
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
|
observeImage(viewModel.headerData, binding.headerPreview, false)
|
||||||
|
|
||||||
viewModel.saveData.observe(
|
viewModel.saveData.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Success -> {
|
is Success -> {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
is Loading -> {
|
is Loading -> {
|
||||||
binding.saveProgressBar.visibility = View.VISIBLE
|
binding.saveProgressBar.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
is Error -> {
|
is Error -> {
|
||||||
onSaveFailure(it.errorMessage)
|
onSaveFailure(it.errorMessage)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putString(BUNDLE_CURRENTLY_PICKING, currentlyPicking.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
@ -218,90 +208,60 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeImage(
|
private fun observeImage(
|
||||||
liveData: LiveData<Resource<Bitmap>>,
|
liveData: LiveData<Uri>,
|
||||||
imageView: ImageView,
|
imageView: ImageView,
|
||||||
progressBar: View,
|
|
||||||
roundedCorners: Boolean
|
roundedCorners: Boolean
|
||||||
) {
|
) {
|
||||||
liveData.observe(
|
liveData.observe(
|
||||||
this,
|
this
|
||||||
{
|
) { imageUri ->
|
||||||
|
|
||||||
when (it) {
|
// skipping all caches so we can always reuse the same uri
|
||||||
is Success -> {
|
val glide = Glide.with(imageView)
|
||||||
val glide = Glide.with(imageView)
|
.load(imageUri)
|
||||||
.load(it.data)
|
.skipMemoryCache(true)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
|
||||||
if (roundedCorners) {
|
if (roundedCorners) {
|
||||||
glide.transform(
|
glide.transform(
|
||||||
FitCenter(),
|
FitCenter(),
|
||||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||||
)
|
).into(imageView)
|
||||||
}
|
} else {
|
||||||
|
glide.into(imageView)
|
||||||
glide.into(imageView)
|
|
||||||
|
|
||||||
imageView.show()
|
|
||||||
progressBar.hide()
|
|
||||||
}
|
|
||||||
is Loading -> {
|
|
||||||
progressBar.show()
|
|
||||||
}
|
|
||||||
is Error -> {
|
|
||||||
progressBar.hide()
|
|
||||||
if (!it.consumed) {
|
|
||||||
onResizeFailure()
|
|
||||||
it.consumed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMediaPick(pickType: PickType) {
|
imageView.show()
|
||||||
if (currentlyPicking != PickType.NOTHING) {
|
|
||||||
// Ignore inputs if another pick operation is still occurring.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentlyPicking = pickType
|
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
|
|
||||||
} else {
|
|
||||||
initiateMediaPicking()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
private fun pickMedia(pickType: PickType) {
|
||||||
requestCode: Int,
|
|
||||||
permissions: Array<String>,
|
|
||||||
grantResults: IntArray
|
|
||||||
) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
when (requestCode) {
|
|
||||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
|
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
initiateMediaPicking()
|
|
||||||
} else {
|
|
||||||
endMediaPicking()
|
|
||||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initiateMediaPicking() {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
intent.type = "image/*"
|
intent.type = "image/*"
|
||||||
when (currentlyPicking) {
|
when (pickType) {
|
||||||
PickType.AVATAR -> {
|
PickType.AVATAR -> {
|
||||||
startActivityForResult(intent, AVATAR_PICK_RESULT)
|
cropImage.launch(
|
||||||
|
options {
|
||||||
|
setRequestedSize(AVATAR_SIZE, AVATAR_SIZE)
|
||||||
|
setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
||||||
|
setImageSource(includeGallery = true, includeCamera = false)
|
||||||
|
setOutputUri(viewModel.getAvatarUri())
|
||||||
|
setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
PickType.HEADER -> {
|
PickType.HEADER -> {
|
||||||
startActivityForResult(intent, HEADER_PICK_RESULT)
|
cropImage.launch(
|
||||||
|
options {
|
||||||
|
setRequestedSize(HEADER_WIDTH, HEADER_HEIGHT)
|
||||||
|
setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||||
|
setImageSource(includeGallery = true, includeCamera = false)
|
||||||
|
setOutputUri(viewModel.getHeaderUri())
|
||||||
|
setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
PickType.NOTHING -> { /* do nothing */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,16 +281,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
if (currentlyPicking != PickType.NOTHING) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.save(
|
viewModel.save(
|
||||||
binding.displayNameEditText.text.toString(),
|
binding.displayNameEditText.text.toString(),
|
||||||
binding.noteEditText.text.toString(),
|
binding.noteEditText.text.toString(),
|
||||||
binding.lockedCheckBox.isChecked,
|
binding.lockedCheckBox.isChecked,
|
||||||
accountFieldEditAdapter.getFieldData(),
|
accountFieldEditAdapter.getFieldData()
|
||||||
this
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,90 +295,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
binding.saveProgressBar.visibility = View.GONE
|
binding.saveProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun beginMediaPicking() {
|
private fun onPickFailure(throwable: Throwable?) {
|
||||||
when (currentlyPicking) {
|
Log.w("EditProfileActivity", "failed to pick media", throwable)
|
||||||
PickType.AVATAR -> {
|
|
||||||
binding.avatarProgressBar.visibility = View.VISIBLE
|
|
||||||
binding.avatarPreview.visibility = View.INVISIBLE
|
|
||||||
binding.avatarButton.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
PickType.HEADER -> {
|
|
||||||
binding.headerProgressBar.visibility = View.VISIBLE
|
|
||||||
binding.headerPreview.visibility = View.INVISIBLE
|
|
||||||
binding.headerButton.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
PickType.NOTHING -> { /* do nothing */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun endMediaPicking() {
|
|
||||||
binding.avatarProgressBar.visibility = View.GONE
|
|
||||||
binding.headerProgressBar.visibility = View.GONE
|
|
||||||
|
|
||||||
currentlyPicking = PickType.NOTHING
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
when (requestCode) {
|
|
||||||
AVATAR_PICK_RESULT -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
|
||||||
CropImage.activity(data.data)
|
|
||||||
.setInitialCropWindowPaddingRatio(0f)
|
|
||||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
|
||||||
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
|
||||||
.start(this)
|
|
||||||
} else {
|
|
||||||
endMediaPicking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HEADER_PICK_RESULT -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
|
||||||
CropImage.activity(data.data)
|
|
||||||
.setInitialCropWindowPaddingRatio(0f)
|
|
||||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
|
||||||
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
|
||||||
.start(this)
|
|
||||||
} else {
|
|
||||||
endMediaPicking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> {
|
|
||||||
val result = CropImage.getActivityResult(data)
|
|
||||||
when (resultCode) {
|
|
||||||
Activity.RESULT_OK -> beginResize(result?.uriContent)
|
|
||||||
CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure()
|
|
||||||
else -> endMediaPicking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun beginResize(uri: Uri?) {
|
|
||||||
if (uri == null) {
|
|
||||||
currentlyPicking = PickType.NOTHING
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
beginMediaPicking()
|
|
||||||
|
|
||||||
when (currentlyPicking) {
|
|
||||||
PickType.AVATAR -> {
|
|
||||||
viewModel.newAvatar(uri, this)
|
|
||||||
}
|
|
||||||
PickType.HEADER -> {
|
|
||||||
viewModel.newHeader(uri, this)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw AssertionError("PickType not set.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentlyPicking = PickType.NOTHING
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onResizeFailure() {
|
|
||||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||||
endMediaPicking()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,6 @@ import at.connyduck.sparkbutton.helpers.Utils
|
||||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
import autodispose2.autoDispose
|
import autodispose2.autoDispose
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
|
||||||
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
@ -201,7 +200,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
|
|
||||||
private fun onListSelected(listId: String) {
|
private fun onListSelected(listId: String) {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithSlideInAnimation(
|
||||||
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
|
StatusListActivity.newListIntent(this, listId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,381 +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.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.entity.AccessToken
|
|
||||||
import com.keylesspalace.tusky.entity.AppCredentials
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
|
||||||
import com.keylesspalace.tusky.util.rickRoll
|
|
||||||
import com.keylesspalace.tusky.util.shouldRickRoll
|
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class LoginActivity : BaseActivity(), Injectable {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var mastodonApi: MastodonApi
|
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityLoginBinding::inflate)
|
|
||||||
|
|
||||||
private lateinit var preferences: SharedPreferences
|
|
||||||
|
|
||||||
private val oauthRedirectUri: String
|
|
||||||
get() {
|
|
||||||
val scheme = getString(R.string.oauth_scheme)
|
|
||||||
val host = BuildConfig.APPLICATION_ID
|
|
||||||
return "$scheme://$host/"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
if (savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
|
|
||||||
binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
|
|
||||||
binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
|
|
||||||
Glide.with(binding.loginLogo)
|
|
||||||
.load(BuildConfig.CUSTOM_LOGO_URL)
|
|
||||||
.placeholder(null)
|
|
||||||
.into(binding.loginLogo)
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences = getSharedPreferences(
|
|
||||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.loginButton.setOnClickListener { onButtonClick() }
|
|
||||||
binding.registerButton.setOnClickListener { onRegisterClick() }
|
|
||||||
|
|
||||||
binding.whatsAnInstanceTextView.setOnClickListener {
|
|
||||||
val dialog = AlertDialog.Builder(this)
|
|
||||||
.setMessage(R.string.dialog_whats_an_instance)
|
|
||||||
.setPositiveButton(R.string.action_close, null)
|
|
||||||
.show()
|
|
||||||
val textView = dialog.findViewById<TextView>(android.R.id.message)
|
|
||||||
textView?.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAdditionalLogin()) {
|
|
||||||
setSupportActionBar(binding.toolbar)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
|
||||||
} else {
|
|
||||||
binding.toolbar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun requiresLogin(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
|
||||||
super.finish()
|
|
||||||
if (isAdditionalLogin()) {
|
|
||||||
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onRegisterClick() {
|
|
||||||
binding.registerButton.isEnabled = false
|
|
||||||
|
|
||||||
val uri = Uri.parse(BuildConfig.REGISTER_ACCOUNT_URL)
|
|
||||||
if (!openInCustomTab(uri, this)) {
|
|
||||||
val viewIntent = Intent(Intent.ACTION_VIEW, uri)
|
|
||||||
if (viewIntent.resolveActivity(packageManager) != null) {
|
|
||||||
startActivity(viewIntent)
|
|
||||||
} else {
|
|
||||||
binding.domainEditText.error = getString(R.string.error_no_web_browser_found)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtain the oauth client credentials for this app. This is only necessary the first time the
|
|
||||||
* app is run on a given server instance. So, after the first authentication, they are
|
|
||||||
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
|
|
||||||
*/
|
|
||||||
private fun onButtonClick() {
|
|
||||||
|
|
||||||
binding.loginButton.isEnabled = false
|
|
||||||
|
|
||||||
val domain = canonicalizeDomain(binding.domainEditText.text.toString())
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpUrl.Builder().host(domain).scheme("https").build()
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
setLoading(false)
|
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_invalid_domain)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRickRoll(this, domain)) {
|
|
||||||
rickRoll(this)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val callback = object : Callback<AppCredentials> {
|
|
||||||
override fun onResponse(
|
|
||||||
call: Call<AppCredentials>,
|
|
||||||
response: Response<AppCredentials>
|
|
||||||
) {
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
binding.loginButton.isEnabled = true
|
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
|
|
||||||
setLoading(false)
|
|
||||||
Log.e(TAG, "App authentication failed. " + response.message())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val credentials = response.body()
|
|
||||||
val clientId = credentials!!.clientId
|
|
||||||
val clientSecret = credentials.clientSecret
|
|
||||||
|
|
||||||
preferences.edit()
|
|
||||||
.putString("domain", domain)
|
|
||||||
.putString("clientId", clientId)
|
|
||||||
.putString("clientSecret", clientSecret)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
redirectUserToAuthorizeAndLogin(domain, clientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<AppCredentials>, t: Throwable) {
|
|
||||||
binding.loginButton.isEnabled = true
|
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
|
|
||||||
setLoading(false)
|
|
||||||
Log.e(TAG, Log.getStackTraceString(t))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mastodonApi
|
|
||||||
.authenticateApp(
|
|
||||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
|
||||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
|
||||||
)
|
|
||||||
.enqueue(callback)
|
|
||||||
setLoading(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
|
|
||||||
/* To authorize this app and log in it's necessary to redirect to the domain given,
|
|
||||||
* login there, and the server will redirect back to the app with its response. */
|
|
||||||
val endpoint = MastodonApi.ENDPOINT_AUTHORIZE
|
|
||||||
val parameters = mapOf(
|
|
||||||
"client_id" to clientId,
|
|
||||||
"redirect_uri" to oauthRedirectUri,
|
|
||||||
"response_type" to "code",
|
|
||||||
"scope" to OAUTH_SCOPES
|
|
||||||
)
|
|
||||||
val url = "https://" + domain + endpoint + "?" + toQueryString(parameters)
|
|
||||||
val uri = Uri.parse(url)
|
|
||||||
if (!openInCustomTab(uri, this)) {
|
|
||||||
val viewIntent = Intent(Intent.ACTION_VIEW, uri)
|
|
||||||
if (viewIntent.resolveActivity(packageManager) != null) {
|
|
||||||
startActivity(viewIntent)
|
|
||||||
} else {
|
|
||||||
binding.domainEditText.error = getString(R.string.error_no_web_browser_found)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
/* Check if we are resuming during authorization by seeing if the intent contains the
|
|
||||||
* redirect that was given to the server. If so, its response is here! */
|
|
||||||
val uri = intent.data
|
|
||||||
val redirectUri = oauthRedirectUri
|
|
||||||
|
|
||||||
if (uri != null && uri.toString().startsWith(redirectUri)) {
|
|
||||||
// This should either have returned an authorization code or an error.
|
|
||||||
val code = uri.getQueryParameter("code")
|
|
||||||
val error = uri.getQueryParameter("error")
|
|
||||||
|
|
||||||
/* restore variables from SharedPreferences */
|
|
||||||
val domain = preferences.getNonNullString(DOMAIN, "")
|
|
||||||
val clientId = preferences.getNonNullString(CLIENT_ID, "")
|
|
||||||
val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "")
|
|
||||||
|
|
||||||
if (code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) {
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
/* Since authorization has succeeded, the final step to log in is to exchange
|
|
||||||
* the authorization code for an access token. */
|
|
||||||
val callback = object : Callback<AccessToken> {
|
|
||||||
override fun onResponse(call: Call<AccessToken>, response: Response<AccessToken>) {
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
onLoginSuccess(response.body()!!.accessToken, domain)
|
|
||||||
} else {
|
|
||||||
setLoading(false)
|
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
|
||||||
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
|
|
||||||
setLoading(false)
|
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
|
||||||
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mastodonApi.fetchOAuthToken(
|
|
||||||
domain, clientId, clientSecret, redirectUri, code,
|
|
||||||
"authorization_code"
|
|
||||||
).enqueue(callback)
|
|
||||||
} else if (error != null) {
|
|
||||||
/* Authorization failed. Put the error response where the user can read it and they
|
|
||||||
* can try again. */
|
|
||||||
setLoading(false)
|
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
|
|
||||||
Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
|
|
||||||
} else {
|
|
||||||
// This case means a junk response was received somehow.
|
|
||||||
setLoading(false)
|
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_authorization_unknown)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// first show or user cancelled login
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setLoading(loadingState: Boolean) {
|
|
||||||
if (loadingState) {
|
|
||||||
binding.loginLoadingLayout.visibility = View.VISIBLE
|
|
||||||
binding.loginInputLayout.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
binding.loginLoadingLayout.visibility = View.GONE
|
|
||||||
binding.loginInputLayout.visibility = View.VISIBLE
|
|
||||||
binding.loginButton.isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isAdditionalLogin(): Boolean {
|
|
||||||
return intent.getBooleanExtra(LOGIN_MODE, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLoginSuccess(accessToken: String, domain: String) {
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
accountManager.addAccount(accessToken, domain)
|
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "LoginActivity" // logging tag
|
|
||||||
private const val OAUTH_SCOPES = "read write follow"
|
|
||||||
private const val LOGIN_MODE = "LOGIN_MODE"
|
|
||||||
private const val DOMAIN = "domain"
|
|
||||||
private const val CLIENT_ID = "clientId"
|
|
||||||
private const val CLIENT_SECRET = "clientSecret"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getIntent(context: Context, mode: Boolean): Intent {
|
|
||||||
val loginIntent = Intent(context, LoginActivity::class.java)
|
|
||||||
loginIntent.putExtra(LOGIN_MODE, mode)
|
|
||||||
return loginIntent
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Make sure the user-entered text is just a fully-qualified domain name. */
|
|
||||||
private fun canonicalizeDomain(domain: String): String {
|
|
||||||
// Strip any schemes out.
|
|
||||||
var s = domain.replaceFirst("http://", "")
|
|
||||||
s = s.replaceFirst("https://", "")
|
|
||||||
// If a username was included (e.g. username@example.com), just take what's after the '@'.
|
|
||||||
val at = s.lastIndexOf('@')
|
|
||||||
if (at != -1) {
|
|
||||||
s = s.substring(at + 1)
|
|
||||||
}
|
|
||||||
return s.trim { it <= ' ' }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chain together the key-value pairs into a query string, for either appending to a URL or
|
|
||||||
* as the content of an HTTP request.
|
|
||||||
*/
|
|
||||||
private fun toQueryString(parameters: Map<String, String>): String {
|
|
||||||
val s = StringBuilder()
|
|
||||||
var between = ""
|
|
||||||
for ((key, value) in parameters) {
|
|
||||||
s.append(between)
|
|
||||||
s.append(Uri.encode(key))
|
|
||||||
s.append("=")
|
|
||||||
s.append(Uri.encode(value))
|
|
||||||
between = "&"
|
|
||||||
}
|
|
||||||
return s.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openInCustomTab(uri: Uri, context: Context): Boolean {
|
|
||||||
|
|
||||||
val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface)
|
|
||||||
val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
|
|
||||||
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
|
|
||||||
|
|
||||||
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
|
|
||||||
.setToolbarColor(toolbarColor)
|
|
||||||
.setNavigationBarColor(navigationbarColor)
|
|
||||||
.setNavigationBarDividerColor(navigationbarDividerColor)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val customTabsIntent = CustomTabsIntent.Builder()
|
|
||||||
.setDefaultColorSchemeParams(colorSchemeParams)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
try {
|
|
||||||
customTabsIntent.launchUrl(context, uri)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.w(TAG, "Activity was not found for intent $customTabsIntent")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -64,9 +64,10 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canH
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
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.databinding.ActivityMainBinding
|
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
|
@ -325,9 +326,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
super.onPostCreate(savedInstanceState)
|
super.onPostCreate(savedInstanceState)
|
||||||
|
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val statusUrl = intent.getStringExtra(STATUS_URL)
|
val redirectUrl = intent.getStringExtra(REDIRECT_URL)
|
||||||
if (statusUrl != null) {
|
if (redirectUrl != null) {
|
||||||
viewUrl(statusUrl, PostLookupFallbackBehavior.DISPLAY_ERROR)
|
viewUrl(redirectUrl, PostLookupFallbackBehavior.DISPLAY_ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -454,10 +455,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.action_access_scheduled_toot
|
nameRes = R.string.action_access_scheduled_posts
|
||||||
iconRes = R.drawable.ic_access_time
|
iconRes = R.drawable.ic_access_time
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
startActivityWithSlideInAnimation(ScheduledStatusActivity.newIntent(context))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
|
@ -834,7 +835,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
private const val TAG = "MainActivity" // logging tag
|
private const val TAG = "MainActivity" // logging tag
|
||||||
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 STATUS_URL = "statusUrl"
|
const val REDIRECT_URL = "redirectUrl"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
package com.keylesspalace.tusky
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
|
||||||
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
|
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
|
||||||
import dagger.android.DispatchingAndroidInjector
|
|
||||||
import dagger.android.HasAndroidInjector
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val binding = ActivityModalTimelineBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
|
||||||
supportActionBar?.apply {
|
|
||||||
title = getString(R.string.title_list_timeline)
|
|
||||||
setDisplayHomeAsUpEnabled(true)
|
|
||||||
setDisplayShowHomeEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
|
|
||||||
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind
|
|
||||||
?: TimelineViewModel.Kind.HOME
|
|
||||||
val argument = intent?.getStringExtra(ARG_ARG)
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getActionButton(): FloatingActionButton? = null
|
|
||||||
|
|
||||||
override fun androidInjector() = dispatchingAndroidInjector
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ARG_KIND = "kind"
|
|
||||||
private const val ARG_ARG = "arg"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun newIntent(
|
|
||||||
context: Context,
|
|
||||||
kind: TimelineViewModel.Kind,
|
|
||||||
argument: String?
|
|
||||||
): Intent {
|
|
||||||
val intent = Intent(context, ModalTimelineActivity::class.java)
|
|
||||||
intent.putExtra(ARG_KIND, kind)
|
|
||||||
intent.putExtra(ARG_ARG, argument)
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,14 +15,17 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SuppressLint("CustomSplashScreen")
|
||||||
class SplashActivity : AppCompatActivity(), Injectable {
|
class SplashActivity : AppCompatActivity(), Injectable {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
|
|
@ -31,9 +31,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
||||||
|
|
||||||
private val kind: Kind
|
|
||||||
get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val binding = ActivityStatuslistBinding.inflate(layoutInflater)
|
val binding = ActivityStatuslistBinding.inflate(layoutInflater)
|
||||||
|
@ -41,10 +38,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
|
|
||||||
val title = if (kind == Kind.FAVOURITES) {
|
val kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
||||||
R.string.title_favourites
|
val listId = intent.getStringExtra(EXTRA_LIST_ID)
|
||||||
} else {
|
val hashtag = intent.getStringExtra(EXTRA_HASHTAG)
|
||||||
R.string.title_bookmarks
|
|
||||||
|
val title = when (kind) {
|
||||||
|
Kind.FAVOURITES -> getString(R.string.title_favourites)
|
||||||
|
Kind.BOOKMARKS -> getString(R.string.title_bookmarks)
|
||||||
|
Kind.TAG -> getString(R.string.title_tag).format(hashtag)
|
||||||
|
else -> getString(R.string.title_list_timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
|
@ -53,9 +55,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
supportFragmentManager.commit {
|
if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
|
||||||
val fragment = TimelineFragment.newInstance(kind)
|
supportFragmentManager.commit {
|
||||||
replace(R.id.fragment_container, fragment)
|
val fragment = if (kind == Kind.TAG) {
|
||||||
|
TimelineFragment.newHashtagInstance(listOf(hashtag!!))
|
||||||
|
} else {
|
||||||
|
TimelineFragment.newInstance(kind, listId)
|
||||||
|
}
|
||||||
|
replace(R.id.fragmentContainer, fragment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,17 +72,30 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EXTRA_KIND = "kind"
|
private const val EXTRA_KIND = "kind"
|
||||||
|
private const val EXTRA_LIST_ID = "id"
|
||||||
|
private const val EXTRA_HASHTAG = "tag"
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun newFavouritesIntent(context: Context) =
|
fun newFavouritesIntent(context: Context) =
|
||||||
Intent(context, StatusListActivity::class.java).apply {
|
Intent(context, StatusListActivity::class.java).apply {
|
||||||
putExtra(EXTRA_KIND, Kind.FAVOURITES.name)
|
putExtra(EXTRA_KIND, Kind.FAVOURITES.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun newBookmarksIntent(context: Context) =
|
fun newBookmarksIntent(context: Context) =
|
||||||
Intent(context, StatusListActivity::class.java).apply {
|
Intent(context, StatusListActivity::class.java).apply {
|
||||||
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newListIntent(context: Context, listId: String) =
|
||||||
|
Intent(context, StatusListActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_KIND, Kind.LIST.name)
|
||||||
|
putExtra(EXTRA_LIST_ID, listId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun newHashtagIntent(context: Context, hashtag: String) =
|
||||||
|
Intent(context, StatusListActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_KIND, Kind.TAG.name)
|
||||||
|
putExtra(EXTRA_HASHTAG, hashtag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
@ -205,10 +206,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
|
|
||||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
val request = DownloadManager.Request(Uri.parse(url))
|
||||||
request.setDestinationInExternalPublicDir(
|
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
|
||||||
Environment.DIRECTORY_PICTURES,
|
|
||||||
getString(R.string.app_name) + "/" + filename
|
|
||||||
)
|
|
||||||
downloadManager.enqueue(request)
|
downloadManager.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,11 +253,11 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareFile(file: File, mimeType: String?) {
|
private fun shareFile(file: File, mimeType: String?) {
|
||||||
val sendIntent = Intent()
|
ShareCompat.IntentBuilder(this)
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
.setType(mimeType)
|
||||||
sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
|
.addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
|
||||||
sendIntent.type = mimeType
|
.setChooserTitle(R.string.send_media_to)
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
|
.startChooser()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isCreating: Boolean = false
|
private var isCreating: Boolean = false
|
||||||
|
|
|
@ -1,79 +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.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import dagger.android.AndroidInjector;
|
|
||||||
import dagger.android.DispatchingAndroidInjector;
|
|
||||||
import dagger.android.HasAndroidInjector;
|
|
||||||
|
|
||||||
public class ViewTagActivity extends BottomSheetActivity implements HasAndroidInjector {
|
|
||||||
|
|
||||||
private static final String HASHTAG = "hashtag";
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public DispatchingAndroidInjector<Object> dispatchingAndroidInjector;
|
|
||||||
|
|
||||||
public static Intent getIntent(Context context, String tag){
|
|
||||||
Intent intent = new Intent(context,ViewTagActivity.class);
|
|
||||||
intent.putExtra(HASHTAG,tag);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_view_tag);
|
|
||||||
|
|
||||||
String hashtag = getIntent().getStringExtra(HASHTAG);
|
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
ActionBar bar = getSupportActionBar();
|
|
||||||
|
|
||||||
if (bar != null) {
|
|
||||||
bar.setTitle(String.format(getString(R.string.title_tag), hashtag));
|
|
||||||
bar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
bar.setDisplayShowHomeEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
|
||||||
Fragment fragment = TimelineFragment.newHashtagInstance(Collections.singletonList(hashtag));
|
|
||||||
fragmentTransaction.replace(R.id.fragment_container, fragment);
|
|
||||||
fragmentTransaction.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AndroidInjector<Object> androidInjector() {
|
|
||||||
return dispatchingAndroidInjector;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -111,7 +111,7 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasAndroi
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.action_open_in_web: {
|
case R.id.action_open_in_web: {
|
||||||
LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this);
|
openLink(getIntent().getStringExtra(URL_EXTRA));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case R.id.action_reveal: {
|
case R.id.action_reveal: {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.removeDuplicates
|
import com.keylesspalace.tusky.util.removeDuplicates
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
protected val animateAvatar: Boolean,
|
protected val animateAvatar: Boolean,
|
||||||
protected val animateEmojis: Boolean
|
protected val animateEmojis: Boolean
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
|
||||||
var accountList = mutableListOf<Account>()
|
var accountList = mutableListOf<TimelineAccount>()
|
||||||
private var bottomLoading: Boolean = false
|
private var bottomLoading: Boolean = false
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
|
@ -73,12 +73,12 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(newAccounts: List<Account>) {
|
fun update(newAccounts: List<TimelineAccount>) {
|
||||||
accountList = removeDuplicates(newAccounts)
|
accountList = removeDuplicates(newAccounts)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addItems(newAccounts: List<Account>) {
|
fun addItems(newAccounts: List<TimelineAccount>) {
|
||||||
val end = accountList.size
|
val end = accountList.size
|
||||||
val last = accountList[end - 1]
|
val last = accountList[end - 1]
|
||||||
if (newAccounts.none { it.id == last.id }) {
|
if (newAccounts.none { it.id == last.id }) {
|
||||||
|
@ -100,7 +100,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeItem(position: Int): Account? {
|
fun removeItem(position: Int): TimelineAccount? {
|
||||||
if (position < 0 || position >= accountList.size) {
|
if (position < 0 || position >= accountList.size) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
return account
|
return account
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addItem(account: Account, position: Int) {
|
fun addItem(account: TimelineAccount, position: Int) {
|
||||||
if (position < 0 || position > accountList.size) {
|
if (position < 0 || position > accountList.size) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
|
@ -33,9 +33,9 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
|
||||||
showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true);
|
showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
|
public void setupWithAccount(TimelineAccount account, boolean animateAvatar, boolean animateEmojis) {
|
||||||
accountId = account.getId();
|
accountId = account.getId();
|
||||||
String format = username.getContext().getString(R.string.status_username_format);
|
String format = username.getContext().getString(R.string.post_username_format);
|
||||||
String formattedUsername = String.format(format, account.getUsername());
|
String formattedUsername = String.format(format, account.getUsername());
|
||||||
username.setText(formattedUsername);
|
username.setText(formattedUsername);
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
||||||
|
|
|
@ -22,7 +22,7 @@ import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
@ -55,11 +55,11 @@ class BlocksAdapter(
|
||||||
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
|
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
|
||||||
private var id: String? = null
|
private var id: String? = null
|
||||||
|
|
||||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||||
id = account.id
|
id = account.id
|
||||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||||
displayName.text = emojifiedName
|
displayName.text = emojifiedName
|
||||||
val format = username.context.getString(R.string.status_username_format)
|
val format = username.context.getString(R.string.post_username_format)
|
||||||
val formattedUsername = String.format(format, account.username)
|
val formattedUsername = String.format(format, account.username)
|
||||||
username.text = formattedUsername
|
username.text = formattedUsername
|
||||||
val avatarRadius = avatar.context.resources
|
val avatarRadius = avatar.context.resources
|
||||||
|
|
|
@ -22,7 +22,7 @@ import android.text.style.StyleSpan
|
||||||
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.ItemFollowRequestBinding
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
@ -34,7 +34,7 @@ class FollowRequestViewHolder(
|
||||||
private val showHeader: Boolean
|
private val showHeader: Boolean
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||||
val wrappedName = account.name.unicodeWrap()
|
val wrappedName = account.name.unicodeWrap()
|
||||||
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
|
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
|
||||||
binding.displayNameTextView.text = emojifiedName
|
binding.displayNameTextView.text = emojifiedName
|
||||||
|
@ -45,7 +45,7 @@ class FollowRequestViewHolder(
|
||||||
}.emojify(account.emojis, itemView, animateEmojis)
|
}.emojify(account.emojis, itemView, animateEmojis)
|
||||||
}
|
}
|
||||||
binding.notificationTextView.visible(showHeader)
|
binding.notificationTextView.visible(showHeader)
|
||||||
val format = itemView.context.getString(R.string.status_username_format)
|
val format = itemView.context.getString(R.string.post_username_format)
|
||||||
val formattedUsername = String.format(format, account.username)
|
val formattedUsername = String.format(format, account.username)
|
||||||
binding.usernameTextView.text = formattedUsername
|
binding.usernameTextView.text = formattedUsername
|
||||||
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import android.widget.TextView
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
@ -69,7 +69,7 @@ class MutesAdapter(
|
||||||
private var notifications = false
|
private var notifications = false
|
||||||
|
|
||||||
fun setupWithAccount(
|
fun setupWithAccount(
|
||||||
account: Account,
|
account: TimelineAccount,
|
||||||
mutingNotifications: Boolean?,
|
mutingNotifications: Boolean?,
|
||||||
animateAvatar: Boolean,
|
animateAvatar: Boolean,
|
||||||
animateEmojis: Boolean
|
animateEmojis: Boolean
|
||||||
|
@ -77,7 +77,7 @@ class MutesAdapter(
|
||||||
id = account.id
|
id = account.id
|
||||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||||
displayName.text = emojifiedName
|
displayName.text = emojifiedName
|
||||||
val format = username.context.getString(R.string.status_username_format)
|
val format = username.context.getString(R.string.post_username_format)
|
||||||
val formattedUsername = String.format(format, account.username)
|
val formattedUsername = String.format(format, account.username)
|
||||||
username.text = formattedUsername
|
username.text = formattedUsername
|
||||||
val avatarRadius = avatar.context.resources
|
val avatarRadius = avatar.context.resources
|
||||||
|
|
|
@ -40,10 +40,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
@ -335,7 +335,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
this.statusDisplayOptions = statusDisplayOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMessage(Account account) {
|
void setMessage(TimelineAccount account) {
|
||||||
Context context = message.getContext();
|
Context context = message.getContext();
|
||||||
|
|
||||||
String format = context.getString(R.string.notification_follow_format);
|
String format = context.getString(R.string.notification_follow_format);
|
||||||
|
@ -346,7 +346,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
);
|
);
|
||||||
message.setText(emojifiedMessage);
|
message.setText(emojifiedMessage);
|
||||||
|
|
||||||
String username = context.getString(R.string.status_username_format, account.getUsername());
|
String username = context.getString(R.string.post_username_format, account.getUsername());
|
||||||
usernameView.setText(username);
|
usernameView.setText(username);
|
||||||
|
|
||||||
CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(
|
CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(
|
||||||
|
@ -440,7 +440,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
private void setUsername(String name) {
|
private void setUsername(String name) {
|
||||||
Context context = username.getContext();
|
Context context = username.getContext();
|
||||||
String format = context.getString(R.string.status_username_format);
|
String format = context.getString(R.string.post_username_format);
|
||||||
String usernameText = String.format(format, name);
|
String usernameText = String.format(format, name);
|
||||||
username.setText(usernameText);
|
username.setText(usernameText);
|
||||||
}
|
}
|
||||||
|
@ -538,9 +538,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||||
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||||
if (statusViewData.isExpanded()) {
|
if (statusViewData.isExpanded()) {
|
||||||
contentWarningButton.setText(R.string.status_content_warning_show_less);
|
contentWarningButton.setText(R.string.post_content_warning_show_less);
|
||||||
} else {
|
} else {
|
||||||
contentWarningButton.setText(R.string.status_content_warning_show_more);
|
contentWarningButton.setText(R.string.post_content_warning_show_more);
|
||||||
}
|
}
|
||||||
|
|
||||||
contentWarningButton.setOnClickListener(view -> {
|
contentWarningButton.setOnClickListener(view -> {
|
||||||
|
@ -630,10 +630,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||||
if (statusViewData.isCollapsed()) {
|
if (statusViewData.isCollapsed()) {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_more);
|
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
||||||
statusContent.setFilters(COLLAPSE_INPUT_FILTER);
|
statusContent.setFilters(COLLAPSE_INPUT_FILTER);
|
||||||
} else {
|
} else {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||||
statusContent.setFilters(NO_INPUT_FILTER);
|
statusContent.setFilters(NO_INPUT_FILTER);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -644,7 +644,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||||
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
|
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), listener);
|
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), statusViewData.getActionable().getTags(), listener);
|
||||||
|
|
||||||
CharSequence emojifiedContentWarning;
|
CharSequence emojifiedContentWarning;
|
||||||
if (statusViewData.getSpoilerText() != null) {
|
if (statusViewData.getSpoilerText() != null) {
|
||||||
|
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.entity.Attachment.Focus;
|
||||||
import com.keylesspalace.tusky.entity.Attachment.MetaData;
|
import com.keylesspalace.tusky.entity.Attachment.MetaData;
|
||||||
import com.keylesspalace.tusky.entity.Card;
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
|
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.CardViewMode;
|
import com.keylesspalace.tusky.util.CardViewMode;
|
||||||
|
@ -190,7 +191,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
protected void setUsername(String name) {
|
protected void setUsername(String name) {
|
||||||
Context context = username.getContext();
|
Context context = username.getContext();
|
||||||
String usernameText = context.getString(R.string.status_username_format, name);
|
String usernameText = context.getString(R.string.post_username_format, name);
|
||||||
username.setText(usernameText);
|
username.setText(usernameText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +203,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
@NonNull Spanned content,
|
@NonNull Spanned content,
|
||||||
@Nullable String spoilerText,
|
@Nullable String spoilerText,
|
||||||
@Nullable List<Status.Mention> mentions,
|
@Nullable List<Status.Mention> mentions,
|
||||||
|
@Nullable List<HashTag> tags,
|
||||||
@NonNull List<Emoji> emojis,
|
@NonNull List<Emoji> emojis,
|
||||||
@Nullable PollViewData poll,
|
@Nullable PollViewData poll,
|
||||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||||
|
@ -222,21 +224,21 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
setContentWarningButtonText(!expanded);
|
setContentWarningButtonText(!expanded);
|
||||||
|
|
||||||
this.setTextVisible(sensitive, !expanded, content, mentions, emojis, poll, statusDisplayOptions, listener);
|
this.setTextVisible(sensitive, !expanded, content, mentions, tags, emojis, poll, statusDisplayOptions, listener);
|
||||||
});
|
});
|
||||||
this.setTextVisible(sensitive, expanded, content, mentions, emojis, poll, statusDisplayOptions, listener);
|
this.setTextVisible(sensitive, expanded, content, mentions, tags, emojis, poll, statusDisplayOptions, listener);
|
||||||
} else {
|
} else {
|
||||||
contentWarningDescription.setVisibility(View.GONE);
|
contentWarningDescription.setVisibility(View.GONE);
|
||||||
contentWarningButton.setVisibility(View.GONE);
|
contentWarningButton.setVisibility(View.GONE);
|
||||||
this.setTextVisible(sensitive, true, content, mentions, emojis, poll, statusDisplayOptions, listener);
|
this.setTextVisible(sensitive, true, content, mentions, tags, emojis, poll, statusDisplayOptions, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setContentWarningButtonText(boolean expanded) {
|
private void setContentWarningButtonText(boolean expanded) {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
contentWarningButton.setText(R.string.status_content_warning_show_less);
|
contentWarningButton.setText(R.string.post_content_warning_show_less);
|
||||||
} else {
|
} else {
|
||||||
contentWarningButton.setText(R.string.status_content_warning_show_more);
|
contentWarningButton.setText(R.string.post_content_warning_show_more);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,13 +246,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
boolean expanded,
|
boolean expanded,
|
||||||
Spanned content,
|
Spanned content,
|
||||||
List<Status.Mention> mentions,
|
List<Status.Mention> mentions,
|
||||||
|
List<HashTag> tags,
|
||||||
List<Emoji> emojis,
|
List<Emoji> emojis,
|
||||||
@Nullable PollViewData poll,
|
@Nullable PollViewData poll,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
final StatusActionListener listener) {
|
final StatusActionListener listener) {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
|
||||||
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener);
|
LinkHelper.setClickableText(this.content, emojifiedText, mentions, tags, listener);
|
||||||
for (int i = 0; i < mediaLabels.length; ++i) {
|
for (int i = 0; i < mediaLabels.length; ++i) {
|
||||||
updateMediaLabel(i, sensitive, expanded);
|
updateMediaLabel(i, sensitive, expanded);
|
||||||
}
|
}
|
||||||
|
@ -505,9 +508,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
sensitiveMediaWarning.setText(R.string.status_sensitive_media_title);
|
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
|
||||||
} else {
|
} else {
|
||||||
sensitiveMediaWarning.setText(R.string.status_media_hidden_title);
|
sensitiveMediaWarning.setText(R.string.post_media_hidden_title);
|
||||||
}
|
}
|
||||||
|
|
||||||
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
|
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
|
||||||
|
@ -552,7 +555,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) {
|
private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) {
|
||||||
Context context = itemView.getContext();
|
Context context = itemView.getContext();
|
||||||
CharSequence label = (sensitive && !showingContent) ?
|
CharSequence label = (sensitive && !showingContent) ?
|
||||||
context.getString(R.string.status_sensitive_media_title) :
|
context.getString(R.string.post_sensitive_media_title) :
|
||||||
mediaDescriptions[index];
|
mediaDescriptions[index];
|
||||||
mediaLabels[index].setText(label);
|
mediaLabels[index].setText(label);
|
||||||
}
|
}
|
||||||
|
@ -604,7 +607,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
duration = formatDuration(attachment.getMeta().getDuration()) + " ";
|
duration = formatDuration(attachment.getMeta().getDuration()) + " ";
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(attachment.getDescription())) {
|
if (TextUtils.isEmpty(attachment.getDescription())) {
|
||||||
return duration + context.getString(R.string.description_status_media_no_description_placeholder);
|
return duration + context.getString(R.string.description_post_media_no_description_placeholder);
|
||||||
} else {
|
} else {
|
||||||
return duration + attachment.getDescription();
|
return duration + attachment.getDescription();
|
||||||
}
|
}
|
||||||
|
@ -739,7 +742,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
if (payloads == null) {
|
if (payloads == null) {
|
||||||
Status actionable = status.getActionable();
|
Status actionable = status.getActionable();
|
||||||
setDisplayName(actionable.getAccount().getDisplayName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
|
setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
|
||||||
setUsername(status.getUsername());
|
setUsername(status.getUsername());
|
||||||
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
|
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
|
||||||
setIsReply(actionable.getInReplyToId() != null);
|
setIsReply(actionable.getInReplyToId() != null);
|
||||||
|
@ -779,7 +782,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
||||||
|
|
||||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(),
|
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(),
|
||||||
actionable.getMentions(), actionable.getEmojis(),
|
actionable.getMentions(), actionable.getTags(), actionable.getEmojis(),
|
||||||
PollViewDataKt.toViewData(actionable.getPoll()), statusDisplayOptions,
|
PollViewDataKt.toViewData(actionable.getPoll()), statusDisplayOptions,
|
||||||
listener);
|
listener);
|
||||||
|
|
||||||
|
@ -823,9 +826,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
||||||
getReblogDescription(context, status),
|
getReblogDescription(context, status),
|
||||||
status.getUsername(),
|
status.getUsername(),
|
||||||
actionable.getReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",
|
||||||
actionable.getFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
actionable.getFavourited() ? context.getString(R.string.description_post_favourited) : "",
|
||||||
actionable.getBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
actionable.getBookmarked() ? context.getString(R.string.description_post_bookmarked) : "",
|
||||||
getMediaDescription(context, status),
|
getMediaDescription(context, status),
|
||||||
getVisibilityDescription(context, actionable.getVisibility()),
|
getVisibilityDescription(context, actionable.getVisibility()),
|
||||||
getFavsText(context, actionable.getFavouritesCount()),
|
getFavsText(context, actionable.getFavouritesCount()),
|
||||||
|
@ -840,7 +843,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
Status reblog = status.getRebloggingStatus();
|
Status reblog = status.getRebloggingStatus();
|
||||||
if (reblog != null) {
|
if (reblog != null) {
|
||||||
return context
|
return context
|
||||||
.getString(R.string.status_boosted_format, reblog.getAccount().getUsername());
|
.getString(R.string.post_boosted_format, reblog.getAccount().getUsername());
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -857,20 +860,20 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
(builder, a) -> {
|
(builder, a) -> {
|
||||||
if (a.getDescription() == null) {
|
if (a.getDescription() == null) {
|
||||||
String placeholder =
|
String placeholder =
|
||||||
context.getString(R.string.description_status_media_no_description_placeholder);
|
context.getString(R.string.description_post_media_no_description_placeholder);
|
||||||
return builder.append(placeholder);
|
return builder.append(placeholder);
|
||||||
} else {
|
} else {
|
||||||
builder.append("; ");
|
builder.append("; ");
|
||||||
return builder.append(a.getDescription());
|
return builder.append(a.getDescription());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return context.getString(R.string.description_status_media, mediaDescriptions);
|
return context.getString(R.string.description_post_media, mediaDescriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharSequence getContentWarningDescription(Context context,
|
private static CharSequence getContentWarningDescription(Context context,
|
||||||
@NonNull StatusViewData.Concrete status) {
|
@NonNull StatusViewData.Concrete status) {
|
||||||
if (!TextUtils.isEmpty(status.getSpoilerText())) {
|
if (!TextUtils.isEmpty(status.getSpoilerText())) {
|
||||||
return context.getString(R.string.description_status_cw, status.getSpoilerText());
|
return context.getString(R.string.description_post_cw, status.getSpoilerText());
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
if (reblogging == null) {
|
if (reblogging == null) {
|
||||||
hideStatusInfo();
|
hideStatusInfo();
|
||||||
} else {
|
} else {
|
||||||
String rebloggedByDisplayName = reblogging.getAccount().getDisplayName();
|
String rebloggedByDisplayName = reblogging.getAccount().getName();
|
||||||
setRebloggedByDisplayName(rebloggedByDisplayName,
|
setRebloggedByDisplayName(rebloggedByDisplayName,
|
||||||
reblogging.getAccount().getEmojis(), statusDisplayOptions);
|
reblogging.getAccount().getEmojis(), statusDisplayOptions);
|
||||||
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
|
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
|
||||||
|
@ -86,7 +86,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
final StatusDisplayOptions statusDisplayOptions) {
|
final StatusDisplayOptions statusDisplayOptions) {
|
||||||
Context context = statusInfo.getContext();
|
Context context = statusInfo.getContext();
|
||||||
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
||||||
CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName);
|
CharSequence boostedText = context.getString(R.string.post_boosted_format, wrappedName);
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||||
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
|
@ -118,10 +118,10 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||||
if (status.isCollapsed()) {
|
if (status.isCollapsed()) {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_more);
|
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
||||||
content.setFilters(COLLAPSE_INPUT_FILTER);
|
content.setFilters(COLLAPSE_INPUT_FILTER);
|
||||||
} else {
|
} else {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||||
content.setFilters(NO_INPUT_FILTER);
|
content.setFilters(NO_INPUT_FILTER);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,9 +3,7 @@ package com.keylesspalace.tusky.appstore
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CacheUpdater @Inject constructor(
|
class CacheUpdater @Inject constructor(
|
||||||
|
@ -47,12 +45,7 @@ class CacheUpdater @Inject constructor(
|
||||||
this.disposable.dispose()
|
this.disposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearForUser(accountId: Long) {
|
suspend fun clearForUser(accountId: Long) {
|
||||||
Single.fromCallable {
|
appDatabase.timelineDao().removeAll(accountId)
|
||||||
appDatabase.timelineDao().removeAllForAccount(accountId)
|
|
||||||
appDatabase.timelineDao().removeAllUsersForAccount(accountId)
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,27 +55,31 @@ import com.keylesspalace.tusky.AccountListActivity
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity
|
import com.keylesspalace.tusky.BottomSheetActivity
|
||||||
import com.keylesspalace.tusky.EditProfileActivity
|
import com.keylesspalace.tusky.EditProfileActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
import com.keylesspalace.tusky.ViewTagActivity
|
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
|
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.DefaultTextWatcher
|
import com.keylesspalace.tusky.util.DefaultTextWatcher
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.getDomain
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
import com.keylesspalace.tusky.util.openLink
|
||||||
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
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
|
||||||
|
@ -230,7 +234,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
binding.accountFragmentViewPager.adapter = adapter
|
binding.accountFragmentViewPager.adapter = adapter
|
||||||
binding.accountFragmentViewPager.offscreenPageLimit = 2
|
binding.accountFragmentViewPager.offscreenPageLimit = 2
|
||||||
|
|
||||||
val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media))
|
val pageTitles = arrayOf(getString(R.string.title_posts), getString(R.string.title_posts_with_replies), getString(R.string.title_posts_pinned), getString(R.string.title_media))
|
||||||
|
|
||||||
TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position ->
|
TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position ->
|
||||||
tab.text = pageTitles[position]
|
tab.text = pageTitles[position]
|
||||||
|
@ -402,12 +406,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
private fun onAccountChanged(account: Account?) {
|
private fun onAccountChanged(account: Account?) {
|
||||||
loadedAccount = account ?: return
|
loadedAccount = account ?: return
|
||||||
|
|
||||||
val usernameFormatted = getString(R.string.status_username_format, account.username)
|
val usernameFormatted = getString(R.string.post_username_format, account.username)
|
||||||
binding.accountUsernameTextView.text = usernameFormatted
|
binding.accountUsernameTextView.text = usernameFormatted
|
||||||
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
||||||
|
|
||||||
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||||
LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this)
|
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||||
|
|
||||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||||
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
||||||
|
@ -473,7 +477,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
supportActionBar?.title = emojifiedName
|
supportActionBar?.title = emojifiedName
|
||||||
}
|
}
|
||||||
supportActionBar?.subtitle = String.format(getString(R.string.status_username_format), account.username)
|
supportActionBar?.subtitle = String.format(getString(R.string.post_username_format), account.username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,7 +494,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.accountMovedDisplayName.text = movedAccount.name
|
binding.accountMovedDisplayName.text = movedAccount.name
|
||||||
binding.accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username)
|
binding.accountMovedUsername.text = getString(R.string.post_username_format, movedAccount.username)
|
||||||
|
|
||||||
val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||||
|
|
||||||
|
@ -515,7 +519,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
if (account.isRemote()) {
|
if (account.isRemote()) {
|
||||||
binding.accountRemoveView.show()
|
binding.accountRemoveView.show()
|
||||||
binding.accountRemoveView.setOnClickListener {
|
binding.accountRemoveView.setOnClickListener {
|
||||||
LinkHelper.openLink(account.url, this)
|
openLink(account.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -686,6 +690,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.account_toolbar, menu)
|
menuInflater.inflate(R.menu.account_toolbar, menu)
|
||||||
|
|
||||||
|
val openAsItem = menu.findItem(R.id.action_open_as)
|
||||||
|
val title = openAsText
|
||||||
|
if (title == null) {
|
||||||
|
openAsItem.isVisible = false
|
||||||
|
} else {
|
||||||
|
openAsItem.title = title
|
||||||
|
}
|
||||||
|
|
||||||
if (!viewModel.isSelf) {
|
if (!viewModel.isSelf) {
|
||||||
|
|
||||||
val block = menu.findItem(R.id.action_block)
|
val block = menu.findItem(R.id.action_block)
|
||||||
|
@ -704,7 +716,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
|
|
||||||
if (loadedAccount != null) {
|
if (loadedAccount != null) {
|
||||||
val muteDomain = menu.findItem(R.id.action_mute_domain)
|
val muteDomain = menu.findItem(R.id.action_mute_domain)
|
||||||
domain = LinkHelper.getDomain(loadedAccount?.url)
|
domain = getDomain(loadedAccount?.url)
|
||||||
if (domain.isEmpty()) {
|
if (domain.isEmpty()) {
|
||||||
// If we can't get the domain, there's no way we can mute it anyway...
|
// If we can't get the domain, there's no way we can mute it anyway...
|
||||||
menu.removeItem(R.id.action_mute_domain)
|
menu.removeItem(R.id.action_mute_domain)
|
||||||
|
@ -805,8 +817,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
val intent = Intent(this, ViewTagActivity::class.java)
|
val intent = StatusListActivity.newHashtagIntent(this, tag)
|
||||||
intent.putExtra("hashtag", tag)
|
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithSlideInAnimation(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -824,11 +835,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_open_in_web -> {
|
R.id.action_open_in_web -> {
|
||||||
// If the account isn't loaded yet, eat the input.
|
// If the account isn't loaded yet, eat the input.
|
||||||
if (loadedAccount != null) {
|
if (loadedAccount?.url != null) {
|
||||||
LinkHelper.openLink(loadedAccount?.url, this)
|
openLink(loadedAccount!!.url)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.action_open_as -> {
|
||||||
|
if (loadedAccount != null) {
|
||||||
|
showAccountChooserDialog(
|
||||||
|
item.title, false,
|
||||||
|
object : AccountSelectionListener {
|
||||||
|
override fun onAccountSelected(account: AccountEntity) {
|
||||||
|
openAsAccount(loadedAccount!!.url, account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
R.id.action_block -> {
|
R.id.action_block -> {
|
||||||
toggleBlock()
|
toggleBlock()
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -27,8 +27,9 @@ import com.keylesspalace.tusky.entity.IdentityProof
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.Either
|
import com.keylesspalace.tusky.util.Either
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
import com.keylesspalace.tusky.util.createClickableText
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
|
|
||||||
class AccountFieldAdapter(
|
class AccountFieldAdapter(
|
||||||
private val linkListener: LinkListener,
|
private val linkListener: LinkListener,
|
||||||
|
@ -54,7 +55,7 @@ class AccountFieldAdapter(
|
||||||
val identityProof = proofOrField.asLeft()
|
val identityProof = proofOrField.asLeft()
|
||||||
|
|
||||||
nameTextView.text = identityProof.provider
|
nameTextView.text = identityProof.provider
|
||||||
valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl)
|
valueTextView.text = createClickableText(identityProof.username, identityProof.profileUrl)
|
||||||
|
|
||||||
valueTextView.movementMethod = LinkMovementMethod.getInstance()
|
valueTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ class AccountFieldAdapter(
|
||||||
nameTextView.text = emojifiedName
|
nameTextView.text = emojifiedName
|
||||||
|
|
||||||
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
||||||
LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener)
|
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
||||||
|
|
||||||
if (field.verifiedAt != null) {
|
if (field.verifiedAt != null) {
|
||||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||||
|
|
|
@ -37,9 +37,9 @@ import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Status
|
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.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
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.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.view.SquareImageView
|
||||||
|
@ -252,7 +252,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Attachment.Type.UNKNOWN -> {
|
Attachment.Type.UNKNOWN -> {
|
||||||
LinkHelper.openLink(items[currentIndex].attachment.url, context)
|
context?.openLink(items[currentIndex].attachment.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ import com.keylesspalace.tusky.entity.Announcement
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.EmojiSpan
|
import com.keylesspalace.tusky.util.EmojiSpan
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
interface AnnouncementActionListener : LinkListener {
|
interface AnnouncementActionListener : LinkListener {
|
||||||
|
@ -62,7 +62,7 @@ class AnnouncementAdapter(
|
||||||
|
|
||||||
val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis)
|
val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis)
|
||||||
|
|
||||||
LinkHelper.setClickableText(text, emojifiedText, item.mentions, listener)
|
setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
|
||||||
|
|
||||||
// If wellbeing mode is enabled, announcement badge counts should not be shown.
|
// If wellbeing mode is enabled, announcement badge counts should not be shown.
|
||||||
if (wellbeingEnabled) {
|
if (wellbeingEnabled) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity
|
import com.keylesspalace.tusky.BottomSheetActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.ViewTagActivity
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
||||||
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
||||||
|
@ -152,22 +152,17 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
|
||||||
viewModel.removeReaction(announcementId, name)
|
viewModel.removeReaction(announcementId, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String?) {
|
override fun onViewTag(tag: String) {
|
||||||
val intent = Intent(this, ViewTagActivity::class.java)
|
val intent = StatusListActivity.newHashtagIntent(this, tag)
|
||||||
intent.putExtra("hashtag", tag)
|
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithSlideInAnimation(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String?) {
|
override fun onViewAccount(id: String) {
|
||||||
if (id != null) {
|
viewAccount(id)
|
||||||
viewAccount(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewUrl(url: String?) {
|
override fun onViewUrl(url: String) {
|
||||||
if (url != null) {
|
viewUrl(url)
|
||||||
viewUrl(url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.InstanceEntity
|
import com.keylesspalace.tusky.db.InstanceEntity
|
||||||
|
@ -57,19 +58,21 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
.onErrorResumeNext {
|
.onErrorResumeNext {
|
||||||
mastodonApi.getInstance()
|
mastodonApi.getInstance()
|
||||||
.map { Either.Right(it) }
|
.map { Either.Right(it) }
|
||||||
},
|
}
|
||||||
{ emojis, either ->
|
) { emojis, either ->
|
||||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||||
?: InstanceEntity(
|
?: InstanceEntity(
|
||||||
accountManager.activeAccount?.domain!!,
|
accountManager.activeAccount?.domain!!,
|
||||||
emojis,
|
emojis,
|
||||||
either.asRight().maxTootChars,
|
either.asRight().configuration?.statuses?.maxCharacters ?: either.asRight().maxTootChars,
|
||||||
either.asRight().pollLimits?.maxOptions,
|
either.asRight().configuration?.polls?.maxOptions ?: either.asRight().pollConfiguration?.maxOptions,
|
||||||
either.asRight().pollLimits?.maxOptionChars,
|
either.asRight().configuration?.polls?.maxCharactersPerOption ?: either.asRight().pollConfiguration?.maxOptionChars,
|
||||||
either.asRight().version
|
either.asRight().configuration?.polls?.minExpiration ?: either.asRight().pollConfiguration?.minExpiration,
|
||||||
)
|
either.asRight().configuration?.polls?.maxExpiration ?: either.asRight().pollConfiguration?.maxExpiration,
|
||||||
}
|
either.asRight().configuration?.statuses?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH,
|
||||||
)
|
either.asRight().version
|
||||||
|
)
|
||||||
|
}
|
||||||
.doOnSuccess {
|
.doOnSuccess {
|
||||||
appDatabase.instanceDao().insertOrReplace(it)
|
appDatabase.instanceDao().insertOrReplace(it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
package com.keylesspalace.tusky.components.compose
|
package com.keylesspalace.tusky.components.compose
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
@ -45,8 +47,8 @@ import androidx.appcompat.app.AlertDialog
|
||||||
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.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.ContentInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputContentInfoCompat
|
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.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
@ -105,7 +107,7 @@ class ComposeActivity :
|
||||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
Injectable,
|
Injectable,
|
||||||
InputConnectionCompat.OnCommitContentListener,
|
OnReceiveContentListener,
|
||||||
ComposeScheduleView.OnTimeSetListener {
|
ComposeScheduleView.OnTimeSetListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -122,6 +124,7 @@ class ComposeActivity :
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
|
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
|
||||||
|
var charactersReservedPerUrl = DEFAULT_MAXIMUM_URL_LENGTH
|
||||||
|
|
||||||
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
|
@ -148,6 +151,18 @@ class ComposeActivity :
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1)
|
||||||
|
if (notificationId != -1) {
|
||||||
|
// ComposeActivity was opened from a notification, delete the notification
|
||||||
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.cancel(notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountId = intent.getLongExtra(ACCOUNT_ID_EXTRA, -1)
|
||||||
|
if (accountId != -1L) {
|
||||||
|
accountManager.setActiveAccount(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||||
if (theme == "black") {
|
if (theme == "black") {
|
||||||
|
@ -186,9 +201,9 @@ class ComposeActivity :
|
||||||
|
|
||||||
viewModel.setup(composeOptions)
|
viewModel.setup(composeOptions)
|
||||||
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
||||||
val tootText = composeOptions?.tootText
|
val statusContent = composeOptions?.content
|
||||||
if (!tootText.isNullOrEmpty()) {
|
if (!statusContent.isNullOrEmpty()) {
|
||||||
binding.composeEditField.setText(tootText)
|
binding.composeEditField.setText(statusContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
|
if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
|
||||||
|
@ -221,26 +236,25 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) {
|
}
|
||||||
|
|
||||||
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()
|
val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()
|
||||||
val shareBody = if (!subject.isNullOrBlank() && subject !in text) {
|
val shareBody = if (!subject.isNullOrBlank() && subject !in text) {
|
||||||
subject + '\n' + text
|
subject + '\n' + text
|
||||||
} else {
|
} else {
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shareBody.isNotBlank()) {
|
if (shareBody.isNotBlank()) {
|
||||||
val start = binding.composeEditField.selectionStart.coerceAtLeast(0)
|
val start = binding.composeEditField.selectionStart.coerceAtLeast(0)
|
||||||
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
||||||
val left = min(start, end)
|
val left = min(start, end)
|
||||||
val right = max(start, end)
|
val right = max(start, end)
|
||||||
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
||||||
// move edittext cursor to first when shareBody parsed
|
// move edittext cursor to first when shareBody parsed
|
||||||
binding.composeEditField.text.insert(0, "\n")
|
binding.composeEditField.text.insert(0, "\n")
|
||||||
binding.composeEditField.setSelection(0)
|
binding.composeEditField.setSelection(0)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,7 +295,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
||||||
binding.composeEditField.setOnCommitContentListener(this)
|
binding.composeEditField.setOnReceiveContentListener(this)
|
||||||
|
|
||||||
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
||||||
|
|
||||||
|
@ -316,6 +330,7 @@ class ComposeActivity :
|
||||||
withLifecycleContext {
|
withLifecycleContext {
|
||||||
viewModel.instanceParams.observe { instanceData ->
|
viewModel.instanceParams.observe { instanceData ->
|
||||||
maximumTootCharacters = instanceData.maxChars
|
maximumTootCharacters = instanceData.maxChars
|
||||||
|
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
||||||
updateVisibleCharactersLeft()
|
updateVisibleCharactersLeft()
|
||||||
binding.composeScheduleButton.visible(instanceData.supportsScheduled)
|
binding.composeScheduleButton.visible(instanceData.supportsScheduled)
|
||||||
}
|
}
|
||||||
|
@ -654,7 +669,8 @@ class ComposeActivity :
|
||||||
val instanceParams = viewModel.instanceParams.value!!
|
val instanceParams = viewModel.instanceParams.value!!
|
||||||
showAddPollDialog(
|
showAddPollDialog(
|
||||||
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
||||||
instanceParams.pollMaxLength, viewModel::updatePoll
|
instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration,
|
||||||
|
viewModel::updatePoll
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -699,7 +715,9 @@ class ComposeActivity :
|
||||||
val urlSpans = binding.composeEditField.urls
|
val urlSpans = binding.composeEditField.urls
|
||||||
if (urlSpans != null) {
|
if (urlSpans != null) {
|
||||||
for (span in urlSpans) {
|
for (span in urlSpans) {
|
||||||
offset += max(0, span.url.length - MAXIMUM_URL_LENGTH)
|
// it's expected that this will be negative
|
||||||
|
// when the url length is less than the reserved character count
|
||||||
|
offset += (span.url.length - charactersReservedPerUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var length = binding.composeEditField.length() - offset
|
var length = binding.composeEditField.length() - offset
|
||||||
|
@ -739,26 +757,18 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is for the fancy keyboards which can insert images and stuff. */
|
/** This is for the fancy keyboards which can insert images and stuff, and drag&drop etc */
|
||||||
override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean {
|
override fun onReceiveContent(view: View, contentInfo: ContentInfoCompat): ContentInfoCompat? {
|
||||||
// Verify the returned content's type is of the correct MIME type
|
if (contentInfo.clip.description.hasMimeType("image/*")) {
|
||||||
val supported = inputContentInfo.description.hasMimeType("image/*")
|
val split = contentInfo.partition { item: ClipData.Item -> item.uri != null }
|
||||||
|
split.first?.let { content ->
|
||||||
if (supported) {
|
for (i in 0 until content.clip.itemCount) {
|
||||||
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
pickMedia(content.clip.getItemAt(i).uri)
|
||||||
if (lacksPermission) {
|
|
||||||
try {
|
|
||||||
inputContentInfo.requestPermission()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pickMedia(inputContentInfo.contentUri, inputContentInfo)
|
return split.second
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
return contentInfo
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendStatus() {
|
private fun sendStatus() {
|
||||||
|
@ -781,12 +791,11 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.sendStatus(contentText, spoilerText).observe(
|
viewModel.sendStatus(contentText, spoilerText).observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
finishingUploadDialog?.dismiss()
|
finishingUploadDialog?.dismiss()
|
||||||
deleteDraftAndFinish()
|
deleteDraftAndFinish()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||||
enableButtons(true)
|
enableButtons(true)
|
||||||
|
@ -856,12 +865,9 @@ class ComposeActivity :
|
||||||
viewModel.removeMediaFromQueue(item)
|
viewModel.removeMediaFromQueue(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) {
|
private fun pickMedia(uri: Uri) {
|
||||||
withLifecycleContext {
|
withLifecycleContext {
|
||||||
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
||||||
|
|
||||||
contentInfoCompat?.releasePermission()
|
|
||||||
|
|
||||||
exceptionOrItem.asLeftOrNull()?.let {
|
exceptionOrItem.asLeftOrNull()?.let {
|
||||||
val errorId = when (it) {
|
val errorId = when (it) {
|
||||||
is VideoSizeException -> {
|
is VideoSizeException -> {
|
||||||
|
@ -1017,7 +1023,7 @@ class ComposeActivity :
|
||||||
// Let's keep fields var until all consumers are Kotlin
|
// Let's keep fields var until all consumers are Kotlin
|
||||||
var scheduledTootId: String? = null,
|
var scheduledTootId: String? = null,
|
||||||
var draftId: Int? = null,
|
var draftId: Int? = null,
|
||||||
var tootText: String? = null,
|
var content: String? = null,
|
||||||
var mediaUrls: List<String>? = null,
|
var mediaUrls: List<String>? = null,
|
||||||
var mediaDescriptions: List<String>? = null,
|
var mediaDescriptions: List<String>? = null,
|
||||||
var mentionedUsernames: Set<String>? = null,
|
var mentionedUsernames: Set<String>? = null,
|
||||||
|
@ -1040,16 +1046,32 @@ class ComposeActivity :
|
||||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||||
|
|
||||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||||
|
private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_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"
|
||||||
|
|
||||||
// Mastodon only counts URLs as this long in terms of status character limits
|
/**
|
||||||
@VisibleForTesting
|
* @param options ComposeOptions to configure the ComposeActivity
|
||||||
const val MAXIMUM_URL_LENGTH = 23
|
* @param notificationId the id of the notification that starts the Activity
|
||||||
|
* @param accountId the id of the account to compose with, null for the current account
|
||||||
|
* @return an Intent to start the ComposeActivity
|
||||||
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun startIntent(context: Context, options: ComposeOptions): Intent {
|
@JvmOverloads
|
||||||
|
fun startIntent(
|
||||||
|
context: Context,
|
||||||
|
options: ComposeOptions,
|
||||||
|
notificationId: Int? = null,
|
||||||
|
accountId: Long? = null
|
||||||
|
): Intent {
|
||||||
return Intent(context, ComposeActivity::class.java).apply {
|
return Intent(context, ComposeActivity::class.java).apply {
|
||||||
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
||||||
|
if (notificationId != null) {
|
||||||
|
putExtra(NOTIFICATION_ID_EXTRA, notificationId)
|
||||||
|
}
|
||||||
|
if (accountId != null) {
|
||||||
|
putExtra(ACCOUNT_ID_EXTRA, accountId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
package com.keylesspalace.tusky.components.compose;
|
package com.keylesspalace.tusky.components.compose;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -28,9 +27,9 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.entity.HashTag;
|
import com.keylesspalace.tusky.entity.HashTag;
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
|
|
||||||
|
@ -144,9 +143,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
||||||
|
|
||||||
AccountResult accountResult = ((AccountResult) getItem(position));
|
AccountResult accountResult = ((AccountResult) getItem(position));
|
||||||
if (accountResult != null) {
|
if (accountResult != null) {
|
||||||
Account account = accountResult.account;
|
TimelineAccount account = accountResult.account;
|
||||||
String formattedUsername = context.getString(
|
String formattedUsername = context.getString(
|
||||||
R.string.status_username_format,
|
R.string.post_username_format,
|
||||||
account.getUsername()
|
account.getUsername()
|
||||||
);
|
);
|
||||||
accountViewHolder.username.setText(formattedUsername);
|
accountViewHolder.username.setText(formattedUsername);
|
||||||
|
@ -268,9 +267,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
public final static class AccountResult extends AutocompleteResult {
|
public final static class AccountResult extends AutocompleteResult {
|
||||||
private final Account account;
|
private final TimelineAccount account;
|
||||||
|
|
||||||
public AccountResult(Account account) {
|
public AccountResult(TimelineAccount account) {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.TootToSend
|
import com.keylesspalace.tusky.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.Either
|
import com.keylesspalace.tusky.util.Either
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
import com.keylesspalace.tusky.util.VersionUtils
|
import com.keylesspalace.tusky.util.VersionUtils
|
||||||
|
@ -79,6 +79,9 @@ class ComposeViewModel @Inject constructor(
|
||||||
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
|
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
|
||||||
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
|
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
|
||||||
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
||||||
|
pollMinDuration = instance?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
|
||||||
|
pollMaxDuration = instance?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
|
||||||
|
charactersReservedPerUrl = instance?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH,
|
||||||
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
|
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -102,18 +105,20 @@ class ComposeViewModel @Inject constructor(
|
||||||
init {
|
init {
|
||||||
|
|
||||||
Single.zip(
|
Single.zip(
|
||||||
api.getCustomEmojis(), api.getInstance(),
|
api.getCustomEmojis(), api.getInstance()
|
||||||
{ emojis, instance ->
|
) { emojis, instance ->
|
||||||
InstanceEntity(
|
InstanceEntity(
|
||||||
instance = accountManager.activeAccount?.domain!!,
|
instance = accountManager.activeAccount?.domain!!,
|
||||||
emojiList = emojis,
|
emojiList = emojis,
|
||||||
maximumTootCharacters = instance.maxTootChars,
|
maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars,
|
||||||
maxPollOptions = instance.pollLimits?.maxOptions,
|
maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions,
|
||||||
maxPollOptionLength = instance.pollLimits?.maxOptionChars,
|
maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars,
|
||||||
version = instance.version
|
minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
|
||||||
)
|
maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
|
||||||
}
|
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
|
||||||
)
|
version = instance.version
|
||||||
|
)
|
||||||
|
}
|
||||||
.doOnSuccess {
|
.doOnSuccess {
|
||||||
db.instanceDao().insertOrReplace(it)
|
db.instanceDao().insertOrReplace(it)
|
||||||
}
|
}
|
||||||
|
@ -185,7 +190,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
is UploadEvent.ProgressEvent ->
|
is UploadEvent.ProgressEvent ->
|
||||||
item.copy(uploadPercent = event.percentage)
|
item.copy(uploadPercent = event.percentage)
|
||||||
is UploadEvent.FinishedEvent ->
|
is UploadEvent.FinishedEvent ->
|
||||||
item.copy(id = event.attachment.id, uploadPercent = -1)
|
item.copy(id = event.mediaId, uploadPercent = -1)
|
||||||
}
|
}
|
||||||
synchronized(media) {
|
synchronized(media) {
|
||||||
val mediaValue = media.value!!
|
val mediaValue = media.value!!
|
||||||
|
@ -303,7 +308,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaDescriptions.add(item.description ?: "")
|
mediaDescriptions.add(item.description ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
val tootToSend = TootToSend(
|
val tootToSend = StatusToSend(
|
||||||
text = content,
|
text = content,
|
||||||
warningText = spoilerText,
|
warningText = spoilerText,
|
||||||
visibility = statusVisibility.value!!.serverString(),
|
visibility = statusVisibility.value!!.serverString(),
|
||||||
|
@ -451,7 +456,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
draftId = composeOptions?.draftId ?: 0
|
draftId = composeOptions?.draftId ?: 0
|
||||||
scheduledTootId = composeOptions?.scheduledTootId
|
scheduledTootId = composeOptions?.scheduledTootId
|
||||||
startingText = composeOptions?.tootText
|
startingText = composeOptions?.content
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -506,11 +511,19 @@ fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = defau
|
||||||
const val DEFAULT_CHARACTER_LIMIT = 500
|
const val DEFAULT_CHARACTER_LIMIT = 500
|
||||||
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
||||||
private const val DEFAULT_MAX_OPTION_LENGTH = 50
|
private const val DEFAULT_MAX_OPTION_LENGTH = 50
|
||||||
|
private const val DEFAULT_MIN_POLL_DURATION = 300
|
||||||
|
private const val DEFAULT_MAX_POLL_DURATION = 604800
|
||||||
|
|
||||||
|
// Mastodon only counts URLs as this long in terms of status character limits
|
||||||
|
const val DEFAULT_MAXIMUM_URL_LENGTH = 23
|
||||||
|
|
||||||
data class ComposeInstanceParams(
|
data class ComposeInstanceParams(
|
||||||
val maxChars: Int,
|
val maxChars: Int,
|
||||||
val pollMaxOptions: Int,
|
val pollMaxOptions: Int,
|
||||||
val pollMaxLength: Int,
|
val pollMaxLength: Int,
|
||||||
|
val pollMinDuration: Int,
|
||||||
|
val pollMaxDuration: Int,
|
||||||
|
val charactersReservedPerUrl: Int,
|
||||||
val supportsScheduled: Boolean
|
val supportsScheduled: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.compose
|
package com.keylesspalace.tusky.components.compose
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
@ -25,7 +26,6 @@ import androidx.core.net.toUri
|
||||||
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.entity.Attachment
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||||
|
@ -38,6 +38,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -45,7 +46,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
sealed class UploadEvent {
|
sealed class UploadEvent {
|
||||||
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
||||||
data class FinishedEvent(val attachment: Attachment) : UploadEvent()
|
data class FinishedEvent(val mediaId: String) : UploadEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNewImageFile(context: Context): File {
|
fun createNewImageFile(context: Context): File {
|
||||||
|
@ -84,36 +85,70 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||||
return Single.fromCallable {
|
return Single.fromCallable {
|
||||||
var mediaSize = getMediaSize(contentResolver, inUri)
|
var mediaSize = MEDIA_SIZE_UNKNOWN
|
||||||
var uri = inUri
|
var uri = inUri
|
||||||
val mimeType = contentResolver.getType(uri)
|
var mimeType: String? = null
|
||||||
|
|
||||||
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contentResolver.openInputStream(inUri).use { input ->
|
when (inUri.scheme) {
|
||||||
if (input == null) {
|
ContentResolver.SCHEME_CONTENT -> {
|
||||||
Log.w(TAG, "Media input is null")
|
|
||||||
uri = inUri
|
mimeType = contentResolver.getType(uri)
|
||||||
return@use
|
|
||||||
|
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
||||||
|
|
||||||
|
contentResolver.openInputStream(inUri).use { input ->
|
||||||
|
if (input == null) {
|
||||||
|
Log.w(TAG, "Media input is null")
|
||||||
|
uri = inUri
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
||||||
|
FileOutputStream(file.absoluteFile).use { out ->
|
||||||
|
input.copyTo(out)
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
ContentResolver.SCHEME_FILE -> {
|
||||||
FileOutputStream(file.absoluteFile).use { out ->
|
val path = uri.path
|
||||||
input.copyTo(out)
|
if (path == null) {
|
||||||
uri = FileProvider.getUriForFile(
|
Log.w(TAG, "empty uri path $uri")
|
||||||
context,
|
throw CouldNotOpenFileException()
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
}
|
||||||
file
|
val inputFile = File(path)
|
||||||
)
|
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
||||||
mediaSize = getMediaSize(contentResolver, uri)
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
|
||||||
|
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
|
||||||
|
val input = FileInputStream(inputFile)
|
||||||
|
|
||||||
|
FileOutputStream(file.absoluteFile).use { out ->
|
||||||
|
input.copyTo(out)
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown uri scheme $uri")
|
||||||
|
throw CouldNotOpenFileException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, e)
|
Log.w(TAG, e)
|
||||||
uri = inUri
|
throw CouldNotOpenFileException()
|
||||||
}
|
}
|
||||||
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
||||||
throw CouldNotOpenFileException()
|
Log.w(TAG, "Could not determine file size of upload")
|
||||||
|
throw MediaTypeException()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
|
@ -139,6 +174,7 @@ class MediaUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Log.w(TAG, "Could not determine mime type of upload")
|
||||||
throw MediaTypeException()
|
throw MediaTypeException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,8 +219,8 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ attachment ->
|
{ result ->
|
||||||
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
emitter.onNext(UploadEvent.FinishedEvent(result.id))
|
||||||
emitter.onComplete()
|
emitter.onComplete()
|
||||||
},
|
},
|
||||||
{ e ->
|
{ e ->
|
||||||
|
|
|
@ -20,6 +20,7 @@ package com.keylesspalace.tusky.components.compose.dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.DialogAddPollBinding
|
import com.keylesspalace.tusky.databinding.DialogAddPollBinding
|
||||||
|
@ -30,6 +31,8 @@ fun showAddPollDialog(
|
||||||
poll: NewPoll?,
|
poll: NewPoll?,
|
||||||
maxOptionCount: Int,
|
maxOptionCount: Int,
|
||||||
maxOptionLength: Int,
|
maxOptionLength: Int,
|
||||||
|
minDuration: Int,
|
||||||
|
maxDuration: Int,
|
||||||
onUpdatePoll: (NewPoll) -> Unit
|
onUpdatePoll: (NewPoll) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -57,6 +60,13 @@ fun showAddPollDialog(
|
||||||
|
|
||||||
binding.pollChoices.adapter = adapter
|
binding.pollChoices.adapter = adapter
|
||||||
|
|
||||||
|
var durations = context.resources.getIntArray(R.array.poll_duration_values).toList()
|
||||||
|
val durationLabels = context.resources.getStringArray(R.array.poll_duration_names).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration }
|
||||||
|
binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply {
|
||||||
|
setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
durations = durations.filter { it in minDuration..maxDuration }
|
||||||
|
|
||||||
binding.addChoiceButton.setOnClickListener {
|
binding.addChoiceButton.setOnClickListener {
|
||||||
if (adapter.itemCount < maxOptionCount) {
|
if (adapter.itemCount < maxOptionCount) {
|
||||||
adapter.addChoice()
|
adapter.addChoice()
|
||||||
|
@ -66,7 +76,7 @@ fun showAddPollDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val pollDurationId = context.resources.getIntArray(R.array.poll_duration_values).indexOfLast {
|
val pollDurationId = durations.indexOfLast {
|
||||||
it <= poll?.expiresIn ?: 0
|
it <= poll?.expiresIn ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,13 +89,10 @@ fun showAddPollDialog(
|
||||||
button.setOnClickListener {
|
button.setOnClickListener {
|
||||||
val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition
|
val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition
|
||||||
|
|
||||||
val pollDuration = context.resources
|
|
||||||
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
|
||||||
|
|
||||||
onUpdatePoll(
|
onUpdatePoll(
|
||||||
NewPoll(
|
NewPoll(
|
||||||
options = adapter.pollOptions,
|
options = adapter.pollOptions,
|
||||||
expiresIn = pollDuration,
|
expiresIn = durations[selectedPollDurationId],
|
||||||
multiple = binding.multipleChoicesCheckBox.isChecked
|
multiple = binding.multipleChoicesCheckBox.isChecked
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,6 +22,8 @@ import android.util.AttributeSet
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputConnection
|
import android.view.inputmethod.InputConnection
|
||||||
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
||||||
|
import androidx.core.view.OnReceiveContentListener
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
import androidx.emoji.widget.EmojiEditTextHelper
|
import androidx.emoji.widget.EmojiEditTextHelper
|
||||||
|
@ -32,41 +34,33 @@ class EditTextTyped @JvmOverloads constructor(
|
||||||
) :
|
) :
|
||||||
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||||
|
|
||||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
|
||||||
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// fix a bug with autocomplete and some keyboards
|
// fix a bug with autocomplete and some keyboards
|
||||||
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
||||||
inputType = newInputType
|
inputType = newInputType
|
||||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener))
|
super.setKeyListener(emojiEditTextHelper.getKeyListener(keyListener))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setKeyListener(input: KeyListener) {
|
override fun setKeyListener(input: KeyListener?) {
|
||||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input))
|
if (input != null) {
|
||||||
|
super.setKeyListener(emojiEditTextHelper.getKeyListener(input))
|
||||||
|
} else {
|
||||||
|
super.setKeyListener(input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) {
|
fun setOnReceiveContentListener(listener: OnReceiveContentListener) {
|
||||||
onCommitContentListener = listener
|
ViewCompat.setOnReceiveContentListener(this, arrayOf("image/*"), listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||||
val connection = super.onCreateInputConnection(editorInfo)
|
val connection = super.onCreateInputConnection(editorInfo)
|
||||||
return if (onCommitContentListener != null) {
|
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
return emojiEditTextHelper.onCreateInputConnection(
|
||||||
getEmojiEditTextHelper().onCreateInputConnection(
|
InputConnectionCompat.createWrapper(this, connection, editorInfo),
|
||||||
InputConnectionCompat.createWrapper(
|
editorInfo
|
||||||
connection, editorInfo,
|
)!!
|
||||||
onCommitContentListener!!
|
|
||||||
),
|
|
||||||
editorInfo
|
|
||||||
)!!
|
|
||||||
} else {
|
|
||||||
connection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getEmojiEditTextHelper(): EmojiEditTextHelper {
|
|
||||||
return emojiEditTextHelper
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,17 +16,17 @@
|
||||||
package com.keylesspalace.tusky.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.SpannedString
|
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.keylesspalace.tusky.db.Converters
|
import com.keylesspalace.tusky.db.Converters
|
||||||
import com.keylesspalace.tusky.entity.Account
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Conversation
|
import com.keylesspalace.tusky.entity.Conversation
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -47,17 +47,15 @@ data class ConversationAccountEntity(
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val emojis: List<Emoji>
|
val emojis: List<Emoji>
|
||||||
) {
|
) {
|
||||||
fun toAccount(): Account {
|
fun toAccount(): TimelineAccount {
|
||||||
return Account(
|
return TimelineAccount(
|
||||||
id = id,
|
id = id,
|
||||||
username = username,
|
username = username,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
|
url = "",
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
url = "",
|
|
||||||
localUsername = "",
|
localUsername = "",
|
||||||
note = SpannedString(""),
|
|
||||||
header = ""
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +77,7 @@ data class ConversationStatusEntity(
|
||||||
val spoilerText: String,
|
val spoilerText: String,
|
||||||
val attachments: ArrayList<Attachment>,
|
val attachments: ArrayList<Attachment>,
|
||||||
val mentions: List<Status.Mention>,
|
val mentions: List<Status.Mention>,
|
||||||
|
val tags: List<HashTag>?,
|
||||||
val showingHiddenContent: Boolean,
|
val showingHiddenContent: Boolean,
|
||||||
val expanded: Boolean,
|
val expanded: Boolean,
|
||||||
val collapsible: Boolean,
|
val collapsible: Boolean,
|
||||||
|
@ -98,7 +97,7 @@ data class ConversationStatusEntity(
|
||||||
if (inReplyToId != other.inReplyToId) return false
|
if (inReplyToId != other.inReplyToId) return false
|
||||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||||
if (account != other.account) return false
|
if (account != other.account) return false
|
||||||
if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings
|
if (content.toString() != other.content.toString()) return false
|
||||||
if (createdAt != other.createdAt) return false
|
if (createdAt != other.createdAt) return false
|
||||||
if (emojis != other.emojis) return false
|
if (emojis != other.emojis) return false
|
||||||
if (favouritesCount != other.favouritesCount) return false
|
if (favouritesCount != other.favouritesCount) return false
|
||||||
|
@ -107,6 +106,7 @@ data class ConversationStatusEntity(
|
||||||
if (spoilerText != other.spoilerText) return false
|
if (spoilerText != other.spoilerText) return false
|
||||||
if (attachments != other.attachments) return false
|
if (attachments != other.attachments) return false
|
||||||
if (mentions != other.mentions) return false
|
if (mentions != other.mentions) return false
|
||||||
|
if (tags != other.tags) return false
|
||||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
if (showingHiddenContent != other.showingHiddenContent) return false
|
||||||
if (expanded != other.expanded) return false
|
if (expanded != other.expanded) return false
|
||||||
if (collapsible != other.collapsible) return false
|
if (collapsible != other.collapsible) return false
|
||||||
|
@ -123,7 +123,7 @@ data class ConversationStatusEntity(
|
||||||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||||
result = 31 * result + account.hashCode()
|
result = 31 * result + account.hashCode()
|
||||||
result = 31 * result + content.hashCode()
|
result = 31 * result + content.toString().hashCode()
|
||||||
result = 31 * result + createdAt.hashCode()
|
result = 31 * result + createdAt.hashCode()
|
||||||
result = 31 * result + emojis.hashCode()
|
result = 31 * result + emojis.hashCode()
|
||||||
result = 31 * result + favouritesCount
|
result = 31 * result + favouritesCount
|
||||||
|
@ -132,6 +132,7 @@ data class ConversationStatusEntity(
|
||||||
result = 31 * result + spoilerText.hashCode()
|
result = 31 * result + spoilerText.hashCode()
|
||||||
result = 31 * result + attachments.hashCode()
|
result = 31 * result + attachments.hashCode()
|
||||||
result = 31 * result + mentions.hashCode()
|
result = 31 * result + mentions.hashCode()
|
||||||
|
result = 31 * result + tags.hashCode()
|
||||||
result = 31 * result + showingHiddenContent.hashCode()
|
result = 31 * result + showingHiddenContent.hashCode()
|
||||||
result = 31 * result + expanded.hashCode()
|
result = 31 * result + expanded.hashCode()
|
||||||
result = 31 * result + collapsible.hashCode()
|
result = 31 * result + collapsible.hashCode()
|
||||||
|
@ -162,6 +163,7 @@ data class ConversationStatusEntity(
|
||||||
visibility = Status.Visibility.DIRECT,
|
visibility = Status.Visibility.DIRECT,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
|
tags = tags,
|
||||||
application = null,
|
application = null,
|
||||||
pinned = false,
|
pinned = false,
|
||||||
muted = muted,
|
muted = muted,
|
||||||
|
@ -171,7 +173,7 @@ data class ConversationStatusEntity(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Account.toEntity() =
|
fun TimelineAccount.toEntity() =
|
||||||
ConversationAccountEntity(
|
ConversationAccountEntity(
|
||||||
id = id,
|
id = id,
|
||||||
username = username,
|
username = username,
|
||||||
|
@ -197,6 +199,7 @@ fun Status.toEntity() =
|
||||||
spoilerText = spoilerText,
|
spoilerText = spoilerText,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
|
tags = tags,
|
||||||
showingHiddenContent = false,
|
showingHiddenContent = false,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
collapsible = shouldTrimStatus(content),
|
collapsible = shouldTrimStatus(content),
|
||||||
|
|
|
@ -108,7 +108,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
statusDisplayOptions);
|
statusDisplayOptions);
|
||||||
|
|
||||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
|
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
|
||||||
status.getMentions(), status.getEmojis(),
|
status.getMentions(), status.getTags(), status.getEmojis(),
|
||||||
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
||||||
|
|
||||||
setConversationName(conversation.getAccounts());
|
setConversationName(conversation.getAccounts());
|
||||||
|
@ -154,10 +154,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_more);
|
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
||||||
content.setFilters(COLLAPSE_INPUT_FILTER);
|
content.setFilters(COLLAPSE_INPUT_FILTER);
|
||||||
} else {
|
} else {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||||
content.setFilters(NO_INPUT_FILTER);
|
content.setFilters(NO_INPUT_FILTER);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -31,7 +30,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.ViewTagActivity
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
@ -103,7 +102,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
|
|
||||||
initSwipeToRefresh()
|
initSwipeToRefresh()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.conversationFlow.collectLatest { pagingData ->
|
viewModel.conversationFlow.collectLatest { pagingData ->
|
||||||
adapter.submitData(pagingData)
|
adapter.submitData(pagingData)
|
||||||
}
|
}
|
||||||
|
@ -233,8 +232,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
val intent = Intent(context, ViewTagActivity::class.java)
|
val intent = StatusListActivity.newHashtagIntent(requireContext(), tag)
|
||||||
intent.putExtra("hashtag", tag)
|
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,14 +90,14 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
|
|
||||||
if (draft.inReplyToId != null) {
|
if (draft.inReplyToId != null) {
|
||||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
viewModel.getToot(draft.inReplyToId)
|
viewModel.getStatus(draft.inReplyToId)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(from(this))
|
.autoDispose(from(this))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ status ->
|
{ status ->
|
||||||
val composeOptions = ComposeActivity.ComposeOptions(
|
val composeOptions = ComposeActivity.ComposeOptions(
|
||||||
draftId = draft.id,
|
draftId = draft.id,
|
||||||
tootText = draft.content,
|
content = draft.content,
|
||||||
contentWarning = draft.contentWarning,
|
contentWarning = draft.contentWarning,
|
||||||
inReplyToId = draft.inReplyToId,
|
inReplyToId = draft.inReplyToId,
|
||||||
replyingStatusContent = status.content.toString(),
|
replyingStatusContent = status.content.toString(),
|
||||||
|
@ -121,7 +121,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
if (throwable is HttpException && throwable.code() == 404) {
|
if (throwable is HttpException && throwable.code() == 404) {
|
||||||
// the original status to which a reply was drafted has been deleted
|
// the original status to which a reply was drafted has been deleted
|
||||||
// let's open the ComposeActivity without reply information
|
// let's open the ComposeActivity without reply information
|
||||||
Toast.makeText(this, getString(R.string.drafts_toot_reply_removed), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show()
|
||||||
openDraftWithoutReply(draft)
|
openDraftWithoutReply(draft)
|
||||||
} else {
|
} else {
|
||||||
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
||||||
|
@ -137,7 +137,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
private fun openDraftWithoutReply(draft: DraftEntity) {
|
private fun openDraftWithoutReply(draft: DraftEntity) {
|
||||||
val composeOptions = ComposeActivity.ComposeOptions(
|
val composeOptions = ComposeActivity.ComposeOptions(
|
||||||
draftId = draft.id,
|
draftId = draft.id,
|
||||||
tootText = draft.content,
|
content = draft.content,
|
||||||
contentWarning = draft.contentWarning,
|
contentWarning = draft.contentWarning,
|
||||||
draftAttachments = draft.attachments,
|
draftAttachments = draft.attachments,
|
||||||
poll = draft.poll,
|
poll = draft.poll,
|
||||||
|
|
|
@ -60,8 +60,8 @@ class DraftsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToot(tootId: String): Single<Status> {
|
fun getStatus(statusId: String): Single<Status> {
|
||||||
return api.status(tootId)
|
return api.status(statusId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -0,0 +1,309 @@
|
||||||
|
/* 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.components.login
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
|
import com.keylesspalace.tusky.MainActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.entity.AppCredentials
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
|
import com.keylesspalace.tusky.util.rickRoll
|
||||||
|
import com.keylesspalace.tusky.util.shouldRickRoll
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/** Main login page, the first thing that users see. Has prompt for instance and login button. */
|
||||||
|
class LoginActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var mastodonApi: MastodonApi
|
||||||
|
|
||||||
|
private val binding by viewBinding(ActivityLoginBinding::inflate)
|
||||||
|
|
||||||
|
private lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
|
private val oauthRedirectUri: String
|
||||||
|
get() {
|
||||||
|
val scheme = getString(R.string.oauth_scheme)
|
||||||
|
val host = BuildConfig.APPLICATION_ID
|
||||||
|
return "$scheme://$host/"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val doWebViewAuth = registerForActivityResult(OauthLogin()) { result ->
|
||||||
|
when (result) {
|
||||||
|
is LoginResult.Ok -> lifecycleScope.launch {
|
||||||
|
fetchOauthToken(result.code)
|
||||||
|
}
|
||||||
|
is LoginResult.Err -> {
|
||||||
|
// Authorization failed. Put the error response where the user can read it and they
|
||||||
|
// can try again.
|
||||||
|
setLoading(false)
|
||||||
|
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"%s %s".format(
|
||||||
|
getString(R.string.error_authorization_denied),
|
||||||
|
result.errorMessage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is LoginResult.Cancel -> {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
if (savedInstanceState == null &&
|
||||||
|
BuildConfig.CUSTOM_INSTANCE.isNotBlank() &&
|
||||||
|
!isAdditionalLogin()
|
||||||
|
) {
|
||||||
|
binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
|
||||||
|
binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
|
||||||
|
Glide.with(binding.loginLogo)
|
||||||
|
.load(BuildConfig.CUSTOM_LOGO_URL)
|
||||||
|
.placeholder(null)
|
||||||
|
.into(binding.loginLogo)
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences = getSharedPreferences(
|
||||||
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.loginButton.setOnClickListener { onButtonClick() }
|
||||||
|
binding.registerButton.setOnClickListener { onRegisterClick() }
|
||||||
|
|
||||||
|
binding.whatsAnInstanceTextView.setOnClickListener {
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.dialog_whats_an_instance)
|
||||||
|
.setPositiveButton(R.string.action_close, null)
|
||||||
|
.show()
|
||||||
|
val textView = dialog.findViewById<TextView>(android.R.id.message)
|
||||||
|
textView?.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAdditionalLogin()) {
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
} else {
|
||||||
|
binding.toolbar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requiresLogin(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
super.finish()
|
||||||
|
if (isAdditionalLogin()) {
|
||||||
|
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle registation of new account in the most basic way possible; open a URL
|
||||||
|
* in the system default browser.
|
||||||
|
*/
|
||||||
|
private fun onRegisterClick() {
|
||||||
|
binding.registerButton.isEnabled = false
|
||||||
|
|
||||||
|
val openRegisterPage = Intent(android.content.Intent.ACTION_VIEW)
|
||||||
|
openRegisterPage.data = Uri.parse(BuildConfig.REGISTER_ACCOUNT_URL)
|
||||||
|
startActivity(openRegisterPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain the oauth client credentials for this app. This is only necessary the first time the
|
||||||
|
* app is run on a given server instance. So, after the first authentication, they are
|
||||||
|
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
|
||||||
|
*/
|
||||||
|
private fun onButtonClick() {
|
||||||
|
binding.loginButton.isEnabled = false
|
||||||
|
binding.domainTextInputLayout.error = null
|
||||||
|
|
||||||
|
val domain = canonicalizeDomain(binding.domainEditText.text.toString())
|
||||||
|
|
||||||
|
try {
|
||||||
|
HttpUrl.Builder().host(domain).scheme("https").build()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
setLoading(false)
|
||||||
|
binding.domainTextInputLayout.error = getString(R.string.error_invalid_domain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRickRoll(this, domain)) {
|
||||||
|
rickRoll(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val credentials: AppCredentials = try {
|
||||||
|
mastodonApi.authenticateApp(
|
||||||
|
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||||
|
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.loginButton.isEnabled = true
|
||||||
|
binding.domainTextInputLayout.error =
|
||||||
|
getString(R.string.error_failed_app_registration)
|
||||||
|
setLoading(false)
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before we open browser page we save the data.
|
||||||
|
// Even if we don't open other apps user may go to password manager or somewhere else
|
||||||
|
// and we will need to pick up the process where we left off.
|
||||||
|
// Alternatively we could pass it all as part of the intent and receive it back
|
||||||
|
// but it is a bit of a workaround.
|
||||||
|
preferences.edit()
|
||||||
|
.putString(DOMAIN, domain)
|
||||||
|
.putString(CLIENT_ID, credentials.clientId)
|
||||||
|
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
|
||||||
|
// To authorize this app and log in it's necessary to redirect to the domain given,
|
||||||
|
// login there, and the server will redirect back to the app with its response.
|
||||||
|
val url = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host(domain)
|
||||||
|
.addPathSegments(MastodonApi.ENDPOINT_AUTHORIZE)
|
||||||
|
.addQueryParameter("client_id", clientId)
|
||||||
|
.addQueryParameter("redirect_uri", oauthRedirectUri)
|
||||||
|
.addQueryParameter("response_type", "code")
|
||||||
|
.addQueryParameter("scope", OAUTH_SCOPES)
|
||||||
|
.build()
|
||||||
|
doWebViewAuth.launch(LoginData(url.toString().toUri(), oauthRedirectUri.toUri()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
// first show or user cancelled login
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchOauthToken(code: String) {
|
||||||
|
/* restore variables from SharedPreferences */
|
||||||
|
val domain = preferences.getNonNullString(DOMAIN, "")
|
||||||
|
val clientId = preferences.getNonNullString(CLIENT_ID, "")
|
||||||
|
val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "")
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
val accessToken = try {
|
||||||
|
mastodonApi.fetchOAuthToken(
|
||||||
|
domain, clientId, clientSecret, oauthRedirectUri, code,
|
||||||
|
"authorization_code"
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setLoading(false)
|
||||||
|
binding.domainTextInputLayout.error =
|
||||||
|
getString(R.string.error_retrieving_oauth_token)
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accountManager.addAccount(accessToken.accessToken, domain)
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(loadingState: Boolean) {
|
||||||
|
if (loadingState) {
|
||||||
|
binding.loginLoadingLayout.visibility = View.VISIBLE
|
||||||
|
binding.loginInputLayout.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.loginLoadingLayout.visibility = View.GONE
|
||||||
|
binding.loginInputLayout.visibility = View.VISIBLE
|
||||||
|
binding.loginButton.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAdditionalLogin(): Boolean {
|
||||||
|
return intent.getBooleanExtra(LOGIN_MODE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LoginActivity" // logging tag
|
||||||
|
private const val OAUTH_SCOPES = "read write follow"
|
||||||
|
private const val LOGIN_MODE = "LOGIN_MODE"
|
||||||
|
private const val DOMAIN = "domain"
|
||||||
|
private const val CLIENT_ID = "clientId"
|
||||||
|
private const val CLIENT_SECRET = "clientSecret"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getIntent(context: Context, mode: Boolean): Intent {
|
||||||
|
val loginIntent = Intent(context, LoginActivity::class.java)
|
||||||
|
loginIntent.putExtra(LOGIN_MODE, mode)
|
||||||
|
return loginIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Make sure the user-entered text is just a fully-qualified domain name. */
|
||||||
|
private fun canonicalizeDomain(domain: String): String {
|
||||||
|
// Strip any schemes out.
|
||||||
|
var s = domain.replaceFirst("http://", "")
|
||||||
|
s = s.replaceFirst("https://", "")
|
||||||
|
// If a username was included (e.g. username@example.com), just take what's after the '@'.
|
||||||
|
val at = s.lastIndexOf('@')
|
||||||
|
if (at != -1) {
|
||||||
|
s = s.substring(at + 1)
|
||||||
|
}
|
||||||
|
return s.trim { it <= ' ' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package com.keylesspalace.tusky.components.login
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebResourceError
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebStorage
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
|
import com.keylesspalace.tusky.databinding.LoginWebviewBinding
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/** Contract for starting [LoginWebViewActivity]. */
|
||||||
|
class OauthLogin : ActivityResultContract<LoginData, LoginResult>() {
|
||||||
|
override fun createIntent(context: Context, input: LoginData): Intent {
|
||||||
|
val intent = Intent(context, LoginWebViewActivity::class.java)
|
||||||
|
intent.putExtra(DATA_EXTRA, input)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(resultCode: Int, intent: Intent?): LoginResult {
|
||||||
|
// Can happen automatically on up or back press
|
||||||
|
return if (resultCode == Activity.RESULT_CANCELED) {
|
||||||
|
LoginResult.Cancel
|
||||||
|
} else {
|
||||||
|
intent!!.getParcelableExtra(RESULT_EXTRA)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RESULT_EXTRA = "result"
|
||||||
|
private const val DATA_EXTRA = "data"
|
||||||
|
|
||||||
|
fun parseData(intent: Intent): LoginData {
|
||||||
|
return intent.getParcelableExtra(DATA_EXTRA)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeResultIntent(result: LoginResult): Intent {
|
||||||
|
val intent = Intent()
|
||||||
|
intent.putExtra(RESULT_EXTRA, result)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class LoginData(
|
||||||
|
val url: Uri,
|
||||||
|
val oauthRedirectUrl: Uri,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
sealed class LoginResult : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data class Ok(val code: String) : LoginResult()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Err(val errorMessage: String) : LoginResult()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object Cancel : LoginResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Activity to do Oauth process using WebView. */
|
||||||
|
class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
|
private val binding by viewBinding(LoginWebviewBinding::inflate)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val data = OauthLogin.parseData(intent)
|
||||||
|
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.loginToolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
|
||||||
|
val webView = binding.loginWebView
|
||||||
|
webView.settings.allowContentAccess = false
|
||||||
|
webView.settings.allowFileAccess = false
|
||||||
|
webView.settings.databaseEnabled = false
|
||||||
|
webView.settings.displayZoomControls = false
|
||||||
|
webView.settings.javaScriptCanOpenWindowsAutomatically = false
|
||||||
|
// Javascript needs to be enabled because otherwise 2FA does not work in some instances
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
webView.settings.javaScriptEnabled = true
|
||||||
|
webView.settings.userAgentString += " Tusky/${BuildConfig.VERSION_NAME}"
|
||||||
|
|
||||||
|
val oauthUrl = data.oauthRedirectUrl
|
||||||
|
|
||||||
|
webView.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
error: WebResourceError
|
||||||
|
) {
|
||||||
|
Log.d("LoginWeb", "Failed to load ${data.url}: $error")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): Boolean {
|
||||||
|
val url = request.url
|
||||||
|
return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) {
|
||||||
|
val error = url.getQueryParameter("error")
|
||||||
|
if (error != null) {
|
||||||
|
sendResult(LoginResult.Err(error))
|
||||||
|
} else {
|
||||||
|
val code = url.getQueryParameter("code").orEmpty()
|
||||||
|
sendResult(LoginResult.Ok(code))
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
webView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
webView.loadUrl(data.url.toString())
|
||||||
|
} else {
|
||||||
|
webView.restoreState(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
binding.loginWebView.saveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (isFinishing) {
|
||||||
|
// We don't want to keep user session in WebView, we just want our own accessToken
|
||||||
|
WebStorage.getInstance().deleteAllData()
|
||||||
|
CookieManager.getInstance().removeAllCookies(null)
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requiresLogin() = false
|
||||||
|
|
||||||
|
private fun sendResult(result: LoginResult) {
|
||||||
|
setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,6 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -46,9 +45,9 @@ import androidx.work.WorkRequest;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||||
import com.bumptech.glide.request.FutureTarget;
|
import com.bumptech.glide.request.FutureTarget;
|
||||||
import com.keylesspalace.tusky.BuildConfig;
|
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||||
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.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
@ -67,6 +66,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -88,8 +88,6 @@ public class NotificationHelper {
|
||||||
|
|
||||||
public static final String REPLY_ACTION = "REPLY_ACTION";
|
public static final String REPLY_ACTION = "REPLY_ACTION";
|
||||||
|
|
||||||
public static final String COMPOSE_ACTION = "COMPOSE_ACTION";
|
|
||||||
|
|
||||||
public static final String KEY_REPLY = "KEY_REPLY";
|
public static final String KEY_REPLY = "KEY_REPLY";
|
||||||
|
|
||||||
public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID";
|
public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID";
|
||||||
|
@ -108,10 +106,6 @@ public class NotificationHelper {
|
||||||
|
|
||||||
public static final String KEY_MENTIONS = "KEY_MENTIONS";
|
public static final String KEY_MENTIONS = "KEY_MENTIONS";
|
||||||
|
|
||||||
public static final String KEY_CITED_TEXT = "KEY_CITED_TEXT";
|
|
||||||
|
|
||||||
public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* notification channels used on Android O+
|
* notification channels used on Android O+
|
||||||
**/
|
**/
|
||||||
|
@ -206,21 +200,24 @@ public class NotificationHelper {
|
||||||
.setLabel(context.getString(R.string.label_quick_reply))
|
.setLabel(context.getString(R.string.label_quick_reply))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account);
|
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(context, body, account);
|
||||||
|
|
||||||
NotificationCompat.Action quickReplyAction =
|
NotificationCompat.Action quickReplyAction =
|
||||||
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
||||||
context.getString(R.string.action_quick_reply), quickReplyPendingIntent)
|
context.getString(R.string.action_quick_reply),
|
||||||
|
quickReplyPendingIntent)
|
||||||
.addRemoteInput(replyRemoteInput)
|
.addRemoteInput(replyRemoteInput)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
builder.addAction(quickReplyAction);
|
builder.addAction(quickReplyAction);
|
||||||
|
|
||||||
PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account);
|
PendingIntent composeIntent = getStatusComposeIntent(context, body, account);
|
||||||
|
|
||||||
NotificationCompat.Action composeAction =
|
NotificationCompat.Action composeAction =
|
||||||
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
||||||
context.getString(R.string.action_compose_shortcut), composePendingIntent)
|
context.getString(R.string.action_compose_shortcut),
|
||||||
|
composeIntent)
|
||||||
|
.setShowsUserInterface(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
builder.addAction(composeAction);
|
builder.addAction(composeAction);
|
||||||
|
@ -237,7 +234,6 @@ public class NotificationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
// =======
|
|
||||||
final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true);
|
final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true);
|
||||||
|
|
||||||
if (currentNotifications.length() != 1) {
|
if (currentNotifications.length() != 1) {
|
||||||
|
@ -275,7 +271,7 @@ public class NotificationHelper {
|
||||||
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
||||||
|
|
||||||
PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000),
|
PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
pendingIntentFlags(false));
|
||||||
|
|
||||||
// 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);
|
||||||
|
@ -285,18 +281,18 @@ public class NotificationHelper {
|
||||||
eventStackBuilder.addNextIntent(eventResultIntent);
|
eventStackBuilder.addNextIntent(eventResultIntent);
|
||||||
|
|
||||||
PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(),
|
PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
pendingIntentFlags(false));
|
||||||
|
|
||||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
||||||
deleteIntent.putExtra(ACCOUNT_ID, account.getId());
|
deleteIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, summary ? (int) account.getId() : notificationId, deleteIntent,
|
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, summary ? (int) account.getId() : notificationId, deleteIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
pendingIntentFlags(false));
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body))
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body))
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
||||||
.setDeleteIntent(deletePendingIntent)
|
.setDeleteIntent(deletePendingIntent)
|
||||||
.setColor(BuildConfig.FLAVOR == "green" ? Color.parseColor("#19A341") : ContextCompat.getColor(context, R.color.chinwag_green))
|
.setColor(ContextCompat.getColor(context, R.color.notification_color))
|
||||||
.setGroup(account.getAccountId())
|
.setGroup(account.getAccountId())
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setShortcutId(Long.toString(account.getId()))
|
.setShortcutId(Long.toString(account.getId()))
|
||||||
|
@ -307,11 +303,9 @@ public class NotificationHelper {
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) {
|
private static PendingIntent getStatusReplyIntent(Context context, Notification body, AccountEntity account) {
|
||||||
Status status = body.getStatus();
|
Status status = body.getStatus();
|
||||||
|
|
||||||
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
|
||||||
String citedText = status.getContent().toString();
|
|
||||||
String inReplyToId = status.getId();
|
String inReplyToId = status.getId();
|
||||||
Status actionableStatus = status.getActionableStatus();
|
Status actionableStatus = status.getActionableStatus();
|
||||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||||
|
@ -326,9 +320,7 @@ public class NotificationHelper {
|
||||||
mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames));
|
mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames));
|
||||||
|
|
||||||
Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class)
|
Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class)
|
||||||
.setAction(action)
|
.setAction(REPLY_ACTION)
|
||||||
.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor)
|
|
||||||
.putExtra(KEY_CITED_TEXT, citedText)
|
|
||||||
.putExtra(KEY_SENDER_ACCOUNT_ID, account.getId())
|
.putExtra(KEY_SENDER_ACCOUNT_ID, account.getId())
|
||||||
.putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier())
|
.putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier())
|
||||||
.putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName())
|
.putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName())
|
||||||
|
@ -341,7 +333,50 @@ public class NotificationHelper {
|
||||||
return PendingIntent.getBroadcast(context.getApplicationContext(),
|
return PendingIntent.getBroadcast(context.getApplicationContext(),
|
||||||
notificationId,
|
notificationId,
|
||||||
replyIntent,
|
replyIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
pendingIntentFlags(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PendingIntent getStatusComposeIntent(Context context, Notification body, AccountEntity account) {
|
||||||
|
Status status = body.getStatus();
|
||||||
|
|
||||||
|
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
||||||
|
String citedText = status.getContent().toString();
|
||||||
|
String inReplyToId = status.getId();
|
||||||
|
Status actionableStatus = status.getActionableStatus();
|
||||||
|
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||||
|
String contentWarning = actionableStatus.getSpoilerText();
|
||||||
|
List<Status.Mention> mentions = actionableStatus.getMentions();
|
||||||
|
Set<String> mentionedUsernames = new LinkedHashSet<>();
|
||||||
|
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
||||||
|
for (Status.Mention mention : mentions) {
|
||||||
|
String mentionedUsername = mention.getUsername();
|
||||||
|
if (!mentionedUsername.equals(account.getUsername())) {
|
||||||
|
mentionedUsernames.add(mention.getUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ComposeActivity.ComposeOptions composeOptions = new ComposeActivity.ComposeOptions();
|
||||||
|
composeOptions.setInReplyToId(inReplyToId);
|
||||||
|
composeOptions.setReplyVisibility(replyVisibility);
|
||||||
|
composeOptions.setContentWarning(contentWarning);
|
||||||
|
composeOptions.setReplyingStatusAuthor(citedLocalAuthor);
|
||||||
|
composeOptions.setReplyingStatusContent(citedText);
|
||||||
|
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||||
|
composeOptions.setModifiedInitialState(true);
|
||||||
|
|
||||||
|
Intent composeIntent = ComposeActivity.startIntent(
|
||||||
|
context,
|
||||||
|
composeOptions,
|
||||||
|
notificationId,
|
||||||
|
account.getId()
|
||||||
|
);
|
||||||
|
|
||||||
|
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
return PendingIntent.getActivity(context.getApplicationContext(),
|
||||||
|
notificationId,
|
||||||
|
composeIntent,
|
||||||
|
pendingIntentFlags(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) {
|
public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) {
|
||||||
|
@ -409,9 +444,7 @@ public class NotificationHelper {
|
||||||
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
notificationManager.deleteNotificationChannelGroup(account.getIdentifier());
|
notificationManager.deleteNotificationChannelGroup(account.getIdentifier());
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,7 +454,6 @@ public class NotificationHelper {
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
// used until Tusky 1.4
|
// used until Tusky 1.4
|
||||||
//noinspection ConstantConditions
|
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_MENTION);
|
notificationManager.deleteNotificationChannel(CHANNEL_MENTION);
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
|
notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_BOOST);
|
notificationManager.deleteNotificationChannel(CHANNEL_BOOST);
|
||||||
|
@ -440,7 +472,6 @@ public class NotificationHelper {
|
||||||
// on Android >= O, notifications are enabled, if at least one channel is enabled
|
// on Android >= O, notifications are enabled, if at least one channel is enabled
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
if (notificationManager.areNotificationsEnabled()) {
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
|
for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
|
||||||
if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
|
if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
|
||||||
|
@ -491,7 +522,6 @@ public class NotificationHelper {
|
||||||
accountManager.saveAccount(account);
|
accountManager.saveAccount(account);
|
||||||
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
//noinspection ConstantConditions
|
|
||||||
notificationManager.cancel((int) account.getId());
|
notificationManager.cancel((int) account.getId());
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
|
@ -511,7 +541,6 @@ public class NotificationHelper {
|
||||||
// unknown notificationtype
|
// unknown notificationtype
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//noinspection ConstantConditions
|
|
||||||
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
|
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
|
||||||
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
||||||
}
|
}
|
||||||
|
@ -674,4 +703,11 @@ public class NotificationHelper {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int pendingIntentFlags(boolean mutable) {
|
||||||
|
if (mutable) {
|
||||||
|
return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);
|
||||||
|
} else {
|
||||||
|
return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.SplashActivity
|
import com.keylesspalace.tusky.SplashActivity
|
||||||
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding
|
import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding
|
||||||
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
|
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
import com.keylesspalace.tusky.util.EmojiCompatFont
|
||||||
|
@ -220,7 +221,7 @@ class EmojiPreference(
|
||||||
context,
|
context,
|
||||||
0x1f973, // This is the codepoint of the party face emoji :D
|
0x1f973, // This is the codepoint of the party face emoji :D
|
||||||
launchIntent,
|
launchIntent,
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT
|
NotificationHelper.pendingIntentFlags(false)
|
||||||
)
|
)
|
||||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
mgr.set(
|
mgr.set(
|
||||||
|
|
|
@ -78,7 +78,7 @@ class PreferencesActivity :
|
||||||
NotificationPreferencesFragment.newInstance()
|
NotificationPreferencesFragment.newInstance()
|
||||||
}
|
}
|
||||||
TAB_FILTER_PREFERENCES -> {
|
TAB_FILTER_PREFERENCES -> {
|
||||||
setTitle(R.string.pref_title_status_tabs)
|
setTitle(R.string.pref_title_post_tabs)
|
||||||
TabFilterPreferencesFragment.newInstance()
|
TabFilterPreferencesFragment.newInstance()
|
||||||
}
|
}
|
||||||
PROXY_PREFERENCES -> {
|
PROXY_PREFERENCES -> {
|
||||||
|
|
|
@ -86,11 +86,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
|
|
||||||
listPreference {
|
listPreference {
|
||||||
setDefaultValue("medium")
|
setDefaultValue("medium")
|
||||||
setEntries(R.array.status_text_size_names)
|
setEntries(R.array.post_text_size_names)
|
||||||
setEntryValues(R.array.status_text_size_values)
|
setEntryValues(R.array.post_text_size_values)
|
||||||
key = PrefKeys.STATUS_TEXT_SIZE
|
key = PrefKeys.STATUS_TEXT_SIZE
|
||||||
setSummaryProvider { entry }
|
setSummaryProvider { entry }
|
||||||
setTitle(R.string.pref_status_text_size)
|
setTitle(R.string.pref_post_text_size)
|
||||||
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
|
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +138,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
isSingleLineTitle = false
|
isSingleLineTitle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switchPreference {
|
||||||
|
setDefaultValue(false)
|
||||||
|
key = PrefKeys.ANIMATE_CUSTOM_EMOJIS
|
||||||
|
setTitle(R.string.pref_title_animate_custom_emojis)
|
||||||
|
isSingleLineTitle = false
|
||||||
|
}
|
||||||
|
|
||||||
switchPreference {
|
switchPreference {
|
||||||
setDefaultValue(true)
|
setDefaultValue(true)
|
||||||
key = PrefKeys.USE_BLURHASH
|
key = PrefKeys.USE_BLURHASH
|
||||||
|
@ -179,13 +186,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
setTitle(R.string.pref_title_enable_swipe_for_tabs)
|
setTitle(R.string.pref_title_enable_swipe_for_tabs)
|
||||||
isSingleLineTitle = false
|
isSingleLineTitle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
switchPreference {
|
|
||||||
setDefaultValue(false)
|
|
||||||
key = PrefKeys.ANIMATE_CUSTOM_EMOJIS
|
|
||||||
setTitle(R.string.pref_title_animate_custom_emojis)
|
|
||||||
isSingleLineTitle = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preferenceCategory(R.string.pref_title_browser_settings) {
|
preferenceCategory(R.string.pref_title_browser_settings) {
|
||||||
|
@ -199,7 +199,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
|
|
||||||
preferenceCategory(R.string.pref_title_timeline_filters) {
|
preferenceCategory(R.string.pref_title_timeline_filters) {
|
||||||
preference {
|
preference {
|
||||||
setTitle(R.string.pref_title_status_tabs)
|
setTitle(R.string.pref_title_post_tabs)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
|
val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
|
||||||
|
@ -280,23 +280,24 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateHttpProxySummary() {
|
private fun updateHttpProxySummary() {
|
||||||
val sharedPreferences = preferenceManager.sharedPreferences
|
preferenceManager.sharedPreferences?.let { sharedPreferences ->
|
||||||
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
|
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
|
||||||
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
|
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
|
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
|
||||||
.toInt()
|
.toInt()
|
||||||
|
|
||||||
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
|
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
|
||||||
httpProxyPref?.summary = "$httpServer:$httpPort"
|
httpProxyPref?.summary = "$httpServer:$httpPort"
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
// user has entered wrong port, fall back to empty summary
|
||||||
}
|
}
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
// user has entered wrong port, fall back to empty summary
|
|
||||||
}
|
|
||||||
|
|
||||||
httpProxyPref?.summary = ""
|
httpProxyPref?.summary = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -23,9 +23,9 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||||
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
|
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
import com.keylesspalace.tusky.util.StatusViewHelper
|
import com.keylesspalace.tusky.util.StatusViewHelper
|
||||||
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
|
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
|
||||||
|
@ -33,6 +33,8 @@ import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
|
||||||
import com.keylesspalace.tusky.util.TimestampUtils
|
import com.keylesspalace.tusky.util.TimestampUtils
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.setClickableMentions
|
||||||
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.viewdata.toViewData
|
import com.keylesspalace.tusky.viewdata.toViewData
|
||||||
|
@ -96,7 +98,7 @@ class StatusViewHolder(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (status.spoilerText.isBlank()) {
|
if (status.spoilerText.isBlank()) {
|
||||||
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler)
|
setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
||||||
binding.statusContentWarningButton.hide()
|
binding.statusContentWarningButton.hide()
|
||||||
binding.statusContentWarningDescription.hide()
|
binding.statusContentWarningDescription.hide()
|
||||||
} else {
|
} else {
|
||||||
|
@ -110,35 +112,36 @@ class StatusViewHolder(
|
||||||
val contentShown = viewState.isContentShow(status.id, true)
|
val contentShown = viewState.isContentShow(status.id, true)
|
||||||
binding.statusContentWarningDescription.invalidate()
|
binding.statusContentWarningDescription.invalidate()
|
||||||
viewState.setContentShow(status.id, !contentShown)
|
viewState.setContentShow(status.id, !contentShown)
|
||||||
setTextVisible(!contentShown, status.content, status.mentions, status.emojis, adapterHandler)
|
setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
||||||
setContentWarningButtonText(!contentShown)
|
setContentWarningButtonText(!contentShown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.emojis, adapterHandler)
|
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setContentWarningButtonText(contentShown: Boolean) {
|
private fun setContentWarningButtonText(contentShown: Boolean) {
|
||||||
if (contentShown) {
|
if (contentShown) {
|
||||||
binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less)
|
binding.statusContentWarningButton.setText(R.string.post_content_warning_show_less)
|
||||||
} else {
|
} else {
|
||||||
binding.statusContentWarningButton.setText(R.string.status_content_warning_show_more)
|
binding.statusContentWarningButton.setText(R.string.post_content_warning_show_more)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTextVisible(
|
private fun setTextVisible(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
content: Spanned,
|
content: Spanned,
|
||||||
mentions: List<Status.Mention>?,
|
mentions: List<Status.Mention>,
|
||||||
|
tags: List<HashTag>?,
|
||||||
emojis: List<Emoji>,
|
emojis: List<Emoji>,
|
||||||
listener: LinkListener
|
listener: LinkListener
|
||||||
) {
|
) {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
|
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
|
||||||
LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener)
|
setClickableText(binding.statusContent, emojifiedText, mentions, tags, listener)
|
||||||
} else {
|
} else {
|
||||||
LinkHelper.setClickableMentions(binding.statusContent, mentions, listener)
|
setClickableMentions(binding.statusContent, mentions, listener)
|
||||||
}
|
}
|
||||||
if (binding.statusContent.text.isNullOrBlank()) {
|
if (binding.statusContent.text.isNullOrBlank()) {
|
||||||
binding.statusContent.hide()
|
binding.statusContent.hide()
|
||||||
|
@ -174,10 +177,10 @@ class StatusViewHolder(
|
||||||
|
|
||||||
binding.buttonToggleContent.show()
|
binding.buttonToggleContent.show()
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
binding.buttonToggleContent.setText(R.string.status_content_show_more)
|
binding.buttonToggleContent.setText(R.string.post_content_show_more)
|
||||||
binding.statusContent.filters = COLLAPSE_INPUT_FILTER
|
binding.statusContent.filters = COLLAPSE_INPUT_FILTER
|
||||||
} else {
|
} else {
|
||||||
binding.buttonToggleContent.setText(R.string.status_content_show_less)
|
binding.buttonToggleContent.setText(R.string.post_content_show_less)
|
||||||
binding.statusContent.filters = NO_INPUT_FILTER
|
binding.statusContent.filters = NO_INPUT_FILTER
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -29,8 +29,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
import com.keylesspalace.tusky.ViewTagActivity
|
|
||||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||||
import com.keylesspalace.tusky.components.report.Screen
|
import com.keylesspalace.tusky.components.report.Screen
|
||||||
|
@ -152,7 +152,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
||||||
|
|
||||||
private fun showError() {
|
private fun showError() {
|
||||||
if (snackbarErrorRetry?.isShown != true) {
|
if (snackbarErrorRetry?.isShown != true) {
|
||||||
snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE)
|
snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_posts, Snackbar.LENGTH_INDEFINITE)
|
||||||
snackbarErrorRetry?.setAction(R.string.action_retry) {
|
snackbarErrorRetry?.setAction(R.string.action_retry) {
|
||||||
adapter.retry()
|
adapter.retry()
|
||||||
}
|
}
|
||||||
|
@ -180,9 +180,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
||||||
|
|
||||||
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
||||||
|
|
||||||
override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag))
|
override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||||
|
|
||||||
override fun onViewUrl(url: String?) = viewModel.checkClickedUrl(url)
|
override fun onViewUrl(url: String) = viewModel.checkClickedUrl(url)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance() = ReportStatusesFragment()
|
fun newInstance() = ReportStatusesFragment()
|
||||||
|
|
|
@ -29,7 +29,7 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding
|
import com.keylesspalace.tusky.databinding.ActivityScheduledStatusBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
|
@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable {
|
class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, Injectable {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
@ -48,19 +48,19 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var eventHub: EventHub
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory }
|
private val viewModel: ScheduledStatusViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
private val adapter = ScheduledTootAdapter(this)
|
private val adapter = ScheduledStatusAdapter(this)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val binding = ActivityScheduledTootBinding.inflate(layoutInflater)
|
val binding = ActivityScheduledStatusBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
title = getString(R.string.title_scheduled_toot)
|
title = getString(R.string.title_scheduled_posts)
|
||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
if (loadState.refresh is LoadState.NotLoading) {
|
if (loadState.refresh is LoadState.NotLoading) {
|
||||||
binding.progressBar.hide()
|
binding.progressBar.hide()
|
||||||
if (adapter.itemCount == 0) {
|
if (adapter.itemCount == 0) {
|
||||||
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status)
|
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_posts)
|
||||||
binding.errorMessageView.show()
|
binding.errorMessageView.show()
|
||||||
} else {
|
} else {
|
||||||
binding.errorMessageView.hide()
|
binding.errorMessageView.hide()
|
||||||
|
@ -121,7 +121,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
this,
|
this,
|
||||||
ComposeActivity.ComposeOptions(
|
ComposeActivity.ComposeOptions(
|
||||||
scheduledTootId = item.id,
|
scheduledTootId = item.id,
|
||||||
tootText = item.params.text,
|
content = item.params.text,
|
||||||
contentWarning = item.params.spoilerText,
|
contentWarning = item.params.spoilerText,
|
||||||
mediaAttachments = item.mediaAttachments,
|
mediaAttachments = item.mediaAttachments,
|
||||||
inReplyToId = item.params.inReplyToId,
|
inReplyToId = item.params.inReplyToId,
|
||||||
|
@ -138,6 +138,6 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newIntent(context: Context) = Intent(context, ScheduledTootActivity::class.java)
|
fun newIntent(context: Context) = Intent(context, ScheduledStatusActivity::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,18 +20,18 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.paging.PagingDataAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding
|
import com.keylesspalace.tusky.databinding.ItemScheduledStatusBinding
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
|
||||||
interface ScheduledTootActionListener {
|
interface ScheduledStatusActionListener {
|
||||||
fun edit(item: ScheduledStatus)
|
fun edit(item: ScheduledStatus)
|
||||||
fun delete(item: ScheduledStatus)
|
fun delete(item: ScheduledStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduledTootAdapter(
|
class ScheduledStatusAdapter(
|
||||||
val listener: ScheduledTootActionListener
|
val listener: ScheduledStatusActionListener
|
||||||
) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledTootBinding>>(
|
) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledStatusBinding>>(
|
||||||
object : DiffUtil.ItemCallback<ScheduledStatus>() {
|
object : DiffUtil.ItemCallback<ScheduledStatus>() {
|
||||||
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
|
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
|
@ -43,12 +43,12 @@ class ScheduledTootAdapter(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledTootBinding> {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledStatusBinding> {
|
||||||
val binding = ItemScheduledTootBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemScheduledStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledTootBinding>, position: Int) {
|
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledStatusBinding>, position: Int) {
|
||||||
getItem(position)?.let { item ->
|
getItem(position)?.let { item ->
|
||||||
holder.binding.edit.isEnabled = true
|
holder.binding.edit.isEnabled = true
|
||||||
holder.binding.delete.isEnabled = true
|
holder.binding.delete.isEnabled = true
|
|
@ -22,16 +22,16 @@ import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
|
||||||
class ScheduledTootPagingSourceFactory(
|
class ScheduledStatusPagingSourceFactory(
|
||||||
private val mastodonApi: MastodonApi
|
private val mastodonApi: MastodonApi
|
||||||
) : () -> ScheduledTootPagingSource {
|
) : () -> ScheduledStatusPagingSource {
|
||||||
|
|
||||||
private val scheduledTootsCache = mutableListOf<ScheduledStatus>()
|
private val scheduledTootsCache = mutableListOf<ScheduledStatus>()
|
||||||
|
|
||||||
private var pagingSource: ScheduledTootPagingSource? = null
|
private var pagingSource: ScheduledStatusPagingSource? = null
|
||||||
|
|
||||||
override fun invoke(): ScheduledTootPagingSource {
|
override fun invoke(): ScheduledStatusPagingSource {
|
||||||
return ScheduledTootPagingSource(mastodonApi, scheduledTootsCache).also {
|
return ScheduledStatusPagingSource(mastodonApi, scheduledTootsCache).also {
|
||||||
pagingSource = it
|
pagingSource = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,9 +42,9 @@ class ScheduledTootPagingSourceFactory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduledTootPagingSource(
|
class ScheduledStatusPagingSource(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val scheduledTootsCache: MutableList<ScheduledStatus>
|
private val scheduledStatusesCache: MutableList<ScheduledStatus>
|
||||||
) : PagingSource<String, ScheduledStatus>() {
|
) : PagingSource<String, ScheduledStatus>() {
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? {
|
override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? {
|
||||||
|
@ -52,11 +52,11 @@ class ScheduledTootPagingSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, ScheduledStatus> {
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, ScheduledStatus> {
|
||||||
return if (params is LoadParams.Refresh && scheduledTootsCache.isNotEmpty()) {
|
return if (params is LoadParams.Refresh && scheduledStatusesCache.isNotEmpty()) {
|
||||||
LoadResult.Page(
|
LoadResult.Page(
|
||||||
data = scheduledTootsCache,
|
data = scheduledStatusesCache,
|
||||||
prevKey = null,
|
prevKey = null,
|
||||||
nextKey = scheduledTootsCache.lastOrNull()?.id
|
nextKey = scheduledStatusesCache.lastOrNull()?.id
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
@ -71,7 +71,7 @@ class ScheduledTootPagingSource(
|
||||||
nextKey = result.lastOrNull()?.id
|
nextKey = result.lastOrNull()?.id
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("ScheduledTootPgngSrc", "Error loading scheduled statuses", e)
|
Log.w("ScheduledStatuses", "Error loading scheduled statuses", e)
|
||||||
LoadResult.Error(e)
|
LoadResult.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,12 +28,12 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ScheduledTootViewModel @Inject constructor(
|
class ScheduledStatusViewModel @Inject constructor(
|
||||||
val mastodonApi: MastodonApi,
|
val mastodonApi: MastodonApi,
|
||||||
val eventHub: EventHub
|
val eventHub: EventHub
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi)
|
private val pagingSourceFactory = ScheduledStatusPagingSourceFactory(mastodonApi)
|
||||||
|
|
||||||
val data = Pager(
|
val data = Pager(
|
||||||
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
|
@ -86,7 +86,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
private fun getPageTitle(position: Int): CharSequence {
|
private fun getPageTitle(position: Int): CharSequence {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> getString(R.string.title_statuses)
|
0 -> getString(R.string.title_posts)
|
||||||
1 -> getString(R.string.title_accounts)
|
1 -> getString(R.string.title_accounts)
|
||||||
2 -> getString(R.string.title_hashtags_dialog)
|
2 -> getString(R.string.title_hashtags_dialog)
|
||||||
else -> throw IllegalArgumentException("Unknown page index: $position")
|
else -> throw IllegalArgumentException("Unknown page index: $position")
|
||||||
|
|
|
@ -53,17 +53,15 @@ class SearchViewModel @Inject constructor(
|
||||||
val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false
|
val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false
|
||||||
val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
|
val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
|
||||||
|
|
||||||
private val loadedStatuses: MutableList<Pair<Status, StatusViewData.Concrete>> = mutableListOf()
|
private val loadedStatuses: MutableList<StatusViewData.Concrete> = mutableListOf()
|
||||||
|
|
||||||
private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) {
|
private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) {
|
||||||
it.statuses.map { status ->
|
it.statuses.map { status ->
|
||||||
val statusViewData = status.toViewData(
|
status.toViewData(
|
||||||
isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||||
isExpanded = alwaysOpenSpoiler,
|
isExpanded = alwaysOpenSpoiler,
|
||||||
isCollapsed = true
|
isCollapsed = true
|
||||||
)
|
)
|
||||||
|
|
||||||
Pair(status, statusViewData)
|
|
||||||
}.apply {
|
}.apply {
|
||||||
loadedStatuses.addAll(this)
|
loadedStatuses.addAll(this)
|
||||||
}
|
}
|
||||||
|
@ -100,11 +98,11 @@ class SearchViewModel @Inject constructor(
|
||||||
hashtagsPagingSourceFactory.newSearch(query)
|
hashtagsPagingSourceFactory.newSearch(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
|
fun removeItem(statusViewData: StatusViewData.Concrete) {
|
||||||
timelineCases.delete(status.first.id)
|
timelineCases.delete(statusViewData.id)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{
|
{
|
||||||
if (loadedStatuses.remove(status))
|
if (loadedStatuses.remove(statusViewData))
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
},
|
},
|
||||||
{ err ->
|
{ err ->
|
||||||
|
@ -114,82 +112,81 @@ class SearchViewModel @Inject constructor(
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expandedChange(status: Pair<Status, StatusViewData.Concrete>, expanded: Boolean) {
|
fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) {
|
||||||
val idx = loadedStatuses.indexOf(status)
|
val idx = loadedStatuses.indexOf(statusViewData)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isExpanded = expanded))
|
loadedStatuses[idx] = statusViewData.copy(isExpanded = expanded)
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reblog(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
|
fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
||||||
timelineCases.reblog(status.first.id, reblog)
|
timelineCases.reblog(statusViewData.id, reblog)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ setRebloggedForStatus(status, reblog) },
|
{ setRebloggedForStatus(statusViewData, reblog) },
|
||||||
{ t -> Log.d(TAG, "Failed to reblog status ${status.first.id}", t) }
|
{ t -> Log.d(TAG, "Failed to reblog status ${statusViewData.id}", t) }
|
||||||
)
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setRebloggedForStatus(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
|
private fun setRebloggedForStatus(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
||||||
status.first.reblogged = reblog
|
statusViewData.status.reblogged = reblog
|
||||||
status.first.reblog?.reblogged = reblog
|
statusViewData.status.reblog?.reblogged = reblog
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contentHiddenChange(status: Pair<Status, StatusViewData.Concrete>, isShowing: Boolean) {
|
fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) {
|
||||||
val idx = loadedStatuses.indexOf(status)
|
val idx = loadedStatuses.indexOf(statusViewData)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isShowingContent = isShowing))
|
loadedStatuses[idx] = statusViewData.copy(isShowingContent = isShowing)
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun collapsedChange(status: Pair<Status, StatusViewData.Concrete>, collapsed: Boolean) {
|
fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) {
|
||||||
val idx = loadedStatuses.indexOf(status)
|
val idx = loadedStatuses.indexOf(statusViewData)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isCollapsed = collapsed))
|
loadedStatuses[idx] = statusViewData.copy(isCollapsed = collapsed)
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun voteInPoll(status: Pair<Status, StatusViewData.Concrete>, choices: MutableList<Int>) {
|
fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList<Int>) {
|
||||||
val votedPoll = status.first.actionableStatus.poll!!.votedCopy(choices)
|
val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices)
|
||||||
updateStatus(status, votedPoll)
|
updateStatus(statusViewData, votedPoll)
|
||||||
timelineCases.voteInPoll(status.first.id, votedPoll.id, choices)
|
timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ newPoll -> updateStatus(status, newPoll) },
|
{ newPoll -> updateStatus(statusViewData, newPoll) },
|
||||||
{ t -> Log.d(TAG, "Failed to vote in poll: ${status.first.id}", t) }
|
{ t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) }
|
||||||
)
|
)
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStatus(status: Pair<Status, StatusViewData.Concrete>, newPoll: Poll) {
|
private fun updateStatus(statusViewData: StatusViewData.Concrete, newPoll: Poll) {
|
||||||
val idx = loadedStatuses.indexOf(status)
|
val idx = loadedStatuses.indexOf(statusViewData)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
val newStatus = status.first.copy(poll = newPoll)
|
val newStatus = statusViewData.status.copy(poll = newPoll)
|
||||||
val newViewData = status.second.copy(status = newStatus)
|
loadedStatuses[idx] = statusViewData.copy(status = newStatus)
|
||||||
loadedStatuses[idx] = Pair(newStatus, newViewData)
|
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun favorite(status: Pair<Status, StatusViewData.Concrete>, isFavorited: Boolean) {
|
fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) {
|
||||||
status.first.favourited = isFavorited
|
statusViewData.status.favourited = isFavorited
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
timelineCases.favourite(status.first.id, isFavorited)
|
timelineCases.favourite(statusViewData.id, isFavorited)
|
||||||
.onErrorReturnItem(status.first)
|
.onErrorReturnItem(statusViewData.status)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) {
|
fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) {
|
||||||
status.first.bookmarked = isBookmarked
|
statusViewData.status.bookmarked = isBookmarked
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
timelineCases.bookmark(status.first.id, isBookmarked)
|
timelineCases.bookmark(statusViewData.id, isBookmarked)
|
||||||
.onErrorReturnItem(status.first)
|
.onErrorReturnItem(statusViewData.status)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
@ -214,19 +211,15 @@ class SearchViewModel @Inject constructor(
|
||||||
return timelineCases.delete(id)
|
return timelineCases.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun muteConversation(status: Pair<Status, StatusViewData.Concrete>, mute: Boolean) {
|
fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) {
|
||||||
val idx = loadedStatuses.indexOf(status)
|
val idx = loadedStatuses.indexOf(statusViewData)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
val newStatus = status.first.copy(muted = mute)
|
val newStatus = statusViewData.status.copy(muted = mute)
|
||||||
val newPair = Pair(
|
loadedStatuses[idx] = statusViewData.copy(status = newStatus)
|
||||||
newStatus,
|
|
||||||
status.second.copy(status = newStatus)
|
|
||||||
)
|
|
||||||
loadedStatuses[idx] = newPair
|
|
||||||
statusesPagingSourceFactory.invalidate()
|
statusesPagingSourceFactory.invalidate()
|
||||||
}
|
}
|
||||||
timelineCases.muteConversation(status.first.id, mute)
|
timelineCases.muteConversation(statusViewData.id, mute)
|
||||||
.onErrorReturnItem(status.first)
|
.onErrorReturnItem(statusViewData.status)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,11 @@ import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.adapter.AccountViewHolder
|
import com.keylesspalace.tusky.adapter.AccountViewHolder
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
|
|
||||||
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
|
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
|
||||||
PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
|
PagingDataAdapter<TimelineAccount, AccountViewHolder>(ACCOUNT_COMPARATOR) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context)
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
@ -44,11 +44,11 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
|
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
|
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean =
|
||||||
oldItem.deepEquals(newItem)
|
oldItem == newItem
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean =
|
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean =
|
||||||
oldItem.id == newItem.id
|
oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.adapter.StatusViewHolder
|
import com.keylesspalace.tusky.adapter.StatusViewHolder
|
||||||
import com.keylesspalace.tusky.entity.Status
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
@ -29,7 +28,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
class SearchStatusesAdapter(
|
class SearchStatusesAdapter(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val statusListener: StatusActionListener
|
private val statusListener: StatusActionListener
|
||||||
) : PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, StatusViewHolder>(STATUS_COMPARATOR) {
|
) : PagingDataAdapter<StatusViewData.Concrete, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context)
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
@ -39,22 +38,18 @@ class SearchStatusesAdapter(
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
|
||||||
getItem(position)?.let { item ->
|
getItem(position)?.let { item ->
|
||||||
holder.setupWithStatus(item.second, statusListener, statusDisplayOptions)
|
holder.setupWithStatus(item, statusListener, statusDisplayOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun item(position: Int): Pair<Status, StatusViewData.Concrete>? {
|
|
||||||
return getItem(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Pair<Status, StatusViewData.Concrete>>() {
|
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
|
||||||
override fun areContentsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||||
oldItem == newItem
|
oldItem == newItem
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||||
oldItem.second.id == newItem.second.id
|
oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,12 @@ import androidx.paging.PagingData
|
||||||
import androidx.paging.PagingDataAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
|
import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class SearchAccountsFragment : SearchFragment<Account>() {
|
class SearchAccountsFragment : SearchFragment<TimelineAccount>() {
|
||||||
override fun createAdapter(): PagingDataAdapter<Account, *> {
|
override fun createAdapter(): PagingDataAdapter<TimelineAccount, *> {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||||
|
|
||||||
return SearchAccountsAdapter(
|
return SearchAccountsAdapter(
|
||||||
|
@ -34,7 +34,7 @@ class SearchAccountsFragment : SearchFragment<Account>() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val data: Flow<PagingData<Account>>
|
override val data: Flow<PagingData<TimelineAccount>>
|
||||||
get() = viewModel.accountsFlow
|
get() = viewModel.accountsFlow
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity
|
import com.keylesspalace.tusky.BottomSheetActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.ViewTagActivity
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchViewModel
|
import com.keylesspalace.tusky.components.search.SearchViewModel
|
||||||
import com.keylesspalace.tusky.databinding.FragmentSearchBinding
|
import com.keylesspalace.tusky.databinding.FragmentSearchBinding
|
||||||
|
@ -113,7 +113,7 @@ abstract class SearchFragment<T : Any> :
|
||||||
|
|
||||||
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
||||||
|
|
||||||
override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag))
|
override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||||
|
|
||||||
override fun onViewUrl(url: String) {
|
override fun onViewUrl(url: String) {
|
||||||
bottomSheetActivity?.viewUrl(url)
|
bottomSheetActivity?.viewUrl(url)
|
||||||
|
|
|
@ -40,7 +40,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
import autodispose2.autoDispose
|
import autodispose2.autoDispose
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.MainActivity
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
@ -55,23 +54,23 @@ import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.CardViewMode
|
import com.keylesspalace.tusky.util.CardViewMode
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
|
import com.keylesspalace.tusky.util.openLink
|
||||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
|
class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), StatusActionListener {
|
||||||
|
|
||||||
override val data: Flow<PagingData<Pair<Status, StatusViewData.Concrete>>>
|
override val data: Flow<PagingData<StatusViewData.Concrete>>
|
||||||
get() = viewModel.statusesFlow
|
get() = viewModel.statusesFlow
|
||||||
|
|
||||||
private val searchAdapter
|
private val searchAdapter
|
||||||
get() = super.adapter as SearchStatusesAdapter
|
get() = super.adapter as SearchStatusesAdapter
|
||||||
|
|
||||||
override fun createAdapter(): PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, *> {
|
override fun createAdapter(): PagingDataAdapter<StatusViewData.Concrete, *> {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||||
val statusDisplayOptions = StatusDisplayOptions(
|
val statusDisplayOptions = StatusDisplayOptions(
|
||||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||||
|
@ -92,37 +91,37 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||||
searchAdapter.item(position)?.let {
|
searchAdapter.peek(position)?.let {
|
||||||
viewModel.contentHiddenChange(it, isShowing)
|
viewModel.contentHiddenChange(it, isShowing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReply(position: Int) {
|
override fun onReply(position: Int) {
|
||||||
searchAdapter.item(position)?.first?.let { status ->
|
searchAdapter.peek(position)?.status?.let { status ->
|
||||||
reply(status)
|
reply(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||||
searchAdapter.item(position)?.let { status ->
|
searchAdapter.peek(position)?.let { status ->
|
||||||
viewModel.favorite(status, favourite)
|
viewModel.favorite(status, favourite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBookmark(bookmark: Boolean, position: Int) {
|
override fun onBookmark(bookmark: Boolean, position: Int) {
|
||||||
searchAdapter.item(position)?.let { status ->
|
searchAdapter.peek(position)?.let { status ->
|
||||||
viewModel.bookmark(status, bookmark)
|
viewModel.bookmark(status, bookmark)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMore(view: View, position: Int) {
|
override fun onMore(view: View, position: Int) {
|
||||||
searchAdapter.item(position)?.first?.let {
|
searchAdapter.peek(position)?.status?.let {
|
||||||
more(it, view, position)
|
more(it, view, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||||
searchAdapter.item(position)?.first?.actionableStatus?.let { actionable ->
|
searchAdapter.peek(position)?.status?.actionableStatus?.let { actionable ->
|
||||||
when (actionable.attachments[attachmentIndex].type) {
|
when (actionable.attachments[attachmentIndex].type) {
|
||||||
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
|
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
|
||||||
val attachments = AttachmentViewData.list(actionable)
|
val attachments = AttachmentViewData.list(actionable)
|
||||||
|
@ -143,27 +142,27 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Attachment.Type.UNKNOWN -> {
|
Attachment.Type.UNKNOWN -> {
|
||||||
LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context)
|
context?.openLink(actionable.attachments[attachmentIndex].url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewThread(position: Int) {
|
override fun onViewThread(position: Int) {
|
||||||
searchAdapter.item(position)?.first?.let { status ->
|
searchAdapter.peek(position)?.status?.let { status ->
|
||||||
val actionableStatus = status.actionableStatus
|
val actionableStatus = status.actionableStatus
|
||||||
bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url)
|
bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenReblog(position: Int) {
|
override fun onOpenReblog(position: Int) {
|
||||||
searchAdapter.item(position)?.first?.let { status ->
|
searchAdapter.peek(position)?.status?.let { status ->
|
||||||
bottomSheetActivity?.viewAccount(status.account.id)
|
bottomSheetActivity?.viewAccount(status.account.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||||
searchAdapter.item(position)?.let {
|
searchAdapter.peek(position)?.let {
|
||||||
viewModel.expandedChange(it, expanded)
|
viewModel.expandedChange(it, expanded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,25 +172,25 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||||
searchAdapter.item(position)?.let {
|
searchAdapter.peek(position)?.let {
|
||||||
viewModel.collapsedChange(it, isCollapsed)
|
viewModel.collapsedChange(it, isCollapsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
searchAdapter.item(position)?.let {
|
searchAdapter.peek(position)?.let {
|
||||||
viewModel.voteInPoll(it, choices)
|
viewModel.voteInPoll(it, choices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeItem(position: Int) {
|
private fun removeItem(position: Int) {
|
||||||
searchAdapter.item(position)?.let {
|
searchAdapter.peek(position)?.let {
|
||||||
viewModel.removeItem(it)
|
viewModel.removeItem(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReblog(reblog: Boolean, position: Int) {
|
override fun onReblog(reblog: Boolean, position: Int) {
|
||||||
searchAdapter.item(position)?.let { status ->
|
searchAdapter.peek(position)?.let { status ->
|
||||||
viewModel.reblog(status, reblog)
|
viewModel.reblog(status, reblog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,9 +227,6 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
val accountId = status.actionableStatus.account.id
|
val accountId = status.actionableStatus.account.id
|
||||||
val accountUsername = status.actionableStatus.account.username
|
val accountUsername = status.actionableStatus.account.username
|
||||||
val statusUrl = status.actionableStatus.url
|
val statusUrl = status.actionableStatus.url
|
||||||
val accounts = viewModel.getAllAccountsOrderedByActive()
|
|
||||||
var openAsTitle: String? = null
|
|
||||||
|
|
||||||
val loggedInAccountId = viewModel.activeAccount?.accountId
|
val loggedInAccountId = viewModel.activeAccount?.accountId
|
||||||
|
|
||||||
val popup = PopupMenu(view.context, view)
|
val popup = PopupMenu(view.context, view)
|
||||||
|
@ -261,17 +257,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
}
|
}
|
||||||
|
|
||||||
val openAsItem = popup.menu.findItem(R.id.status_open_as)
|
val openAsItem = popup.menu.findItem(R.id.status_open_as)
|
||||||
when (accounts.size) {
|
val openAsText = bottomSheetActivity?.openAsText
|
||||||
0, 1 -> openAsItem.isVisible = false
|
if (openAsText == null) {
|
||||||
2 -> for (account in accounts) {
|
openAsItem.isVisible = false
|
||||||
if (account !== viewModel.activeAccount) {
|
} else {
|
||||||
openAsTitle = String.format(getString(R.string.action_open_as), account.fullName)
|
openAsItem.title = openAsText
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> openAsTitle = String.format(getString(R.string.action_open_as), "…")
|
|
||||||
}
|
}
|
||||||
openAsItem.title = openAsTitle
|
|
||||||
|
|
||||||
val mutable = statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions)
|
val mutable = statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions)
|
||||||
val muteConversationItem = popup.menu.findItem(R.id.status_mute_conversation).apply {
|
val muteConversationItem = popup.menu.findItem(R.id.status_mute_conversation).apply {
|
||||||
|
@ -289,7 +280,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener { item ->
|
popup.setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.status_share_content -> {
|
R.id.post_share_content -> {
|
||||||
val statusToShare: Status = status.actionableStatus
|
val statusToShare: Status = status.actionableStatus
|
||||||
|
|
||||||
val sendIntent = Intent()
|
val sendIntent = Intent()
|
||||||
|
@ -300,15 +291,15 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
statusToShare.content.toString()
|
statusToShare.content.toString()
|
||||||
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_status_content_to)))
|
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_post_content_to)))
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
R.id.status_share_link -> {
|
R.id.post_share_link -> {
|
||||||
val sendIntent = Intent()
|
val sendIntent = Intent()
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
sendIntent.action = Intent.ACTION_SEND
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl)
|
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl)
|
||||||
sendIntent.type = "text/plain"
|
sendIntent.type = "text/plain"
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_link_to)))
|
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_post_link_to)))
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
R.id.status_copy_link -> {
|
R.id.status_copy_link -> {
|
||||||
|
@ -325,7 +316,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
R.id.status_mute_conversation -> {
|
R.id.status_mute_conversation -> {
|
||||||
searchAdapter.item(position)?.let { foundStatus ->
|
searchAdapter.peek(position)?.let { foundStatus ->
|
||||||
viewModel.muteConversation(foundStatus, status.muted != true)
|
viewModel.muteConversation(foundStatus, status.muted != true)
|
||||||
}
|
}
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
|
@ -396,21 +387,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
dialogTitle, false,
|
dialogTitle, false,
|
||||||
object : AccountSelectionListener {
|
object : AccountSelectionListener {
|
||||||
override fun onAccountSelected(account: AccountEntity) {
|
override fun onAccountSelected(account: AccountEntity) {
|
||||||
openAsAccount(statusUrl, account)
|
bottomSheetActivity?.openAsAccount(statusUrl, account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAsAccount(statusUrl: String, account: AccountEntity) {
|
|
||||||
viewModel.activeAccount = account
|
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
intent.putExtra(MainActivity.STATUS_URL, statusUrl)
|
|
||||||
startActivity(intent)
|
|
||||||
(activity as BaseActivity).finishWithoutSlideOutAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadAllMedia(status: Status) {
|
private fun downloadAllMedia(status: Status) {
|
||||||
Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show()
|
||||||
for ((_, url) in status.attachments) {
|
for ((_, url) in status.attachments) {
|
||||||
|
@ -442,7 +424,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
private fun showConfirmDeleteDialog(id: String, position: Int) {
|
private fun showConfirmDeleteDialog(id: String, position: Int) {
|
||||||
context?.let {
|
context?.let {
|
||||||
AlertDialog.Builder(it)
|
AlertDialog.Builder(it)
|
||||||
.setMessage(R.string.dialog_delete_toot_warning)
|
.setMessage(R.string.dialog_delete_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
viewModel.deleteStatus(id)
|
viewModel.deleteStatus(id)
|
||||||
removeItem(position)
|
removeItem(position)
|
||||||
|
@ -455,7 +437,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
|
private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
AlertDialog.Builder(it)
|
AlertDialog.Builder(it)
|
||||||
.setMessage(R.string.dialog_redraft_toot_warning)
|
.setMessage(R.string.dialog_redraft_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
viewModel.deleteStatus(id)
|
viewModel.deleteStatus(id)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -473,7 +455,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
||||||
val intent = ComposeActivity.startIntent(
|
val intent = ComposeActivity.startIntent(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
ComposeOptions(
|
ComposeOptions(
|
||||||
tootText = redraftStatus.text ?: "",
|
content = redraftStatus.text ?: "",
|
||||||
inReplyToId = redraftStatus.inReplyToId,
|
inReplyToId = redraftStatus.inReplyToId,
|
||||||
visibility = redraftStatus.visibility,
|
visibility = redraftStatus.visibility,
|
||||||
contentWarning = redraftStatus.spoilerText,
|
contentWarning = redraftStatus.spoilerText,
|
||||||
|
|
|
@ -38,6 +38,7 @@ import com.keylesspalace.tusky.AccountListActivity
|
||||||
import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent
|
import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
||||||
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.appstore.StatusComposedEvent
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||||
|
@ -89,9 +90,9 @@ class TimelineFragment :
|
||||||
|
|
||||||
private val viewModel: TimelineViewModel by lazy {
|
private val viewModel: TimelineViewModel by lazy {
|
||||||
if (kind == TimelineViewModel.Kind.HOME) {
|
if (kind == TimelineViewModel.Kind.HOME) {
|
||||||
ViewModelProvider(this, viewModelFactory).get(CachedTimelineViewModel::class.java)
|
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
|
||||||
} else {
|
} else {
|
||||||
ViewModelProvider(this, viewModelFactory).get(NetworkTimelineViewModel::class.java)
|
ViewModelProvider(this, viewModelFactory)[NetworkTimelineViewModel::class.java]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,8 +104,6 @@ class TimelineFragment :
|
||||||
|
|
||||||
private var isSwipeToRefreshEnabled = true
|
private var isSwipeToRefreshEnabled = true
|
||||||
|
|
||||||
private var eventRegistered = false
|
|
||||||
|
|
||||||
private var layoutManager: LinearLayoutManager? = null
|
private var layoutManager: LinearLayoutManager? = null
|
||||||
private var scrollListener: RecyclerView.OnScrollListener? = null
|
private var scrollListener: RecyclerView.OnScrollListener? = null
|
||||||
private var hideFab = false
|
private var hideFab = false
|
||||||
|
@ -137,7 +136,7 @@ class TimelineFragment :
|
||||||
|
|
||||||
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
val statusDisplayOptions = StatusDisplayOptions(
|
val statusDisplayOptions = StatusDisplayOptions(
|
||||||
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||||
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
||||||
|
@ -183,7 +182,7 @@ class TimelineFragment :
|
||||||
if (adapter.itemCount == 0) {
|
if (adapter.itemCount == 0) {
|
||||||
when (loadState.refresh) {
|
when (loadState.refresh) {
|
||||||
is LoadState.NotLoading -> {
|
is LoadState.NotLoading -> {
|
||||||
if (loadState.append is LoadState.NotLoading) {
|
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||||
binding.statusView.show()
|
binding.statusView.show()
|
||||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
||||||
}
|
}
|
||||||
|
@ -218,43 +217,14 @@ class TimelineFragment :
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.statuses.collectLatest { pagingData ->
|
viewModel.statuses.collectLatest { pagingData ->
|
||||||
adapter.submitData(pagingData)
|
adapter.submitData(pagingData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupSwipeRefreshLayout() {
|
|
||||||
binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
|
||||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
|
||||||
binding.recyclerView.setAccessibilityDelegateCompat(
|
|
||||||
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos ->
|
|
||||||
adapter.peek(pos)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
|
|
||||||
binding.recyclerView.addItemDecoration(divider)
|
|
||||||
|
|
||||||
// CWs are expanded without animation, buttons animate itself, we don't need it basically
|
|
||||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
||||||
super.onActivityCreated(savedInstanceState)
|
|
||||||
|
|
||||||
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
|
|
||||||
* guaranteed to be set until then. */
|
|
||||||
if (actionButtonPresent()) {
|
if (actionButtonPresent()) {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
hideFab = preferences.getBoolean("fabHide", false)
|
hideFab = preferences.getBoolean("fabHide", false)
|
||||||
scrollListener = object : RecyclerView.OnScrollListener() {
|
scrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
@ -276,23 +246,47 @@ class TimelineFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventRegistered) {
|
eventHub.events
|
||||||
eventHub.events
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
.subscribe { event ->
|
||||||
.subscribe { event ->
|
when (event) {
|
||||||
when (event) {
|
is PreferenceChangedEvent -> {
|
||||||
is PreferenceChangedEvent -> {
|
onPreferenceChanged(event.preferenceKey)
|
||||||
onPreferenceChanged(event.preferenceKey)
|
}
|
||||||
}
|
is StatusComposedEvent -> {
|
||||||
is StatusComposedEvent -> {
|
val status = event.status
|
||||||
val status = event.status
|
handleStatusComposeEvent(status)
|
||||||
handleStatusComposeEvent(status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventRegistered = true
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupSwipeRefreshLayout() {
|
||||||
|
binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
||||||
|
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
binding.recyclerView.setAccessibilityDelegateCompat(
|
||||||
|
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos ->
|
||||||
|
if (pos in 0 until adapter.itemCount) {
|
||||||
|
adapter.peek(pos)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
|
val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
|
||||||
|
binding.recyclerView.addItemDecoration(divider)
|
||||||
|
|
||||||
|
// CWs are expanded without animation, buttons animate itself, we don't need it basically
|
||||||
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
|
@ -407,7 +401,7 @@ class TimelineFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPreferenceChanged(key: String) {
|
private fun onPreferenceChanged(key: String) {
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
when (key) {
|
when (key) {
|
||||||
PrefKeys.FAB_HIDE -> {
|
PrefKeys.FAB_HIDE -> {
|
||||||
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||||
|
@ -417,7 +411,7 @@ class TimelineFragment :
|
||||||
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
||||||
if (enabled != oldMediaPreviewEnabled) {
|
if (enabled != oldMediaPreviewEnabled) {
|
||||||
adapter.mediaPreviewEnabled = enabled
|
adapter.mediaPreviewEnabled = enabled
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,7 +457,7 @@ class TimelineFragment :
|
||||||
talkBackWasEnabled = a11yManager?.isEnabled == true
|
talkBackWasEnabled = a11yManager?.isEnabled == true
|
||||||
Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled")
|
Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled")
|
||||||
if (talkBackWasEnabled && !wasEnabled) {
|
if (talkBackWasEnabled && !wasEnabled) {
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||||
}
|
}
|
||||||
startUpdateTimestamp()
|
startUpdateTimestamp()
|
||||||
}
|
}
|
||||||
|
@ -474,14 +468,14 @@ class TimelineFragment :
|
||||||
* Auto dispose observable on pause
|
* Auto dispose observable on pause
|
||||||
*/
|
*/
|
||||||
private fun startUpdateTimestamp() {
|
private fun startUpdateTimestamp() {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
||||||
if (!useAbsoluteTime) {
|
if (!useAbsoluteTime) {
|
||||||
Observable.interval(1, TimeUnit.MINUTES)
|
Observable.interval(1, TimeUnit.MINUTES)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(this, Lifecycle.Event.ON_PAUSE)
|
.autoDispose(this, Lifecycle.Event.ON_PAUSE)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyItemRangeChanged(0, adapter.itemCount, listOf(StatusBaseViewHolder.Key.KEY_CREATED))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,10 @@ class TimelinePagingAdapter(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
VIEW_TYPE_STATUS -> {
|
VIEW_TYPE_STATUS -> {
|
||||||
|
@ -110,7 +114,7 @@ class TimelinePagingAdapter(
|
||||||
oldItem: StatusViewData,
|
oldItem: StatusViewData,
|
||||||
newItem: StatusViewData
|
newItem: StatusViewData
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return oldItem.viewDataId == newItem.viewDataId
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
override fun areContentsTheSame(
|
||||||
|
@ -124,7 +128,7 @@ class TimelinePagingAdapter(
|
||||||
oldItem: StatusViewData,
|
oldItem: StatusViewData,
|
||||||
newItem: StatusViewData
|
newItem: StatusViewData
|
||||||
): Any? {
|
): Any? {
|
||||||
return if (oldItem === newItem) {
|
return if (oldItem == newItem) {
|
||||||
// If items are equal - update timestamp only
|
// If items are equal - update timestamp only
|
||||||
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
||||||
} else // If items are different - update the whole view holder
|
} else // If items are different - update the whole view holder
|
||||||
|
|
|
@ -23,11 +23,12 @@ import com.google.gson.reflect.TypeToken
|
||||||
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Account
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
@ -41,8 +42,9 @@ data class Placeholder(
|
||||||
private val attachmentArrayListType = object : TypeToken<ArrayList<Attachment>>() {}.type
|
private val attachmentArrayListType = object : TypeToken<ArrayList<Attachment>>() {}.type
|
||||||
private val emojisListType = object : TypeToken<List<Emoji>>() {}.type
|
private val emojisListType = object : TypeToken<List<Emoji>>() {}.type
|
||||||
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
||||||
|
private val tagListType = object : TypeToken<List<HashTag>>() {}.type
|
||||||
|
|
||||||
fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||||
return TimelineAccountEntity(
|
return TimelineAccountEntity(
|
||||||
serverId = id,
|
serverId = id,
|
||||||
timelineUserId = accountId,
|
timelineUserId = accountId,
|
||||||
|
@ -56,25 +58,16 @@ fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineAccountEntity.toAccount(gson: Gson): Account {
|
fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
||||||
return Account(
|
return TimelineAccount(
|
||||||
id = serverId,
|
id = serverId,
|
||||||
localUsername = localUsername,
|
localUsername = localUsername,
|
||||||
username = username,
|
username = username,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
note = SpannedString(""),
|
|
||||||
url = url,
|
url = url,
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
header = "",
|
|
||||||
locked = false,
|
|
||||||
followingCount = 0,
|
|
||||||
followersCount = 0,
|
|
||||||
statusesCount = 0,
|
|
||||||
source = null,
|
|
||||||
bot = bot,
|
bot = bot,
|
||||||
emojis = gson.fromJson(emojis, emojisListType),
|
emojis = gson.fromJson(emojis, emojisListType)
|
||||||
fields = null,
|
|
||||||
moved = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +92,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
||||||
visibility = Status.Visibility.UNKNOWN,
|
visibility = Status.Visibility.UNKNOWN,
|
||||||
attachments = null,
|
attachments = null,
|
||||||
mentions = null,
|
mentions = null,
|
||||||
|
tags = null,
|
||||||
application = null,
|
application = null,
|
||||||
reblogServerId = null,
|
reblogServerId = null,
|
||||||
reblogAccountId = null,
|
reblogAccountId = null,
|
||||||
|
@ -138,6 +132,7 @@ fun Status.toEntity(
|
||||||
visibility = actionableStatus.visibility,
|
visibility = actionableStatus.visibility,
|
||||||
attachments = actionableStatus.attachments.let(gson::toJson),
|
attachments = actionableStatus.attachments.let(gson::toJson),
|
||||||
mentions = actionableStatus.mentions.let(gson::toJson),
|
mentions = actionableStatus.mentions.let(gson::toJson),
|
||||||
|
tags = actionableStatus.tags.let(gson::toJson),
|
||||||
application = actionableStatus.application.let(gson::toJson),
|
application = actionableStatus.application.let(gson::toJson),
|
||||||
reblogServerId = reblog?.id,
|
reblogServerId = reblog?.id,
|
||||||
reblogAccountId = reblog?.let { this.account.id },
|
reblogAccountId = reblog?.let { this.account.id },
|
||||||
|
@ -157,6 +152,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
|
|
||||||
val attachments: ArrayList<Attachment> = gson.fromJson(status.attachments, attachmentArrayListType) ?: arrayListOf()
|
val attachments: ArrayList<Attachment> = gson.fromJson(status.attachments, attachmentArrayListType) ?: arrayListOf()
|
||||||
val mentions: List<Status.Mention> = gson.fromJson(status.mentions, mentionListType) ?: emptyList()
|
val mentions: List<Status.Mention> = gson.fromJson(status.mentions, mentionListType) ?: emptyList()
|
||||||
|
val tags: List<HashTag>? = gson.fromJson(status.tags, tagListType)
|
||||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||||
val emojis: List<Emoji> = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
|
val emojis: List<Emoji> = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
|
||||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||||
|
@ -183,6 +179,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
visibility = status.visibility,
|
visibility = status.visibility,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
|
tags = tags,
|
||||||
application = application,
|
application = application,
|
||||||
pinned = false,
|
pinned = false,
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
|
@ -211,6 +208,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
visibility = status.visibility,
|
visibility = status.visibility,
|
||||||
attachments = ArrayList(),
|
attachments = ArrayList(),
|
||||||
mentions = listOf(),
|
mentions = listOf(),
|
||||||
|
tags = listOf(),
|
||||||
application = null,
|
application = null,
|
||||||
pinned = status.pinned,
|
pinned = status.pinned,
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
|
@ -239,6 +237,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
visibility = status.visibility,
|
visibility = status.visibility,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
|
tags = tags,
|
||||||
application = application,
|
application = application,
|
||||||
pinned = status.pinned,
|
pinned = status.pinned,
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.keylesspalace.tusky.components.timeline.util
|
||||||
|
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
fun Throwable.isExpected() = this is IOException || this is HttpException
|
||||||
|
|
||||||
|
inline fun <T> ifExpected(
|
||||||
|
t: Throwable,
|
||||||
|
cb: () -> T
|
||||||
|
): T {
|
||||||
|
if (t.isExpected()) {
|
||||||
|
return cb()
|
||||||
|
} else {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,13 +23,13 @@ import androidx.room.withTransaction
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.dec
|
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
|
@ -101,15 +101,22 @@ class CachedTimelineRemoteMediator(
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val overlappedStatuses = replaceStatusRange(statuses, state)
|
val overlappedStatuses = replaceStatusRange(statuses, state)
|
||||||
|
|
||||||
if (loadType == LoadType.REFRESH && overlappedStatuses == 0 && statuses.isNotEmpty() && !dbEmpty) {
|
/* In case we loaded a whole page and there was no overlap with existing statuses,
|
||||||
|
we insert a placeholder because there might be even more unknown statuses */
|
||||||
|
if (loadType == LoadType.REFRESH && overlappedStatuses == 0 && statuses.size == state.config.pageSize && !dbEmpty) {
|
||||||
|
/* This overrides the last of the newly loaded statuses with a placeholder
|
||||||
|
to guarantee the placeholder has an id that exists on the server as not all
|
||||||
|
servers handle client generated ids as expected */
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id)
|
Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return MediatorResult.Error(e)
|
return ifExpected(e) {
|
||||||
|
MediatorResult.Error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,14 +34,13 @@ import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||||
import com.keylesspalace.tusky.components.timeline.toViewData
|
import com.keylesspalace.tusky.components.timeline.toViewData
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
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.network.TimelineCases
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.dec
|
|
||||||
import com.keylesspalace.tusky.util.inc
|
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -70,7 +69,14 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
override val statuses = Pager(
|
override val statuses = Pager(
|
||||||
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
||||||
remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, gson),
|
remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, gson),
|
||||||
pagingSourceFactory = { db.timelineDao().getStatusesForAccount(accountManager.activeAccount!!.id) }
|
pagingSourceFactory = {
|
||||||
|
val activeAccount = accountManager.activeAccount
|
||||||
|
if (activeAccount == null) {
|
||||||
|
EmptyTimelinePagingSource()
|
||||||
|
} else {
|
||||||
|
db.timelineDao().getStatuses(activeAccount.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
).flow
|
).flow
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData.map { timelineStatus ->
|
pagingData.map { timelineStatus ->
|
||||||
|
@ -141,9 +147,11 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
||||||
|
|
||||||
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
val response = db.withTransaction {
|
||||||
|
val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId)
|
||||||
val response = api.homeTimeline(maxId = placeholderId.inc(), sinceId = nextPlaceholderId, limit = 20).await()
|
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
||||||
|
api.homeTimeline(maxId = idAbovePlaceholder, sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE)
|
||||||
|
}.await()
|
||||||
|
|
||||||
val statuses = response.body()
|
val statuses = response.body()
|
||||||
if (!response.isSuccessful || statuses == null) {
|
if (!response.isSuccessful || statuses == null) {
|
||||||
|
@ -177,14 +185,21 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overlappedStatuses == 0 && statuses.isNotEmpty()) {
|
/* In case we loaded a whole page and there was no overlap with existing statuses,
|
||||||
|
we insert a placeholder because there might be even more unknown statuses */
|
||||||
|
if (overlappedStatuses == 0 && statuses.size == LOAD_AT_ONCE) {
|
||||||
|
/* This overrides the last of the newly loaded statuses with a placeholder
|
||||||
|
to guarantee the placeholder has an id that exists on the server as not all
|
||||||
|
servers handle client generated ids as expected */
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id)
|
Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
loadMoreFailed(placeholderId, e)
|
ifExpected(e) {
|
||||||
|
loadMoreFailed(placeholderId, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,10 +229,7 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
override fun fullReload() {
|
override fun fullReload() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
db.runInTransaction {
|
db.timelineDao().removeAll(activeAccount.id)
|
||||||
db.timelineDao().removeAllForAccount(activeAccount.id)
|
|
||||||
db.timelineDao().removeAllUsersForAccount(activeAccount.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -19,9 +19,9 @@ import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.paging.LoadType
|
import androidx.paging.LoadType
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.RemoteMediator
|
import androidx.paging.RemoteMediator
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.util.HttpHeaderLink
|
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||||
import com.keylesspalace.tusky.util.dec
|
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
@ -92,7 +92,7 @@ class NetworkTimelineRemoteMediator(
|
||||||
viewModel.statusData.addAll(0, data)
|
viewModel.statusData.addAll(0, data)
|
||||||
|
|
||||||
if (insertPlaceholder) {
|
if (insertPlaceholder) {
|
||||||
viewModel.statusData.add(statuses.size, StatusViewData.Placeholder(statuses.last().id.dec(), false))
|
viewModel.statusData[statuses.size - 1] = StatusViewData.Placeholder(statuses.last().id, false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val linkHeader = statusResponse.headers()["Link"]
|
val linkHeader = statusResponse.headers()["Link"]
|
||||||
|
@ -107,7 +107,9 @@ class NetworkTimelineRemoteMediator(
|
||||||
viewModel.currentSource?.invalidate()
|
viewModel.currentSource?.invalidate()
|
||||||
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return MediatorResult.Error(e)
|
return ifExpected(e) {
|
||||||
|
MediatorResult.Error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,14 +28,16 @@ import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.FavoriteEvent
|
import com.keylesspalace.tusky.appstore.FavoriteEvent
|
||||||
import com.keylesspalace.tusky.appstore.PinEvent
|
import com.keylesspalace.tusky.appstore.PinEvent
|
||||||
import com.keylesspalace.tusky.appstore.ReblogEvent
|
import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.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.network.TimelineCases
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
import com.keylesspalace.tusky.util.getDomain
|
||||||
import com.keylesspalace.tusky.util.inc
|
import com.keylesspalace.tusky.util.isLessThan
|
||||||
|
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -43,6 +45,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,7 +120,7 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
override fun removeAllByInstance(instance: String) {
|
override fun removeAllByInstance(instance: String) {
|
||||||
statusData.removeAll { vd ->
|
statusData.removeAll { vd ->
|
||||||
val status = vd.asStatusOrNull()?.status ?: return@removeAll false
|
val status = vd.asStatusOrNull()?.status ?: return@removeAll false
|
||||||
LinkHelper.getDomain(status.account.url) == instance
|
getDomain(status.account.url) == instance
|
||||||
}
|
}
|
||||||
currentSource?.invalidate()
|
currentSource?.invalidate()
|
||||||
}
|
}
|
||||||
|
@ -133,8 +136,14 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
override fun loadMore(placeholderId: String) {
|
override fun loadMore(placeholderId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
|
val placeholderIndex =
|
||||||
|
statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId }
|
||||||
|
statusData[placeholderIndex] = StatusViewData.Placeholder(placeholderId, isLoading = true)
|
||||||
|
|
||||||
|
val idAbovePlaceholder = statusData.getOrNull(placeholderIndex - 1)?.id
|
||||||
|
|
||||||
val statusResponse = fetchStatusesForKind(
|
val statusResponse = fetchStatusesForKind(
|
||||||
fromId = placeholderId.inc(),
|
fromId = idAbovePlaceholder,
|
||||||
uptoId = null,
|
uptoId = null,
|
||||||
limit = 20
|
limit = 20
|
||||||
)
|
)
|
||||||
|
@ -145,32 +154,53 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusData.removeAt(placeholderIndex)
|
||||||
|
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
|
val data: MutableList<StatusViewData> = statuses.map { status ->
|
||||||
val data = statuses.map { status ->
|
|
||||||
val oldStatus = statusData.find { s ->
|
|
||||||
s.asStatusOrNull()?.id == status.id
|
|
||||||
}?.asStatusOrNull()
|
|
||||||
|
|
||||||
val contentShowing = oldStatus?.isShowingContent ?: activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive
|
|
||||||
val expanded = oldStatus?.isExpanded ?: activeAccount.alwaysOpenSpoiler
|
|
||||||
val contentCollapsed = oldStatus?.isCollapsed ?: true
|
|
||||||
|
|
||||||
status.toViewData(
|
status.toViewData(
|
||||||
isShowingContent = contentShowing,
|
isShowingContent = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||||
isExpanded = expanded,
|
isExpanded = activeAccount.alwaysOpenSpoiler,
|
||||||
isCollapsed = contentCollapsed
|
isCollapsed = true
|
||||||
)
|
)
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
if (statuses.isNotEmpty()) {
|
||||||
|
val firstId = statuses.first().id
|
||||||
|
val lastId = statuses.last().id
|
||||||
|
val overlappedFrom = statusData.indexOfFirst { it.asStatusOrNull()?.id?.isLessThanOrEqual(firstId) ?: false }
|
||||||
|
val overlappedTo = statusData.indexOfFirst { it.asStatusOrNull()?.id?.isLessThan(lastId) ?: false }
|
||||||
|
|
||||||
|
if (overlappedFrom < overlappedTo) {
|
||||||
|
data.mapIndexed { i, status -> i to statusData.firstOrNull { it.asStatusOrNull()?.id == status.id }?.asStatusOrNull() }
|
||||||
|
.filter { (_, oldStatus) -> oldStatus != null }
|
||||||
|
.forEach { (i, oldStatus) ->
|
||||||
|
data[i] = data[i].asStatusOrNull()!!
|
||||||
|
.copy(
|
||||||
|
isShowingContent = oldStatus!!.isShowingContent,
|
||||||
|
isExpanded = oldStatus.isExpanded,
|
||||||
|
isCollapsed = oldStatus.isCollapsed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusData.removeAll { status ->
|
||||||
|
when (status) {
|
||||||
|
is StatusViewData.Placeholder -> lastId.isLessThan(status.id) && status.id.isLessThanOrEqual(firstId)
|
||||||
|
is StatusViewData.Concrete -> lastId.isLessThan(status.id) && status.id.isLessThanOrEqual(firstId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data[data.size - 1] = StatusViewData.Placeholder(statuses.last().id, isLoading = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val index =
|
statusData.addAll(placeholderIndex, data)
|
||||||
statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId }
|
|
||||||
statusData.removeAt(index)
|
|
||||||
statusData.addAll(index, data)
|
|
||||||
|
|
||||||
currentSource?.invalidate()
|
currentSource?.invalidate()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
loadMoreFailed(placeholderId, e)
|
ifExpected(e) {
|
||||||
|
loadMoreFailed(placeholderId, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,10 +240,12 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fullReload() {
|
override fun fullReload() {
|
||||||
|
nextKey = statusData.firstOrNull { it is StatusViewData.Concrete }?.asStatusOrNull()?.id
|
||||||
statusData.clear()
|
statusData.clear()
|
||||||
currentSource?.invalidate()
|
currentSource?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, HttpException::class)
|
||||||
suspend fun fetchStatusesForKind(
|
suspend fun fetchStatusesForKind(
|
||||||
fromId: String?,
|
fromId: String?,
|
||||||
uptoId: String?,
|
uptoId: String?,
|
||||||
|
|
|
@ -33,6 +33,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.appstore.ReblogEvent
|
import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||||
import com.keylesspalace.tusky.appstore.StatusDeletedEvent
|
import com.keylesspalace.tusky.appstore.StatusDeletedEvent
|
||||||
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
|
@ -46,8 +47,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.asFlow
|
import kotlinx.coroutines.rx3.asFlow
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
abstract class TimelineViewModel(
|
abstract class TimelineViewModel(
|
||||||
private val timelineCases: TimelineCases,
|
private val timelineCases: TimelineCases,
|
||||||
|
@ -291,19 +290,6 @@ abstract class TimelineViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isExpectedRequestException(t: Exception) = t is IOException || t is HttpException
|
|
||||||
|
|
||||||
private inline fun ifExpected(
|
|
||||||
t: Exception,
|
|
||||||
cb: () -> Unit
|
|
||||||
) {
|
|
||||||
if (isExpectedRequestException(t)) {
|
|
||||||
cb()
|
|
||||||
} else {
|
|
||||||
throw t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "TimelineVM"
|
private const val TAG = "TimelineVM"
|
||||||
internal const val LOAD_AT_ONCE = 30
|
internal const val LOAD_AT_ONCE = 30
|
||||||
|
|
|
@ -29,10 +29,9 @@ import java.io.File;
|
||||||
/**
|
/**
|
||||||
* DB version & declare DAO
|
* DB version & declare DAO
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@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 = 28)
|
}, version = 31)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
@ -457,4 +456,31 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
"ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)");
|
"ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_28_29 = new Migration(28, 29) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_tags` TEXT");
|
||||||
|
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `tags` TEXT");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_29_30 = new Migration(29, 30) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `charactersReservedPerUrl` INTEGER");
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `minPollDuration` INTEGER");
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollDuration` INTEGER");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_30_31 = new Migration(30, 31) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
|
||||||
|
// no actual scheme change, but placeholder ids are now used differently so the cache needs to be cleared to avoid bugs
|
||||||
|
database.execSQL("DELETE FROM `TimelineAccountEntity`");
|
||||||
|
database.execSQL("DELETE FROM `TimelineStatusEntity`");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
||||||
import com.keylesspalace.tusky.createTabDataFromId
|
import com.keylesspalace.tusky.createTabDataFromId
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
@ -119,6 +120,16 @@ class Converters @Inject constructor (
|
||||||
return gson.fromJson(mentionListJson, object : TypeToken<List<Status.Mention>>() {}.type)
|
return gson.fromJson(mentionListJson, object : TypeToken<List<Status.Mention>>() {}.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun tagListToJson(tagArray: List<HashTag>?): String? {
|
||||||
|
return gson.toJson(tagArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun jsonToTagArray(tagListJson: String?): List<HashTag>? {
|
||||||
|
return gson.fromJson(tagListJson, object : TypeToken<List<HashTag>>() {}.type)
|
||||||
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun dateToLong(date: Date): Long {
|
fun dateToLong(date: Date): Long {
|
||||||
return date.time
|
return date.time
|
||||||
|
|
|
@ -28,5 +28,8 @@ data class InstanceEntity(
|
||||||
val maximumTootCharacters: Int?,
|
val maximumTootCharacters: Int?,
|
||||||
val maxPollOptions: Int?,
|
val maxPollOptions: Int?,
|
||||||
val maxPollOptionLength: Int?,
|
val maxPollOptionLength: Int?,
|
||||||
|
val minPollDuration: Int?,
|
||||||
|
val maxPollDuration: Int?,
|
||||||
|
val charactersReservedPerUrl: Int?,
|
||||||
val version: String?
|
val version: String?
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,7 +35,7 @@ abstract class TimelineDao {
|
||||||
SELECT s.serverId, s.url, s.timelineUserId,
|
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.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
||||||
s.spoilerText, s.visibility, s.mentions, 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.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
|
s.content, s.attachments, s.poll, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
|
||||||
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',
|
||||||
|
@ -51,7 +51,7 @@ LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND
|
||||||
WHERE s.timelineUserId = :account
|
WHERE s.timelineUserId = :account
|
||||||
ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC"""
|
ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC"""
|
||||||
)
|
)
|
||||||
abstract fun getStatusesForAccount(account: Long): PagingSource<Int, TimelineStatusWithAccount>
|
abstract fun getStatuses(account: Long): PagingSource<Int, TimelineStatusWithAccount>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
|
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
|
||||||
|
@ -86,11 +86,20 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId =
|
||||||
)
|
)
|
||||||
abstract fun removeAllByUser(accountId: Long, userId: String)
|
abstract fun removeAllByUser(accountId: Long, userId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes everything in the TimelineStatusEntity and TimelineAccountEntity tables for one user account
|
||||||
|
* @param accountId id of the account for which to clean tables
|
||||||
|
*/
|
||||||
|
suspend fun removeAll(accountId: Long) {
|
||||||
|
removeAllStatuses(accountId)
|
||||||
|
removeAllAccounts(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
@Query("DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId")
|
@Query("DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId")
|
||||||
abstract fun removeAllForAccount(accountId: Long)
|
abstract suspend fun removeAllStatuses(accountId: Long)
|
||||||
|
|
||||||
@Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId")
|
@Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId")
|
||||||
abstract fun removeAllUsersForAccount(accountId: Long)
|
abstract suspend fun removeAllAccounts(accountId: Long)
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId
|
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId
|
||||||
|
@ -98,6 +107,16 @@ AND serverId = :statusId"""
|
||||||
)
|
)
|
||||||
abstract fun delete(accountId: Long, statusId: String)
|
abstract fun delete(accountId: Long, statusId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans the TimelineStatusEntity and TimelineAccountEntity tables from old entries.
|
||||||
|
* @param accountId id of the account for which to clean tables
|
||||||
|
* @param limit how many statuses to keep
|
||||||
|
*/
|
||||||
|
suspend fun cleanup(accountId: Long, limit: Int) {
|
||||||
|
cleanupStatuses(accountId, limit)
|
||||||
|
cleanupAccounts(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans the TimelineStatusEntity table from old status entries.
|
* Cleans the TimelineStatusEntity table from old status entries.
|
||||||
* @param accountId id of the account for which to clean statuses
|
* @param accountId id of the account for which to clean statuses
|
||||||
|
@ -108,7 +127,7 @@ AND serverId = :statusId"""
|
||||||
(SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :limit)
|
(SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :limit)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract suspend fun cleanup(accountId: Long, limit: Int)
|
abstract suspend fun cleanupStatuses(accountId: Long, limit: Int)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans the TimelineAccountEntity table from accounts that are no longer referenced in the TimelineStatusEntity table
|
* Cleans the TimelineAccountEntity table from accounts that are no longer referenced in the TimelineStatusEntity table
|
||||||
|
@ -167,6 +186,15 @@ AND timelineUserId = :accountId
|
||||||
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
||||||
abstract suspend fun getTopPlaceholderId(accountId: Long): String?
|
abstract suspend fun getTopPlaceholderId(accountId: Long): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the id directly above [serverId], or null if [serverId] is the id of the top status
|
||||||
|
*/
|
||||||
|
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND (LENGTH(:serverId) < LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId < serverId)) ORDER BY LENGTH(serverId) ASC, serverId ASC LIMIT 1")
|
||||||
|
abstract suspend fun getIdAbove(accountId: Long, serverId: String): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the id of the next placeholder after [serverId]
|
||||||
|
*/
|
||||||
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
||||||
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
|
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ data class TimelineStatusEntity(
|
||||||
val visibility: Status.Visibility,
|
val visibility: Status.Visibility,
|
||||||
val attachments: String?,
|
val attachments: String?,
|
||||||
val mentions: String?,
|
val mentions: String?,
|
||||||
|
val tags: String?,
|
||||||
val application: String?,
|
val application: String?,
|
||||||
val reblogServerId: String?, // if it has a reblogged status, it's id is stored here
|
val reblogServerId: String?, // if it has a reblogged status, it's id is stored here
|
||||||
val reblogAccountId: String?,
|
val reblogAccountId: String?,
|
||||||
|
|
|
@ -22,23 +22,22 @@ import com.keylesspalace.tusky.EditProfileActivity
|
||||||
import com.keylesspalace.tusky.FiltersActivity
|
import com.keylesspalace.tusky.FiltersActivity
|
||||||
import com.keylesspalace.tusky.LicenseActivity
|
import com.keylesspalace.tusky.LicenseActivity
|
||||||
import com.keylesspalace.tusky.ListsActivity
|
import com.keylesspalace.tusky.ListsActivity
|
||||||
import com.keylesspalace.tusky.LoginActivity
|
|
||||||
import com.keylesspalace.tusky.MainActivity
|
import com.keylesspalace.tusky.MainActivity
|
||||||
import com.keylesspalace.tusky.ModalTimelineActivity
|
|
||||||
import com.keylesspalace.tusky.SplashActivity
|
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.ViewTagActivity
|
|
||||||
import com.keylesspalace.tusky.ViewThreadActivity
|
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
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
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.ScheduledTootActivity
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
@ -71,12 +70,6 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesAccountListActivity(): AccountListActivity
|
abstract fun contributesAccountListActivity(): AccountListActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
|
||||||
abstract fun contributesModalTimelineActivity(): ModalTimelineActivity
|
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
|
||||||
abstract fun contributesViewTagActivity(): ViewTagActivity
|
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesViewThreadActivity(): ViewThreadActivity
|
abstract fun contributesViewThreadActivity(): ViewThreadActivity
|
||||||
|
|
||||||
|
@ -93,7 +86,7 @@ abstract class ActivitiesModule {
|
||||||
abstract fun contributesLoginActivity(): LoginActivity
|
abstract fun contributesLoginActivity(): LoginActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSplashActivity(): SplashActivity
|
abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesPreferencesActivity(): PreferencesActivity
|
abstract fun contributesPreferencesActivity(): PreferencesActivity
|
||||||
|
@ -117,11 +110,14 @@ abstract class ActivitiesModule {
|
||||||
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
|
abstract fun contributesScheduledStatusActivity(): ScheduledStatusActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesDraftActivity(): DraftsActivity
|
abstract fun contributesDraftActivity(): DraftsActivity
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributesSplashActivity(): SplashActivity
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,8 @@ class AppModule {
|
||||||
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
|
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
|
||||||
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
|
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
|
||||||
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
|
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
|
||||||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28
|
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
||||||
|
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.di
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import com.keylesspalace.tusky.service.SendTootService
|
import com.keylesspalace.tusky.service.SendStatusService
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
abstract class ServicesModule {
|
abstract class ServicesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSendTootService(): SendTootService
|
abstract fun contributesSendStatusService(): SendStatusService
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
||||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel
|
||||||
import com.keylesspalace.tusky.components.search.SearchViewModel
|
import com.keylesspalace.tusky.components.search.SearchViewModel
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
|
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||||
|
@ -85,8 +85,8 @@ abstract class ViewModelModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(ScheduledTootViewModel::class)
|
@ViewModelKey(ScheduledStatusViewModel::class)
|
||||||
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
|
internal abstract fun scheduledStatusViewModel(viewModel: ScheduledStatusViewModel): ViewModel
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
|
|
|
@ -45,37 +45,57 @@ data class Account(
|
||||||
localUsername
|
localUsername
|
||||||
} else displayName
|
} else displayName
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return id.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other !is Account) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return other.id == this.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deepEquals(other: Account): Boolean {
|
|
||||||
return id == other.id &&
|
|
||||||
localUsername == other.localUsername &&
|
|
||||||
displayName == other.displayName &&
|
|
||||||
note == other.note &&
|
|
||||||
url == other.url &&
|
|
||||||
avatar == other.avatar &&
|
|
||||||
header == other.header &&
|
|
||||||
locked == other.locked &&
|
|
||||||
followersCount == other.followersCount &&
|
|
||||||
followingCount == other.followingCount &&
|
|
||||||
statusesCount == other.statusesCount &&
|
|
||||||
source == other.source &&
|
|
||||||
bot == other.bot &&
|
|
||||||
emojis == other.emojis &&
|
|
||||||
fields == other.fields &&
|
|
||||||
moved == other.moved
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isRemote(): Boolean = this.username != this.localUsername
|
fun isRemote(): Boolean = this.username != this.localUsername
|
||||||
|
|
||||||
|
/**
|
||||||
|
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
|
||||||
|
*/
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as Account
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (localUsername != other.localUsername) return false
|
||||||
|
if (username != other.username) return false
|
||||||
|
if (displayName != other.displayName) return false
|
||||||
|
if (note.toString() != other.note.toString()) return false
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (avatar != other.avatar) return false
|
||||||
|
if (header != other.header) return false
|
||||||
|
if (locked != other.locked) return false
|
||||||
|
if (followersCount != other.followersCount) return false
|
||||||
|
if (followingCount != other.followingCount) return false
|
||||||
|
if (statusesCount != other.statusesCount) return false
|
||||||
|
if (source != other.source) return false
|
||||||
|
if (bot != other.bot) return false
|
||||||
|
if (emojis != other.emojis) return false
|
||||||
|
if (fields != other.fields) return false
|
||||||
|
if (moved != other.moved) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + localUsername.hashCode()
|
||||||
|
result = 31 * result + username.hashCode()
|
||||||
|
result = 31 * result + (displayName?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + note.toString().hashCode()
|
||||||
|
result = 31 * result + url.hashCode()
|
||||||
|
result = 31 * result + avatar.hashCode()
|
||||||
|
result = 31 * result + header.hashCode()
|
||||||
|
result = 31 * result + locked.hashCode()
|
||||||
|
result = 31 * result + followersCount
|
||||||
|
result = 31 * result + followingCount
|
||||||
|
result = 31 * result + statusesCount
|
||||||
|
result = 31 * result + (source?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + bot.hashCode()
|
||||||
|
result = 31 * result + (emojis?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (fields?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (moved?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AccountSource(
|
data class AccountSource(
|
||||||
|
|
|
@ -19,7 +19,7 @@ import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class Conversation(
|
data class Conversation(
|
||||||
val id: String,
|
val id: String,
|
||||||
val accounts: List<Account>,
|
val accounts: List<TimelineAccount>,
|
||||||
@SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
|
@SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
|
||||||
val unread: Boolean
|
val unread: Boolean
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
data class HashTag(val name: String)
|
data class HashTag(val name: String, val url: String)
|
||||||
|
|
|
@ -30,7 +30,8 @@ data class Instance(
|
||||||
@SerializedName("contact_account") val contactAccount: Account,
|
@SerializedName("contact_account") val contactAccount: Account,
|
||||||
@SerializedName("max_toot_chars") val maxTootChars: Int?,
|
@SerializedName("max_toot_chars") val maxTootChars: Int?,
|
||||||
@SerializedName("max_bio_chars") val maxBioChars: Int?,
|
@SerializedName("max_bio_chars") val maxBioChars: Int?,
|
||||||
@SerializedName("poll_limits") val pollLimits: PollLimits?
|
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?,
|
||||||
|
val configuration: InstanceConfiguration?,
|
||||||
) {
|
) {
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return uri.hashCode()
|
return uri.hashCode()
|
||||||
|
@ -45,7 +46,31 @@ data class Instance(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PollLimits(
|
data class PollConfiguration(
|
||||||
@SerializedName("max_options") val maxOptions: Int?,
|
@SerializedName("max_options") val maxOptions: Int?,
|
||||||
@SerializedName("max_option_chars") val maxOptionChars: Int?
|
@SerializedName("max_option_chars") val maxOptionChars: Int?,
|
||||||
|
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
|
||||||
|
@SerializedName("min_expiration") val minExpiration: Int?,
|
||||||
|
@SerializedName("max_expiration") val maxExpiration: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InstanceConfiguration(
|
||||||
|
val statuses: StatusConfiguration?,
|
||||||
|
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
||||||
|
val polls: PollConfiguration?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StatusConfiguration(
|
||||||
|
@SerializedName("max_characters") val maxCharacters: Int?,
|
||||||
|
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
||||||
|
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MediaAttachmentConfiguration(
|
||||||
|
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>?,
|
||||||
|
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
|
||||||
|
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
|
||||||
|
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
|
||||||
|
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
||||||
|
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as Attachment, except the url is null - see https://docs.joinmastodon.org/methods/statuses/media/
|
||||||
|
* We are only interested in the id, so other attributes are omitted
|
||||||
|
*/
|
||||||
|
data class MediaUploadResult(
|
||||||
|
val id: String
|
||||||
|
)
|
|
@ -24,7 +24,7 @@ import com.google.gson.annotations.JsonAdapter
|
||||||
data class Notification(
|
data class Notification(
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val id: String,
|
val id: String,
|
||||||
val account: Account,
|
val account: TimelineAccount,
|
||||||
val status: Status?
|
val status: Status?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
val accounts: List<Account>,
|
val accounts: List<TimelineAccount>,
|
||||||
val statuses: List<Status>,
|
val statuses: List<Status>,
|
||||||
val hashtags: List<HashTag>
|
val hashtags: List<HashTag>
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,7 +25,7 @@ import java.util.Date
|
||||||
data class Status(
|
data class Status(
|
||||||
val id: String,
|
val id: String,
|
||||||
val url: String?, // not present if it's reblog
|
val url: String?, // not present if it's reblog
|
||||||
val account: Account,
|
val account: TimelineAccount,
|
||||||
@SerializedName("in_reply_to_id") var inReplyToId: String?,
|
@SerializedName("in_reply_to_id") var inReplyToId: String?,
|
||||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||||
val reblog: Status?,
|
val reblog: Status?,
|
||||||
|
@ -42,6 +42,7 @@ data class Status(
|
||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
@SerializedName("media_attachments") var attachments: ArrayList<Attachment>,
|
@SerializedName("media_attachments") var attachments: ArrayList<Attachment>,
|
||||||
val mentions: List<Mention>,
|
val mentions: List<Mention>,
|
||||||
|
val tags: List<HashTag>?,
|
||||||
val application: Application?,
|
val application: Application?,
|
||||||
val pinned: Boolean?,
|
val pinned: Boolean?,
|
||||||
val muted: Boolean?,
|
val muted: Boolean?,
|
||||||
|
@ -148,6 +149,71 @@ data class Status(
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
|
||||||
|
*/
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as Status
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (account != other.account) return false
|
||||||
|
if (inReplyToId != other.inReplyToId) return false
|
||||||
|
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||||
|
if (reblog != other.reblog) return false
|
||||||
|
if (content.toString() != other.content.toString()) return false
|
||||||
|
if (createdAt != other.createdAt) return false
|
||||||
|
if (emojis != other.emojis) return false
|
||||||
|
if (reblogsCount != other.reblogsCount) return false
|
||||||
|
if (favouritesCount != other.favouritesCount) return false
|
||||||
|
if (reblogged != other.reblogged) return false
|
||||||
|
if (favourited != other.favourited) return false
|
||||||
|
if (bookmarked != other.bookmarked) return false
|
||||||
|
if (sensitive != other.sensitive) return false
|
||||||
|
if (spoilerText != other.spoilerText) return false
|
||||||
|
if (visibility != other.visibility) return false
|
||||||
|
if (attachments != other.attachments) return false
|
||||||
|
if (mentions != other.mentions) return false
|
||||||
|
if (tags != other.tags) return false
|
||||||
|
if (application != other.application) return false
|
||||||
|
if (pinned != other.pinned) return false
|
||||||
|
if (muted != other.muted) return false
|
||||||
|
if (poll != other.poll) return false
|
||||||
|
if (card != other.card) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + (url?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + account.hashCode()
|
||||||
|
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (reblog?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + content.toString().hashCode()
|
||||||
|
result = 31 * result + createdAt.hashCode()
|
||||||
|
result = 31 * result + emojis.hashCode()
|
||||||
|
result = 31 * result + reblogsCount
|
||||||
|
result = 31 * result + favouritesCount
|
||||||
|
result = 31 * result + reblogged.hashCode()
|
||||||
|
result = 31 * result + favourited.hashCode()
|
||||||
|
result = 31 * result + bookmarked.hashCode()
|
||||||
|
result = 31 * result + sensitive.hashCode()
|
||||||
|
result = 31 * result + spoilerText.hashCode()
|
||||||
|
result = 31 * result + visibility.hashCode()
|
||||||
|
result = 31 * result + attachments.hashCode()
|
||||||
|
result = 31 * result + mentions.hashCode()
|
||||||
|
result = 31 * result + (tags?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (application?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (pinned?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (muted?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (poll?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (card?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
data class Mention(
|
data class Mention(
|
||||||
val id: String,
|
val id: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* 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.entity
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as [Account], but only with the attributes required in timelines.
|
||||||
|
* Prefer this class over [Account] because it uses way less memory & deserializes faster from json.
|
||||||
|
*/
|
||||||
|
data class TimelineAccount(
|
||||||
|
val id: String,
|
||||||
|
@SerializedName("username") val localUsername: String,
|
||||||
|
@SerializedName("acct") val username: String,
|
||||||
|
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
|
||||||
|
val url: String,
|
||||||
|
val avatar: String,
|
||||||
|
val bot: Boolean = false,
|
||||||
|
val emojis: List<Emoji>? = emptyList(), // nullable for backward compatibility
|
||||||
|
) {
|
||||||
|
|
||||||
|
val name: String
|
||||||
|
get() = if (displayName.isNullOrEmpty()) {
|
||||||
|
localUsername
|
||||||
|
} else displayName
|
||||||
|
}
|
|
@ -42,8 +42,8 @@ import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
|
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.Account
|
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
@ -255,7 +255,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
|
||||||
followRequestsAdapter.removeItem(position)
|
followRequestsAdapter.removeItem(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFetchCallByListType(fromId: String?): Single<Response<List<Account>>> {
|
private fun getFetchCallByListType(fromId: String?): Single<Response<List<TimelineAccount>>> {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
Type.FOLLOWS -> {
|
Type.FOLLOWS -> {
|
||||||
val accountId = requireId(type, id)
|
val accountId = requireId(type, id)
|
||||||
|
@ -313,7 +313,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFetchAccountsSuccess(accounts: List<Account>, linkHeader: String?) {
|
private fun onFetchAccountsSuccess(accounts: List<TimelineAccount>, linkHeader: String?) {
|
||||||
adapter.setBottomLoading(false)
|
adapter.setBottomLoading(false)
|
||||||
|
|
||||||
val links = HttpHeaderLink.parse(linkHeader)
|
val links = HttpHeaderLink.parse(linkHeader)
|
||||||
|
|
|
@ -41,11 +41,10 @@ import androidx.lifecycle.Lifecycle;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.BaseActivity;
|
import com.keylesspalace.tusky.BaseActivity;
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity;
|
import com.keylesspalace.tusky.BottomSheetActivity;
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
|
||||||
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.StatusListActivity;
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||||
import com.keylesspalace.tusky.ViewTagActivity;
|
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity;
|
import com.keylesspalace.tusky.components.report.ReportActivity;
|
||||||
|
@ -162,8 +161,6 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
final String accountId = status.getActionableStatus().getAccount().getId();
|
final String accountId = status.getActionableStatus().getAccount().getId();
|
||||||
final String accountUsername = status.getActionableStatus().getAccount().getUsername();
|
final String accountUsername = status.getActionableStatus().getAccount().getUsername();
|
||||||
final String statusUrl = status.getActionableStatus().getUrl();
|
final String statusUrl = status.getActionableStatus().getUrl();
|
||||||
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
|
|
||||||
String openAsTitle = null;
|
|
||||||
|
|
||||||
String loggedInAccountId = null;
|
String loggedInAccountId = null;
|
||||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||||
|
@ -201,24 +198,12 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
|
|
||||||
Menu menu = popup.getMenu();
|
Menu menu = popup.getMenu();
|
||||||
MenuItem openAsItem = menu.findItem(R.id.status_open_as);
|
MenuItem openAsItem = menu.findItem(R.id.status_open_as);
|
||||||
switch (accounts.size()) {
|
String openAsText = ((BaseActivity)getActivity()).getOpenAsText();
|
||||||
case 0:
|
if (openAsText == null) {
|
||||||
case 1:
|
openAsItem.setVisible(false);
|
||||||
openAsItem.setVisible(false);
|
} else {
|
||||||
break;
|
openAsItem.setTitle(openAsText);
|
||||||
case 2:
|
|
||||||
for (AccountEntity account : accounts) {
|
|
||||||
if (account != activeAccount) {
|
|
||||||
openAsTitle = String.format(getString(R.string.action_open_as), account.getFullName());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
openAsTitle = String.format(getString(R.string.action_open_as), "…");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
openAsItem.setTitle(openAsTitle);
|
|
||||||
|
|
||||||
MenuItem muteConversationItem = menu.findItem(R.id.status_mute_conversation);
|
MenuItem muteConversationItem = menu.findItem(R.id.status_mute_conversation);
|
||||||
boolean mutable = statusIsByCurrentUser || accountIsInMentions(activeAccount, status.getMentions());
|
boolean mutable = statusIsByCurrentUser || accountIsInMentions(activeAccount, status.getMentions());
|
||||||
|
@ -231,7 +216,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener(item -> {
|
popup.setOnMenuItemClickListener(item -> {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.status_share_content: {
|
case R.id.post_share_content: {
|
||||||
Status statusToShare = status;
|
Status statusToShare = status;
|
||||||
if (statusToShare.getReblog() != null)
|
if (statusToShare.getReblog() != null)
|
||||||
statusToShare = statusToShare.getReblog();
|
statusToShare = statusToShare.getReblog();
|
||||||
|
@ -245,15 +230,15 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare);
|
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare);
|
||||||
sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl);
|
sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl);
|
||||||
sendIntent.setType("text/plain");
|
sendIntent.setType("text/plain");
|
||||||
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
|
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_post_content_to)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case R.id.status_share_link: {
|
case R.id.post_share_link: {
|
||||||
Intent sendIntent = new Intent();
|
Intent sendIntent = new Intent();
|
||||||
sendIntent.setAction(Intent.ACTION_SEND);
|
sendIntent.setAction(Intent.ACTION_SEND);
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
|
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
|
||||||
sendIntent.setType("text/plain");
|
sendIntent.setType("text/plain");
|
||||||
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to)));
|
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_post_link_to)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case R.id.status_copy_link: {
|
case R.id.status_copy_link: {
|
||||||
|
@ -378,15 +363,14 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
case UNKNOWN: {
|
case UNKNOWN: {
|
||||||
LinkHelper.openLink(active.getAttachment().getUrl(), getContext());
|
LinkHelper.openLink(requireContext(), active.getAttachment().getUrl());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void viewTag(String tag) {
|
protected void viewTag(String tag) {
|
||||||
Intent intent = new Intent(getContext(), ViewTagActivity.class);
|
Intent intent = StatusListActivity.newHashtagIntent(requireContext(), tag);
|
||||||
intent.putExtra("hashtag", tag);
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,7 +380,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
|
|
||||||
protected void showConfirmDeleteDialog(final String id, final int position) {
|
protected void showConfirmDeleteDialog(final String id, final int position) {
|
||||||
new AlertDialog.Builder(getActivity())
|
new AlertDialog.Builder(getActivity())
|
||||||
.setMessage(R.string.dialog_delete_toot_warning)
|
.setMessage(R.string.dialog_delete_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||||
timelineCases.delete(id)
|
timelineCases.delete(id)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -419,7 +403,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
new AlertDialog.Builder(getActivity())
|
new AlertDialog.Builder(getActivity())
|
||||||
.setMessage(R.string.dialog_redraft_toot_warning)
|
.setMessage(R.string.dialog_redraft_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||||
timelineCases.delete(id)
|
timelineCases.delete(id)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -431,7 +415,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
deletedStatus = status.toDeletedStatus();
|
deletedStatus = status.toDeletedStatus();
|
||||||
}
|
}
|
||||||
ComposeOptions composeOptions = new ComposeOptions();
|
ComposeOptions composeOptions = new ComposeOptions();
|
||||||
composeOptions.setTootText(deletedStatus.getText());
|
composeOptions.setContent(deletedStatus.getText());
|
||||||
composeOptions.setInReplyToId(deletedStatus.getInReplyToId());
|
composeOptions.setInReplyToId(deletedStatus.getInReplyToId());
|
||||||
composeOptions.setVisibility(deletedStatus.getVisibility());
|
composeOptions.setVisibility(deletedStatus.getVisibility());
|
||||||
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
|
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
|
||||||
|
@ -456,18 +440,9 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openAsAccount(String statusUrl, AccountEntity account) {
|
|
||||||
accountManager.setActiveAccount(account);
|
|
||||||
Intent intent = new Intent(getContext(), MainActivity.class);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
intent.putExtra(MainActivity.STATUS_URL, statusUrl);
|
|
||||||
startActivity(intent);
|
|
||||||
((BaseActivity) getActivity()).finishWithoutSlideOutAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) {
|
private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) {
|
||||||
BaseActivity activity = (BaseActivity) getActivity();
|
BaseActivity activity = (BaseActivity) getActivity();
|
||||||
activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account));
|
activity.showAccountChooserDialog(dialogTitle, false, account -> activity.openAsAccount(statusUrl, account));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downloadAllMedia(Status status) {
|
private void downloadAllMedia(Status status) {
|
||||||
|
|
|
@ -330,7 +330,7 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
// already viewing the status with this 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
|
// probably just a preview federated and the user is clicking again to view more -> open the browser
|
||||||
// this can happen with some friendica statuses
|
// this can happen with some friendica statuses
|
||||||
LinkHelper.openLink(url, requireContext());
|
LinkHelper.openLink(requireContext(), url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
super.onViewUrl(url);
|
super.onViewUrl(url);
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
* 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.interfaces;
|
package com.keylesspalace.tusky.interfaces
|
||||||
|
|
||||||
public interface LinkListener {
|
interface LinkListener {
|
||||||
void onViewTag(String tag);
|
fun onViewTag(tag: String)
|
||||||
void onViewAccount(String id);
|
fun onViewAccount(id: String)
|
||||||
void onViewUrl(String url);
|
fun onViewUrl(url: String)
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue