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
|
||||
- Multi-Account support
|
||||
- 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
|
||||
- Optimized for all screen sizes
|
||||
- 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
|
||||
|
||||
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/
|
||||
|
||||
|
|
|
@ -15,13 +15,13 @@ def getGitSha = {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
applicationId APP_ID
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 31
|
||||
versionCode 87
|
||||
versionName "16.0-CW1"
|
||||
versionName "17.0-CW1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
|
@ -90,12 +90,12 @@ android {
|
|||
}
|
||||
|
||||
ext.coroutinesVersion = "1.6.0"
|
||||
ext.lifecycleVersion = "2.3.1"
|
||||
ext.roomVersion = '2.3.0'
|
||||
ext.lifecycleVersion = "2.4.1"
|
||||
ext.roomVersion = '2.4.2'
|
||||
ext.retrofitVersion = '2.9.0'
|
||||
ext.okhttpVersion = '4.9.3'
|
||||
ext.glideVersion = '4.12.0'
|
||||
ext.daggerVersion = '2.40.5'
|
||||
ext.glideVersion = '4.13.1'
|
||||
ext.daggerVersion = '2.41'
|
||||
ext.materialdrawerVersion = '8.4.5'
|
||||
|
||||
// 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-rx3:$coroutinesVersion"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.5.0"
|
||||
implementation "androidx.appcompat:appcompat:1.3.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
implementation "androidx.core:core-ktx:1.7.0"
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||
implementation "androidx.browser:browser:1.4.0"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
|
||||
implementation "androidx.emoji:emoji:1.1.0"
|
||||
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation "androidx.work:work-runtime:2.5.0"
|
||||
implementation "androidx.work:work-runtime:2.7.1"
|
||||
implementation "androidx.room:room-ktx:$roomVersion"
|
||||
implementation "androidx.room:room-paging:$roomVersion"
|
||||
implementation "androidx.room:room-rxjava3:$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:converter-gson:$retrofitVersion"
|
||||
|
@ -146,14 +148,14 @@ dependencies {
|
|||
implementation "com.github.bumptech.glide:okhttp3-integration:$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:rxkotlin:3.0.1"
|
||||
|
||||
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.0.0"
|
||||
implementation "com.uber.autodispose2:autodispose:2.0.0"
|
||||
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"
|
||||
implementation "com.uber.autodispose2:autodispose:2.1.1"
|
||||
|
||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||
|
@ -169,7 +171,7 @@ dependencies {
|
|||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||
|
||||
implementation "com.github.CanHub:Android-Image-Cropper:3.1.0"
|
||||
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
|
||||
|
||||
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:theme="@style/TuskyTheme"
|
||||
android:usesCleartextTraffic="false">
|
||||
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:theme="@style/SplashTheme">
|
||||
android:theme="@style/SplashTheme"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
@ -35,22 +38,15 @@
|
|||
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:name=".components.login.LoginActivity"
|
||||
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 android:name=".components.login.LoginWebViewActivity" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize">
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
|
@ -106,7 +102,6 @@
|
|||
<activity
|
||||
android:name=".ViewThreadActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
<activity android:name=".ViewTagActivity" />
|
||||
<activity
|
||||
android:name=".ViewMediaActivity"
|
||||
android:theme="@style/TuskyBaseTheme" />
|
||||
|
@ -124,7 +119,8 @@
|
|||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
<activity
|
||||
android:name=".components.search.SearchActivity"
|
||||
android:launchMode="singleTop">
|
||||
android:launchMode="singleTop"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
|
@ -134,18 +130,18 @@
|
|||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
<activity android:name=".ListsActivity" />
|
||||
<activity android:name=".ModalTimelineActivity" />
|
||||
<activity android:name=".LicenseActivity" />
|
||||
<activity android:name=".FiltersActivity" />
|
||||
<activity
|
||||
android:name=".components.report.ReportActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<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.drafts.DraftsActivity" />
|
||||
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name=".receiver.SendStatusBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
|
@ -154,15 +150,17 @@
|
|||
<service
|
||||
android:name=".service.TuskyTileService"
|
||||
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:exported="true"
|
||||
tools:targetApi="24">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".service.SendTootService" />
|
||||
<service android:name=".service.SendStatusService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
@ -176,10 +174,16 @@
|
|||
|
||||
<!-- disable automatic WorkManager initialization -->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
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>
|
||||
|
||||
</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"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg181"
|
||||
viewBox="0 0 135.46666 135.46666"
|
||||
version="1.1"
|
||||
height="512"
|
||||
width="512"
|
||||
sodipodi:docname="ic_launcher2.svg"
|
||||
inkscape:version="0.92.1 r15371">
|
||||
<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>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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">
|
||||
<defs class="ClipPathGroup">
|
||||
<clipPath id="presentation_clip_path" clipPathUnits="userSpaceOnUse">
|
||||
<rect x="0" y="0" width="18000" height="18000"/>
|
||||
</clipPath>
|
||||
<clipPath id="presentation_clip_path_shrink" clipPathUnits="userSpaceOnUse">
|
||||
<rect x="18" y="18" width="17964" height="17964"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<defs class="TextShapeIndex">
|
||||
<g ooo:slide="id1" ooo:id-list="id3 id4 id5 id6 id7"/>
|
||||
</defs>
|
||||
<defs class="EmbeddedBulletChars">
|
||||
<g id="bullet-char-template-57356" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 580,1141 L 1163,571 580,0 -4,571 580,1141 Z"/>
|
||||
</g>
|
||||
<style
|
||||
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>
|
||||
</svg>
|
||||
<g id="bullet-char-template-57354" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 8,1128 L 1137,1128 1137,0 8,0 8,1128 Z"/>
|
||||
</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.di.Injectable
|
||||
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.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
|
@ -49,7 +49,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias AccountInfo = Pair<Account, Boolean>
|
||||
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
||||
|
||||
class AccountsInListFragment : DialogFragment(), Injectable {
|
||||
|
||||
|
@ -168,21 +168,21 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
viewModel.deleteAccountFromList(listId, accountId)
|
||||
}
|
||||
|
||||
private fun onAddToList(account: Account) {
|
||||
private fun onAddToList(account: TimelineAccount) {
|
||||
viewModel.addAccountToList(listId, account)
|
||||
}
|
||||
|
||||
private object AccountDiffer : DiffUtil.ItemCallback<Account>() {
|
||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||
return oldItem == newItem
|
||||
private object AccountDiffer : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||
return oldItem.deepEquals(newItem)
|
||||
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||
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> {
|
||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
|
@ -209,12 +209,11 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
|
||||
private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() {
|
||||
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 {
|
||||
return oldItem.second == newItem.second &&
|
||||
oldItem.first.deepEquals(newItem.first)
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import androidx.preference.PreferenceManager;
|
|||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
||||
import com.keylesspalace.tusky.components.login.LoginActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
|
@ -197,6 +198,33 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
|||
.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
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
|
@ -27,7 +28,7 @@ import autodispose2.autoDispose
|
|||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
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 java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
@ -157,9 +158,9 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
|
||||
open fun openLink(url: String) {
|
||||
LinkHelper.openLink(url, this)
|
||||
(this as Context).openLink(url)
|
||||
}
|
||||
|
||||
private fun showQuerySheet() {
|
||||
|
|
|
@ -15,28 +15,26 @@
|
|||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.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.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||
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.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
|
@ -63,12 +59,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
const val HEADER_WIDTH = 1500
|
||||
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 BUNDLE_CURRENTLY_PICKING = "BUNDLE_CURRENTLY_PICKING"
|
||||
}
|
||||
|
||||
@Inject
|
||||
|
@ -78,23 +69,28 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
||||
|
||||
private var currentlyPicking: PickType = PickType.NOTHING
|
||||
|
||||
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
||||
|
||||
private enum class PickType {
|
||||
NOTHING,
|
||||
AVATAR,
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
savedInstanceState?.getString(BUNDLE_CURRENTLY_PICKING)?.let {
|
||||
currentlyPicking = PickType.valueOf(it)
|
||||
}
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
@ -104,8 +100,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
binding.avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
|
||||
binding.headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
|
||||
binding.avatarButton.setOnClickListener { pickMedia(PickType.AVATAR) }
|
||||
binding.headerButton.setOnClickListener { pickMedia(PickType.HEADER) }
|
||||
|
||||
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
||||
binding.fieldList.adapter = accountFieldEditAdapter
|
||||
|
@ -159,11 +155,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
is Error -> {
|
||||
val snackbar = Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
snackbar.setAction(R.string.action_retry) {
|
||||
viewModel.obtainProfile()
|
||||
}
|
||||
snackbar.show()
|
||||
Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.obtainProfile()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
is Loading -> { }
|
||||
}
|
||||
|
@ -179,30 +175,24 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
|
||||
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
|
||||
observeImage(viewModel.avatarData, binding.avatarPreview, true)
|
||||
observeImage(viewModel.headerData, binding.headerPreview, false)
|
||||
|
||||
viewModel.saveData.observe(
|
||||
this,
|
||||
{
|
||||
when (it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
}
|
||||
is Loading -> {
|
||||
binding.saveProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is Error -> {
|
||||
onSaveFailure(it.errorMessage)
|
||||
}
|
||||
this
|
||||
) {
|
||||
when (it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
}
|
||||
is Loading -> {
|
||||
binding.saveProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is Error -> {
|
||||
onSaveFailure(it.errorMessage)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString(BUNDLE_CURRENTLY_PICKING, currentlyPicking.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -218,90 +208,60 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
private fun observeImage(
|
||||
liveData: LiveData<Resource<Bitmap>>,
|
||||
liveData: LiveData<Uri>,
|
||||
imageView: ImageView,
|
||||
progressBar: View,
|
||||
roundedCorners: Boolean
|
||||
) {
|
||||
liveData.observe(
|
||||
this,
|
||||
{
|
||||
this
|
||||
) { imageUri ->
|
||||
|
||||
when (it) {
|
||||
is Success -> {
|
||||
val glide = Glide.with(imageView)
|
||||
.load(it.data)
|
||||
// skipping all caches so we can always reuse the same uri
|
||||
val glide = Glide.with(imageView)
|
||||
.load(imageUri)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
|
||||
if (roundedCorners) {
|
||||
glide.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
)
|
||||
}
|
||||
|
||||
glide.into(imageView)
|
||||
|
||||
imageView.show()
|
||||
progressBar.hide()
|
||||
}
|
||||
is Loading -> {
|
||||
progressBar.show()
|
||||
}
|
||||
is Error -> {
|
||||
progressBar.hide()
|
||||
if (!it.consumed) {
|
||||
onResizeFailure()
|
||||
it.consumed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (roundedCorners) {
|
||||
glide.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
).into(imageView)
|
||||
} else {
|
||||
glide.into(imageView)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onMediaPick(pickType: PickType) {
|
||||
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()
|
||||
imageView.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
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() {
|
||||
private fun pickMedia(pickType: PickType) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "image/*"
|
||||
when (currentlyPicking) {
|
||||
when (pickType) {
|
||||
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 -> {
|
||||
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() {
|
||||
if (currentlyPicking != PickType.NOTHING) {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.save(
|
||||
binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData(),
|
||||
this
|
||||
accountFieldEditAdapter.getFieldData()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -340,90 +295,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
binding.saveProgressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun beginMediaPicking() {
|
||||
when (currentlyPicking) {
|
||||
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() {
|
||||
private fun onPickFailure(throwable: Throwable?) {
|
||||
Log.w("EditProfileActivity", "failed to pick media", throwable)
|
||||
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.autoDispose
|
||||
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.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
|
@ -201,7 +200,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
|
||||
private fun onListSelected(listId: String) {
|
||||
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.drafts.DraftHelper
|
||||
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.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.databinding.ActivityMainBinding
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
|
@ -325,9 +326,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
super.onPostCreate(savedInstanceState)
|
||||
|
||||
if (intent != null) {
|
||||
val statusUrl = intent.getStringExtra(STATUS_URL)
|
||||
if (statusUrl != null) {
|
||||
viewUrl(statusUrl, PostLookupFallbackBehavior.DISPLAY_ERROR)
|
||||
val redirectUrl = intent.getStringExtra(REDIRECT_URL)
|
||||
if (redirectUrl != null) {
|
||||
viewUrl(redirectUrl, PostLookupFallbackBehavior.DISPLAY_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -454,10 +455,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_scheduled_toot
|
||||
nameRes = R.string.action_access_scheduled_posts
|
||||
iconRes = R.drawable.ic_access_time
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
||||
startActivityWithSlideInAnimation(ScheduledStatusActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
|
@ -834,7 +835,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private const val TAG = "MainActivity" // logging tag
|
||||
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import javax.inject.Inject
|
||||
|
||||
@SuppressLint("CustomSplashScreen")
|
||||
class SplashActivity : AppCompatActivity(), Injectable {
|
||||
|
||||
@Inject
|
||||
|
|
|
@ -31,9 +31,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
@Inject
|
||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
||||
|
||||
private val kind: Kind
|
||||
get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityStatuslistBinding.inflate(layoutInflater)
|
||||
|
@ -41,10 +38,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
||||
val title = if (kind == Kind.FAVOURITES) {
|
||||
R.string.title_favourites
|
||||
} else {
|
||||
R.string.title_bookmarks
|
||||
val kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
||||
val listId = intent.getStringExtra(EXTRA_LIST_ID)
|
||||
val hashtag = intent.getStringExtra(EXTRA_HASHTAG)
|
||||
|
||||
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 {
|
||||
|
@ -53,9 +55,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
supportFragmentManager.commit {
|
||||
val fragment = TimelineFragment.newInstance(kind)
|
||||
replace(R.id.fragment_container, fragment)
|
||||
if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
|
||||
supportFragmentManager.commit {
|
||||
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 {
|
||||
|
||||
private const val EXTRA_KIND = "kind"
|
||||
private const val EXTRA_LIST_ID = "id"
|
||||
private const val EXTRA_HASHTAG = "tag"
|
||||
|
||||
@JvmStatic
|
||||
fun newFavouritesIntent(context: Context) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.FAVOURITES.name)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newBookmarksIntent(context: Context) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
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.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
@ -205,10 +206,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
|
||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
request.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_PICTURES,
|
||||
getString(R.string.app_name) + "/" + filename
|
||||
)
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
|
||||
downloadManager.enqueue(request)
|
||||
}
|
||||
|
||||
|
@ -255,11 +253,11 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
|
||||
private fun shareFile(file: File, mimeType: String?) {
|
||||
val sendIntent = Intent()
|
||||
sendIntent.action = Intent.ACTION_SEND
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
|
||||
sendIntent.type = mimeType
|
||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
|
||||
ShareCompat.IntentBuilder(this)
|
||||
.setType(mimeType)
|
||||
.addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
|
||||
.setChooserTitle(R.string.send_media_to)
|
||||
.startChooser()
|
||||
}
|
||||
|
||||
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) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_open_in_web: {
|
||||
LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this);
|
||||
openLink(getIntent().getStringExtra(URL_EXTRA));
|
||||
return true;
|
||||
}
|
||||
case R.id.action_reveal: {
|
||||
|
|
|
@ -18,7 +18,7 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.removeDuplicates
|
||||
|
||||
|
@ -28,7 +28,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
|||
protected val animateAvatar: Boolean,
|
||||
protected val animateEmojis: Boolean
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
|
||||
var accountList = mutableListOf<Account>()
|
||||
var accountList = mutableListOf<TimelineAccount>()
|
||||
private var bottomLoading: Boolean = false
|
||||
|
||||
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)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun addItems(newAccounts: List<Account>) {
|
||||
fun addItems(newAccounts: List<TimelineAccount>) {
|
||||
val end = accountList.size
|
||||
val last = accountList[end - 1]
|
||||
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) {
|
||||
return null
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
|||
return account
|
||||
}
|
||||
|
||||
fun addItem(account: Account, position: Int) {
|
||||
fun addItem(account: TimelineAccount, position: Int) {
|
||||
if (position < 0 || position > accountList.size) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceManager;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
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.LinkListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
|
@ -33,9 +33,9 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
|
|||
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();
|
||||
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());
|
||||
username.setText(formattedUsername);
|
||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
@ -55,11 +55,11 @@ class BlocksAdapter(
|
|||
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
|
||||
private var id: String? = null
|
||||
|
||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
id = account.id
|
||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||
displayName.text = emojifiedName
|
||||
val format = username.context.getString(R.string.status_username_format)
|
||||
val format = username.context.getString(R.string.post_username_format)
|
||||
val formattedUsername = String.format(format, account.username)
|
||||
username.text = formattedUsername
|
||||
val avatarRadius = avatar.context.resources
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.text.style.StyleSpan
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
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.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
@ -34,7 +34,7 @@ class FollowRequestViewHolder(
|
|||
private val showHeader: Boolean
|
||||
) : 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 emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
|
||||
binding.displayNameTextView.text = emojifiedName
|
||||
|
@ -45,7 +45,7 @@ class FollowRequestViewHolder(
|
|||
}.emojify(account.emojis, itemView, animateEmojis)
|
||||
}
|
||||
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)
|
||||
binding.usernameTextView.text = formattedUsername
|
||||
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.recyclerview.widget.RecyclerView
|
||||
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.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
@ -69,7 +69,7 @@ class MutesAdapter(
|
|||
private var notifications = false
|
||||
|
||||
fun setupWithAccount(
|
||||
account: Account,
|
||||
account: TimelineAccount,
|
||||
mutingNotifications: Boolean?,
|
||||
animateAvatar: Boolean,
|
||||
animateEmojis: Boolean
|
||||
|
@ -77,7 +77,7 @@ class MutesAdapter(
|
|||
id = account.id
|
||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||
displayName.text = emojifiedName
|
||||
val format = username.context.getString(R.string.status_username_format)
|
||||
val format = username.context.getString(R.string.post_username_format)
|
||||
val formattedUsername = String.format(format, account.username)
|
||||
username.text = formattedUsername
|
||||
val avatarRadius = avatar.context.resources
|
||||
|
|
|
@ -40,10 +40,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
|
@ -335,7 +335,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
}
|
||||
|
||||
void setMessage(Account account) {
|
||||
void setMessage(TimelineAccount account) {
|
||||
Context context = message.getContext();
|
||||
|
||||
String format = context.getString(R.string.notification_follow_format);
|
||||
|
@ -346,7 +346,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
);
|
||||
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);
|
||||
|
||||
CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(
|
||||
|
@ -440,7 +440,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
|
||||
private void setUsername(String name) {
|
||||
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);
|
||||
username.setText(usernameText);
|
||||
}
|
||||
|
@ -538,9 +538,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||
if (statusViewData.isExpanded()) {
|
||||
contentWarningButton.setText(R.string.status_content_warning_show_less);
|
||||
contentWarningButton.setText(R.string.post_content_warning_show_less);
|
||||
} else {
|
||||
contentWarningButton.setText(R.string.status_content_warning_show_more);
|
||||
contentWarningButton.setText(R.string.post_content_warning_show_more);
|
||||
}
|
||||
|
||||
contentWarningButton.setOnClickListener(view -> {
|
||||
|
@ -630,10 +630,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
|
||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||
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);
|
||||
} else {
|
||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||
statusContent.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
} else {
|
||||
|
@ -644,7 +644,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||
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;
|
||||
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.Card;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.HashTag;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CardViewMode;
|
||||
|
@ -190,7 +191,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
protected void setUsername(String name) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -202,6 +203,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
@NonNull Spanned content,
|
||||
@Nullable String spoilerText,
|
||||
@Nullable List<Status.Mention> mentions,
|
||||
@Nullable List<HashTag> tags,
|
||||
@NonNull List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||
|
@ -222,21 +224,21 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
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 {
|
||||
contentWarningDescription.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) {
|
||||
if (expanded) {
|
||||
contentWarningButton.setText(R.string.status_content_warning_show_less);
|
||||
contentWarningButton.setText(R.string.post_content_warning_show_less);
|
||||
} 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,
|
||||
Spanned content,
|
||||
List<Status.Mention> mentions,
|
||||
List<HashTag> tags,
|
||||
List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
final StatusActionListener listener) {
|
||||
if (expanded) {
|
||||
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) {
|
||||
updateMediaLabel(i, sensitive, expanded);
|
||||
}
|
||||
|
@ -505,9 +508,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
if (sensitive) {
|
||||
sensitiveMediaWarning.setText(R.string.status_sensitive_media_title);
|
||||
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
|
||||
} else {
|
||||
sensitiveMediaWarning.setText(R.string.status_media_hidden_title);
|
||||
sensitiveMediaWarning.setText(R.string.post_media_hidden_title);
|
||||
}
|
||||
|
||||
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) {
|
||||
Context context = itemView.getContext();
|
||||
CharSequence label = (sensitive && !showingContent) ?
|
||||
context.getString(R.string.status_sensitive_media_title) :
|
||||
context.getString(R.string.post_sensitive_media_title) :
|
||||
mediaDescriptions[index];
|
||||
mediaLabels[index].setText(label);
|
||||
}
|
||||
|
@ -604,7 +607,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
duration = formatDuration(attachment.getMeta().getDuration()) + " ";
|
||||
}
|
||||
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 {
|
||||
return duration + attachment.getDescription();
|
||||
}
|
||||
|
@ -739,7 +742,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
@Nullable Object payloads) {
|
||||
if (payloads == null) {
|
||||
Status actionable = status.getActionable();
|
||||
setDisplayName(actionable.getAccount().getDisplayName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
|
||||
setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
|
||||
setUsername(status.getUsername());
|
||||
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
|
||||
setIsReply(actionable.getInReplyToId() != null);
|
||||
|
@ -779,7 +782,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
||||
|
||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(),
|
||||
actionable.getMentions(), actionable.getEmojis(),
|
||||
actionable.getMentions(), actionable.getTags(), actionable.getEmojis(),
|
||||
PollViewDataKt.toViewData(actionable.getPoll()), statusDisplayOptions,
|
||||
listener);
|
||||
|
||||
|
@ -823,9 +826,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
||||
getReblogDescription(context, status),
|
||||
status.getUsername(),
|
||||
actionable.getReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
||||
actionable.getFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
||||
actionable.getBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
||||
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",
|
||||
actionable.getFavourited() ? context.getString(R.string.description_post_favourited) : "",
|
||||
actionable.getBookmarked() ? context.getString(R.string.description_post_bookmarked) : "",
|
||||
getMediaDescription(context, status),
|
||||
getVisibilityDescription(context, actionable.getVisibility()),
|
||||
getFavsText(context, actionable.getFavouritesCount()),
|
||||
|
@ -840,7 +843,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
Status reblog = status.getRebloggingStatus();
|
||||
if (reblog != null) {
|
||||
return context
|
||||
.getString(R.string.status_boosted_format, reblog.getAccount().getUsername());
|
||||
.getString(R.string.post_boosted_format, reblog.getAccount().getUsername());
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
@ -857,20 +860,20 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
(builder, a) -> {
|
||||
if (a.getDescription() == null) {
|
||||
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);
|
||||
} else {
|
||||
builder.append("; ");
|
||||
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,
|
||||
@NonNull StatusViewData.Concrete status) {
|
||||
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 {
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
if (reblogging == null) {
|
||||
hideStatusInfo();
|
||||
} else {
|
||||
String rebloggedByDisplayName = reblogging.getAccount().getDisplayName();
|
||||
String rebloggedByDisplayName = reblogging.getAccount().getName();
|
||||
setRebloggedByDisplayName(rebloggedByDisplayName,
|
||||
reblogging.getAccount().getEmojis(), statusDisplayOptions);
|
||||
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
|
||||
|
@ -86,7 +86,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
final StatusDisplayOptions statusDisplayOptions) {
|
||||
Context context = statusInfo.getContext();
|
||||
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(
|
||||
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
|
@ -118,10 +118,10 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
|
||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||
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);
|
||||
} else {
|
||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -3,9 +3,7 @@ package com.keylesspalace.tusky.appstore
|
|||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
class CacheUpdater @Inject constructor(
|
||||
|
@ -47,12 +45,7 @@ class CacheUpdater @Inject constructor(
|
|||
this.disposable.dispose()
|
||||
}
|
||||
|
||||
fun clearForUser(accountId: Long) {
|
||||
Single.fromCallable {
|
||||
appDatabase.timelineDao().removeAllForAccount(accountId)
|
||||
appDatabase.timelineDao().removeAllUsersForAccount(accountId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
suspend fun clearForUser(accountId: Long) {
|
||||
appDatabase.timelineDao().removeAll(accountId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,27 +55,31 @@ import com.keylesspalace.tusky.AccountListActivity
|
|||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.EditProfileActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.DefaultTextWatcher
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.getDomain
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
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.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
|
@ -230,7 +234,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
binding.accountFragmentViewPager.adapter = adapter
|
||||
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 ->
|
||||
tab.text = pageTitles[position]
|
||||
|
@ -402,12 +406,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
private fun onAccountChanged(account: Account?) {
|
||||
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.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, 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.emojis = account.emojis ?: emptyList()
|
||||
|
@ -473,7 +477,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
} catch (e: IllegalStateException) {
|
||||
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.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)
|
||||
|
||||
|
@ -515,7 +519,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
if (account.isRemote()) {
|
||||
binding.accountRemoveView.show()
|
||||
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 {
|
||||
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) {
|
||||
|
||||
val block = menu.findItem(R.id.action_block)
|
||||
|
@ -704,7 +716,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
if (loadedAccount != null) {
|
||||
val muteDomain = menu.findItem(R.id.action_mute_domain)
|
||||
domain = LinkHelper.getDomain(loadedAccount?.url)
|
||||
domain = getDomain(loadedAccount?.url)
|
||||
if (domain.isEmpty()) {
|
||||
// If we can't get the domain, there's no way we can mute it anyway...
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
|
@ -805,8 +817,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = Intent(this, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
val intent = StatusListActivity.newHashtagIntent(this, tag)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
|
||||
|
@ -824,11 +835,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
when (item.itemId) {
|
||||
R.id.action_open_in_web -> {
|
||||
// If the account isn't loaded yet, eat the input.
|
||||
if (loadedAccount != null) {
|
||||
LinkHelper.openLink(loadedAccount?.url, this)
|
||||
if (loadedAccount?.url != null) {
|
||||
openLink(loadedAccount!!.url)
|
||||
}
|
||||
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 -> {
|
||||
toggleBlock()
|
||||
return true
|
||||
|
|
|
@ -27,8 +27,9 @@ import com.keylesspalace.tusky.entity.IdentityProof
|
|||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.createClickableText
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
|
||||
class AccountFieldAdapter(
|
||||
private val linkListener: LinkListener,
|
||||
|
@ -54,7 +55,7 @@ class AccountFieldAdapter(
|
|||
val identityProof = proofOrField.asLeft()
|
||||
|
||||
nameTextView.text = identityProof.provider
|
||||
valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl)
|
||||
valueTextView.text = createClickableText(identityProof.username, identityProof.profileUrl)
|
||||
|
||||
valueTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
|
@ -65,7 +66,7 @@ class AccountFieldAdapter(
|
|||
nameTextView.text = emojifiedName
|
||||
|
||||
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
||||
LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener)
|
||||
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
||||
|
||||
if (field.verifiedAt != null) {
|
||||
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.interfaces.RefreshableFragment
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.view.SquareImageView
|
||||
|
@ -252,7 +252,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
}
|
||||
}
|
||||
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.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.EmojiSpan
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
interface AnnouncementActionListener : LinkListener {
|
||||
|
@ -62,7 +62,7 @@ class AnnouncementAdapter(
|
|||
|
||||
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 (wellbeingEnabled) {
|
||||
|
|
|
@ -27,7 +27,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
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.OnEmojiSelectedListener
|
||||
import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
||||
|
@ -152,22 +152,17 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
|
|||
viewModel.removeReaction(announcementId, name)
|
||||
}
|
||||
|
||||
override fun onViewTag(tag: String?) {
|
||||
val intent = Intent(this, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = StatusListActivity.newHashtagIntent(this, tag)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String?) {
|
||||
if (id != null) {
|
||||
viewAccount(id)
|
||||
}
|
||||
override fun onViewAccount(id: String) {
|
||||
viewAccount(id)
|
||||
}
|
||||
|
||||
override fun onViewUrl(url: String?) {
|
||||
if (url != null) {
|
||||
viewUrl(url)
|
||||
}
|
||||
override fun onViewUrl(url: String) {
|
||||
viewUrl(url)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||
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.AppDatabase
|
||||
import com.keylesspalace.tusky.db.InstanceEntity
|
||||
|
@ -57,19 +58,21 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
.onErrorResumeNext {
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right(it) }
|
||||
},
|
||||
{ emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
?: InstanceEntity(
|
||||
accountManager.activeAccount?.domain!!,
|
||||
emojis,
|
||||
either.asRight().maxTootChars,
|
||||
either.asRight().pollLimits?.maxOptions,
|
||||
either.asRight().pollLimits?.maxOptionChars,
|
||||
either.asRight().version
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
?: InstanceEntity(
|
||||
accountManager.activeAccount?.domain!!,
|
||||
emojis,
|
||||
either.asRight().configuration?.statuses?.maxCharacters ?: either.asRight().maxTootChars,
|
||||
either.asRight().configuration?.polls?.maxOptions ?: either.asRight().pollConfiguration?.maxOptions,
|
||||
either.asRight().configuration?.polls?.maxCharactersPerOption ?: either.asRight().pollConfiguration?.maxOptionChars,
|
||||
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 {
|
||||
appDatabase.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationManager
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
|
@ -45,8 +47,8 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat
|
||||
import androidx.core.view.ContentInfoCompat
|
||||
import androidx.core.view.OnReceiveContentListener
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.PreferenceManager
|
||||
|
@ -105,7 +107,7 @@ class ComposeActivity :
|
|||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||
OnEmojiSelectedListener,
|
||||
Injectable,
|
||||
InputConnectionCompat.OnCommitContentListener,
|
||||
OnReceiveContentListener,
|
||||
ComposeScheduleView.OnTimeSetListener {
|
||||
|
||||
@Inject
|
||||
|
@ -122,6 +124,7 @@ class ComposeActivity :
|
|||
|
||||
@VisibleForTesting
|
||||
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
|
||||
var charactersReservedPerUrl = DEFAULT_MAXIMUM_URL_LENGTH
|
||||
|
||||
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
||||
|
||||
|
@ -148,6 +151,18 @@ class ComposeActivity :
|
|||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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 theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||
if (theme == "black") {
|
||||
|
@ -186,9 +201,9 @@ class ComposeActivity :
|
|||
|
||||
viewModel.setup(composeOptions)
|
||||
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
||||
val tootText = composeOptions?.tootText
|
||||
if (!tootText.isNullOrEmpty()) {
|
||||
binding.composeEditField.setText(tootText)
|
||||
val statusContent = composeOptions?.content
|
||||
if (!statusContent.isNullOrEmpty()) {
|
||||
binding.composeEditField.setText(statusContent)
|
||||
}
|
||||
|
||||
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 text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()
|
||||
val shareBody = if (!subject.isNullOrBlank() && subject !in text) {
|
||||
subject + '\n' + text
|
||||
} else {
|
||||
text
|
||||
}
|
||||
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()
|
||||
val shareBody = if (!subject.isNullOrBlank() && subject !in text) {
|
||||
subject + '\n' + text
|
||||
} else {
|
||||
text
|
||||
}
|
||||
|
||||
if (shareBody.isNotBlank()) {
|
||||
val start = binding.composeEditField.selectionStart.coerceAtLeast(0)
|
||||
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
||||
val left = min(start, end)
|
||||
val right = max(start, end)
|
||||
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
||||
// move edittext cursor to first when shareBody parsed
|
||||
binding.composeEditField.text.insert(0, "\n")
|
||||
binding.composeEditField.setSelection(0)
|
||||
}
|
||||
if (shareBody.isNotBlank()) {
|
||||
val start = binding.composeEditField.selectionStart.coerceAtLeast(0)
|
||||
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
||||
val left = min(start, end)
|
||||
val right = max(start, end)
|
||||
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
||||
// move edittext cursor to first when shareBody parsed
|
||||
binding.composeEditField.text.insert(0, "\n")
|
||||
binding.composeEditField.setSelection(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +295,7 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
||||
binding.composeEditField.setOnCommitContentListener(this)
|
||||
binding.composeEditField.setOnReceiveContentListener(this)
|
||||
|
||||
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
||||
|
||||
|
@ -316,6 +330,7 @@ class ComposeActivity :
|
|||
withLifecycleContext {
|
||||
viewModel.instanceParams.observe { instanceData ->
|
||||
maximumTootCharacters = instanceData.maxChars
|
||||
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
||||
updateVisibleCharactersLeft()
|
||||
binding.composeScheduleButton.visible(instanceData.supportsScheduled)
|
||||
}
|
||||
|
@ -654,7 +669,8 @@ class ComposeActivity :
|
|||
val instanceParams = viewModel.instanceParams.value!!
|
||||
showAddPollDialog(
|
||||
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
|
||||
if (urlSpans != null) {
|
||||
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
|
||||
|
@ -739,26 +757,18 @@ class ComposeActivity :
|
|||
}
|
||||
}
|
||||
|
||||
/** This is for the fancy keyboards which can insert images and stuff. */
|
||||
override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean {
|
||||
// Verify the returned content's type is of the correct MIME type
|
||||
val supported = inputContentInfo.description.hasMimeType("image/*")
|
||||
|
||||
if (supported) {
|
||||
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
||||
if (lacksPermission) {
|
||||
try {
|
||||
inputContentInfo.requestPermission()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message)
|
||||
return false
|
||||
/** This is for the fancy keyboards which can insert images and stuff, and drag&drop etc */
|
||||
override fun onReceiveContent(view: View, contentInfo: ContentInfoCompat): ContentInfoCompat? {
|
||||
if (contentInfo.clip.description.hasMimeType("image/*")) {
|
||||
val split = contentInfo.partition { item: ClipData.Item -> item.uri != null }
|
||||
split.first?.let { content ->
|
||||
for (i in 0 until content.clip.itemCount) {
|
||||
pickMedia(content.clip.getItemAt(i).uri)
|
||||
}
|
||||
}
|
||||
pickMedia(inputContentInfo.contentUri, inputContentInfo)
|
||||
return true
|
||||
return split.second
|
||||
}
|
||||
|
||||
return false
|
||||
return contentInfo
|
||||
}
|
||||
|
||||
private fun sendStatus() {
|
||||
|
@ -781,12 +791,11 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
viewModel.sendStatus(contentText, spoilerText).observe(
|
||||
this,
|
||||
{
|
||||
finishingUploadDialog?.dismiss()
|
||||
deleteDraftAndFinish()
|
||||
}
|
||||
)
|
||||
this
|
||||
) {
|
||||
finishingUploadDialog?.dismiss()
|
||||
deleteDraftAndFinish()
|
||||
}
|
||||
} else {
|
||||
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||
enableButtons(true)
|
||||
|
@ -856,12 +865,9 @@ class ComposeActivity :
|
|||
viewModel.removeMediaFromQueue(item)
|
||||
}
|
||||
|
||||
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) {
|
||||
private fun pickMedia(uri: Uri) {
|
||||
withLifecycleContext {
|
||||
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
||||
|
||||
contentInfoCompat?.releasePermission()
|
||||
|
||||
exceptionOrItem.asLeftOrNull()?.let {
|
||||
val errorId = when (it) {
|
||||
is VideoSizeException -> {
|
||||
|
@ -1017,7 +1023,7 @@ class ComposeActivity :
|
|||
// Let's keep fields var until all consumers are Kotlin
|
||||
var scheduledTootId: String? = null,
|
||||
var draftId: Int? = null,
|
||||
var tootText: String? = null,
|
||||
var content: String? = null,
|
||||
var mediaUrls: List<String>? = null,
|
||||
var mediaDescriptions: List<String>? = null,
|
||||
var mentionedUsernames: Set<String>? = null,
|
||||
|
@ -1040,16 +1046,32 @@ class ComposeActivity :
|
|||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||
|
||||
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"
|
||||
|
||||
// Mastodon only counts URLs as this long in terms of status character limits
|
||||
@VisibleForTesting
|
||||
const val MAXIMUM_URL_LENGTH = 23
|
||||
|
||||
/**
|
||||
* @param options ComposeOptions to configure the ComposeActivity
|
||||
* @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
|
||||
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 {
|
||||
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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -28,9 +27,9 @@ import android.widget.TextView;
|
|||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.HashTag;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
|
||||
|
@ -144,9 +143,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
|||
|
||||
AccountResult accountResult = ((AccountResult) getItem(position));
|
||||
if (accountResult != null) {
|
||||
Account account = accountResult.account;
|
||||
TimelineAccount account = accountResult.account;
|
||||
String formattedUsername = context.getString(
|
||||
R.string.status_username_format,
|
||||
R.string.post_username_format,
|
||||
account.getUsername()
|
||||
);
|
||||
accountViewHolder.username.setText(formattedUsername);
|
||||
|
@ -268,9 +267,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.NewPoll
|
|||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.keylesspalace.tusky.service.TootToSend
|
||||
import com.keylesspalace.tusky.service.StatusToSend
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.VersionUtils
|
||||
|
@ -79,6 +79,9 @@ class ComposeViewModel @Inject constructor(
|
|||
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
|
||||
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
@ -102,18 +105,20 @@ class ComposeViewModel @Inject constructor(
|
|||
init {
|
||||
|
||||
Single.zip(
|
||||
api.getCustomEmojis(), api.getInstance(),
|
||||
{ emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = accountManager.activeAccount?.domain!!,
|
||||
emojiList = emojis,
|
||||
maximumTootCharacters = instance.maxTootChars,
|
||||
maxPollOptions = instance.pollLimits?.maxOptions,
|
||||
maxPollOptionLength = instance.pollLimits?.maxOptionChars,
|
||||
version = instance.version
|
||||
)
|
||||
}
|
||||
)
|
||||
api.getCustomEmojis(), api.getInstance()
|
||||
) { emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = accountManager.activeAccount?.domain!!,
|
||||
emojiList = emojis,
|
||||
maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars,
|
||||
maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions,
|
||||
maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars,
|
||||
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 {
|
||||
db.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
|
@ -185,7 +190,7 @@ class ComposeViewModel @Inject constructor(
|
|||
is UploadEvent.ProgressEvent ->
|
||||
item.copy(uploadPercent = event.percentage)
|
||||
is UploadEvent.FinishedEvent ->
|
||||
item.copy(id = event.attachment.id, uploadPercent = -1)
|
||||
item.copy(id = event.mediaId, uploadPercent = -1)
|
||||
}
|
||||
synchronized(media) {
|
||||
val mediaValue = media.value!!
|
||||
|
@ -303,7 +308,7 @@ class ComposeViewModel @Inject constructor(
|
|||
mediaDescriptions.add(item.description ?: "")
|
||||
}
|
||||
|
||||
val tootToSend = TootToSend(
|
||||
val tootToSend = StatusToSend(
|
||||
text = content,
|
||||
warningText = spoilerText,
|
||||
visibility = statusVisibility.value!!.serverString(),
|
||||
|
@ -451,7 +456,7 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
draftId = composeOptions?.draftId ?: 0
|
||||
scheduledTootId = composeOptions?.scheduledTootId
|
||||
startingText = composeOptions?.tootText
|
||||
startingText = composeOptions?.content
|
||||
|
||||
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
||||
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
|
||||
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
||||
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(
|
||||
val maxChars: Int,
|
||||
val pollMaxOptions: Int,
|
||||
val pollMaxLength: Int,
|
||||
val pollMinDuration: Int,
|
||||
val pollMaxDuration: Int,
|
||||
val charactersReservedPerUrl: Int,
|
||||
val supportsScheduled: Boolean
|
||||
)
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
|
@ -25,7 +26,6 @@ import androidx.core.net.toUri
|
|||
import com.keylesspalace.tusky.BuildConfig
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||
|
@ -38,6 +38,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
|||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
|
@ -45,7 +46,7 @@ import javax.inject.Inject
|
|||
|
||||
sealed class 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 {
|
||||
|
@ -84,36 +85,70 @@ class MediaUploader @Inject constructor(
|
|||
|
||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||
return Single.fromCallable {
|
||||
var mediaSize = getMediaSize(contentResolver, inUri)
|
||||
var mediaSize = MEDIA_SIZE_UNKNOWN
|
||||
var uri = inUri
|
||||
val mimeType = contentResolver.getType(uri)
|
||||
|
||||
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
||||
var mimeType: String? = null
|
||||
|
||||
try {
|
||||
contentResolver.openInputStream(inUri).use { input ->
|
||||
if (input == null) {
|
||||
Log.w(TAG, "Media input is null")
|
||||
uri = inUri
|
||||
return@use
|
||||
when (inUri.scheme) {
|
||||
ContentResolver.SCHEME_CONTENT -> {
|
||||
|
||||
mimeType = contentResolver.getType(uri)
|
||||
|
||||
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)
|
||||
FileOutputStream(file.absoluteFile).use { out ->
|
||||
input.copyTo(out)
|
||||
uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
file
|
||||
)
|
||||
mediaSize = getMediaSize(contentResolver, uri)
|
||||
ContentResolver.SCHEME_FILE -> {
|
||||
val path = uri.path
|
||||
if (path == null) {
|
||||
Log.w(TAG, "empty uri path $uri")
|
||||
throw CouldNotOpenFileException()
|
||||
}
|
||||
val inputFile = File(path)
|
||||
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
||||
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) {
|
||||
Log.w(TAG, e)
|
||||
uri = inUri
|
||||
throw CouldNotOpenFileException()
|
||||
}
|
||||
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
||||
throw CouldNotOpenFileException()
|
||||
Log.w(TAG, "Could not determine file size of upload")
|
||||
throw MediaTypeException()
|
||||
}
|
||||
|
||||
if (mimeType != null) {
|
||||
|
@ -139,6 +174,7 @@ class MediaUploader @Inject constructor(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Could not determine mime type of upload")
|
||||
throw MediaTypeException()
|
||||
}
|
||||
}
|
||||
|
@ -183,8 +219,8 @@ class MediaUploader @Inject constructor(
|
|||
|
||||
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
||||
.subscribe(
|
||||
{ attachment ->
|
||||
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
||||
{ result ->
|
||||
emitter.onNext(UploadEvent.FinishedEvent(result.id))
|
||||
emitter.onComplete()
|
||||
},
|
||||
{ e ->
|
||||
|
|
|
@ -20,6 +20,7 @@ package com.keylesspalace.tusky.components.compose.dialog
|
|||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.DialogAddPollBinding
|
||||
|
@ -30,6 +31,8 @@ fun showAddPollDialog(
|
|||
poll: NewPoll?,
|
||||
maxOptionCount: Int,
|
||||
maxOptionLength: Int,
|
||||
minDuration: Int,
|
||||
maxDuration: Int,
|
||||
onUpdatePoll: (NewPoll) -> Unit
|
||||
) {
|
||||
|
||||
|
@ -57,6 +60,13 @@ fun showAddPollDialog(
|
|||
|
||||
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 {
|
||||
if (adapter.itemCount < maxOptionCount) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -79,13 +89,10 @@ fun showAddPollDialog(
|
|||
button.setOnClickListener {
|
||||
val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition
|
||||
|
||||
val pollDuration = context.resources
|
||||
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
||||
|
||||
onUpdatePoll(
|
||||
NewPoll(
|
||||
options = adapter.pollOptions,
|
||||
expiresIn = pollDuration,
|
||||
expiresIn = durations[selectedPollDurationId],
|
||||
multiple = binding.multipleChoicesCheckBox.isChecked
|
||||
)
|
||||
)
|
||||
|
|
|
@ -22,6 +22,8 @@ import android.util.AttributeSet
|
|||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
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.InputConnectionCompat
|
||||
import androidx.emoji.widget.EmojiEditTextHelper
|
||||
|
@ -32,41 +34,33 @@ class EditTextTyped @JvmOverloads constructor(
|
|||
) :
|
||||
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||
|
||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
||||
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
||||
|
||||
init {
|
||||
// fix a bug with autocomplete and some keyboards
|
||||
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
||||
inputType = newInputType
|
||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener))
|
||||
super.setKeyListener(emojiEditTextHelper.getKeyListener(keyListener))
|
||||
}
|
||||
|
||||
override fun setKeyListener(input: KeyListener) {
|
||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input))
|
||||
override fun setKeyListener(input: KeyListener?) {
|
||||
if (input != null) {
|
||||
super.setKeyListener(emojiEditTextHelper.getKeyListener(input))
|
||||
} else {
|
||||
super.setKeyListener(input)
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) {
|
||||
onCommitContentListener = listener
|
||||
fun setOnReceiveContentListener(listener: OnReceiveContentListener) {
|
||||
ViewCompat.setOnReceiveContentListener(this, arrayOf("image/*"), listener)
|
||||
}
|
||||
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||
val connection = super.onCreateInputConnection(editorInfo)
|
||||
return if (onCommitContentListener != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
getEmojiEditTextHelper().onCreateInputConnection(
|
||||
InputConnectionCompat.createWrapper(
|
||||
connection, editorInfo,
|
||||
onCommitContentListener!!
|
||||
),
|
||||
editorInfo
|
||||
)!!
|
||||
} else {
|
||||
connection
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEmojiEditTextHelper(): EmojiEditTextHelper {
|
||||
return emojiEditTextHelper
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
return emojiEditTextHelper.onCreateInputConnection(
|
||||
InputConnectionCompat.createWrapper(this, connection, editorInfo),
|
||||
editorInfo
|
||||
)!!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,17 @@
|
|||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Conversation
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import java.util.Date
|
||||
|
||||
|
@ -47,17 +47,15 @@ data class ConversationAccountEntity(
|
|||
val avatar: String,
|
||||
val emojis: List<Emoji>
|
||||
) {
|
||||
fun toAccount(): Account {
|
||||
return Account(
|
||||
fun toAccount(): TimelineAccount {
|
||||
return TimelineAccount(
|
||||
id = id,
|
||||
username = username,
|
||||
displayName = displayName,
|
||||
url = "",
|
||||
avatar = avatar,
|
||||
emojis = emojis,
|
||||
url = "",
|
||||
localUsername = "",
|
||||
note = SpannedString(""),
|
||||
header = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +77,7 @@ data class ConversationStatusEntity(
|
|||
val spoilerText: String,
|
||||
val attachments: ArrayList<Attachment>,
|
||||
val mentions: List<Status.Mention>,
|
||||
val tags: List<HashTag>?,
|
||||
val showingHiddenContent: Boolean,
|
||||
val expanded: Boolean,
|
||||
val collapsible: Boolean,
|
||||
|
@ -98,7 +97,7 @@ data class ConversationStatusEntity(
|
|||
if (inReplyToId != other.inReplyToId) return false
|
||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||
if (account != other.account) return false
|
||||
if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings
|
||||
if (content.toString() != other.content.toString()) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (emojis != other.emojis) return false
|
||||
if (favouritesCount != other.favouritesCount) return false
|
||||
|
@ -107,6 +106,7 @@ data class ConversationStatusEntity(
|
|||
if (spoilerText != other.spoilerText) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (mentions != other.mentions) return false
|
||||
if (tags != other.tags) return false
|
||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
||||
if (expanded != other.expanded) return false
|
||||
if (collapsible != other.collapsible) return false
|
||||
|
@ -123,7 +123,7 @@ data class ConversationStatusEntity(
|
|||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||
result = 31 * result + account.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + content.toString().hashCode()
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + emojis.hashCode()
|
||||
result = 31 * result + favouritesCount
|
||||
|
@ -132,6 +132,7 @@ data class ConversationStatusEntity(
|
|||
result = 31 * result + spoilerText.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + mentions.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
result = 31 * result + showingHiddenContent.hashCode()
|
||||
result = 31 * result + expanded.hashCode()
|
||||
result = 31 * result + collapsible.hashCode()
|
||||
|
@ -162,6 +163,7 @@ data class ConversationStatusEntity(
|
|||
visibility = Status.Visibility.DIRECT,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
application = null,
|
||||
pinned = false,
|
||||
muted = muted,
|
||||
|
@ -171,7 +173,7 @@ data class ConversationStatusEntity(
|
|||
}
|
||||
}
|
||||
|
||||
fun Account.toEntity() =
|
||||
fun TimelineAccount.toEntity() =
|
||||
ConversationAccountEntity(
|
||||
id = id,
|
||||
username = username,
|
||||
|
@ -197,6 +199,7 @@ fun Status.toEntity() =
|
|||
spoilerText = spoilerText,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
showingHiddenContent = false,
|
||||
expanded = false,
|
||||
collapsible = shouldTrimStatus(content),
|
||||
|
|
|
@ -108,7 +108,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
statusDisplayOptions);
|
||||
|
||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
|
||||
status.getMentions(), status.getEmojis(),
|
||||
status.getMentions(), status.getTags(), status.getEmojis(),
|
||||
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
||||
|
||||
setConversationName(conversation.getAccounts());
|
||||
|
@ -154,10 +154,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
|
||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||
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);
|
||||
} else {
|
||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -31,7 +30,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
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.databinding.FragmentTimelineBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
@ -103,7 +102,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
|
||||
initSwipeToRefresh()
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.conversationFlow.collectLatest { pagingData ->
|
||||
adapter.submitData(pagingData)
|
||||
}
|
||||
|
@ -233,8 +232,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = Intent(context, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
val intent = StatusListActivity.newHashtagIntent(requireContext(), tag)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
|
|
@ -90,14 +90,14 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
|
||||
if (draft.inReplyToId != null) {
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
viewModel.getToot(draft.inReplyToId)
|
||||
viewModel.getStatus(draft.inReplyToId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe(
|
||||
{ status ->
|
||||
val composeOptions = ComposeActivity.ComposeOptions(
|
||||
draftId = draft.id,
|
||||
tootText = draft.content,
|
||||
content = draft.content,
|
||||
contentWarning = draft.contentWarning,
|
||||
inReplyToId = draft.inReplyToId,
|
||||
replyingStatusContent = status.content.toString(),
|
||||
|
@ -121,7 +121,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
if (throwable is HttpException && throwable.code() == 404) {
|
||||
// the original status to which a reply was drafted has been deleted
|
||||
// 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)
|
||||
} else {
|
||||
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) {
|
||||
val composeOptions = ComposeActivity.ComposeOptions(
|
||||
draftId = draft.id,
|
||||
tootText = draft.content,
|
||||
content = draft.content,
|
||||
contentWarning = draft.contentWarning,
|
||||
draftAttachments = draft.attachments,
|
||||
poll = draft.poll,
|
||||
|
|
|
@ -60,8 +60,8 @@ class DraftsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun getToot(tootId: String): Single<Status> {
|
||||
return api.status(tootId)
|
||||
fun getStatus(statusId: String): Single<Status> {
|
||||
return api.status(statusId)
|
||||
}
|
||||
|
||||
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.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
|
@ -46,9 +45,9 @@ import androidx.work.WorkRequest;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.request.FutureTarget;
|
||||
import com.keylesspalace.tusky.BuildConfig;
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
|
@ -67,6 +66,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -88,8 +88,6 @@ public class NotificationHelper {
|
|||
|
||||
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_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_CITED_TEXT = "KEY_CITED_TEXT";
|
||||
|
||||
public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL";
|
||||
|
||||
/**
|
||||
* notification channels used on Android O+
|
||||
**/
|
||||
|
@ -206,21 +200,24 @@ public class NotificationHelper {
|
|||
.setLabel(context.getString(R.string.label_quick_reply))
|
||||
.build();
|
||||
|
||||
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account);
|
||||
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(context, body, account);
|
||||
|
||||
NotificationCompat.Action quickReplyAction =
|
||||
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)
|
||||
.build();
|
||||
|
||||
builder.addAction(quickReplyAction);
|
||||
|
||||
PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account);
|
||||
PendingIntent composeIntent = getStatusComposeIntent(context, body, account);
|
||||
|
||||
NotificationCompat.Action composeAction =
|
||||
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();
|
||||
|
||||
builder.addAction(composeAction);
|
||||
|
@ -237,7 +234,6 @@ public class NotificationHelper {
|
|||
}
|
||||
|
||||
// Summary
|
||||
// =======
|
||||
final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true);
|
||||
|
||||
if (currentNotifications.length() != 1) {
|
||||
|
@ -275,7 +271,7 @@ public class NotificationHelper {
|
|||
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
||||
|
||||
PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
pendingIntentFlags(false));
|
||||
|
||||
// we have to switch account here
|
||||
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
||||
|
@ -285,18 +281,18 @@ public class NotificationHelper {
|
|||
eventStackBuilder.addNextIntent(eventResultIntent);
|
||||
|
||||
PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
pendingIntentFlags(false));
|
||||
|
||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
||||
deleteIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||
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))
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
||||
.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())
|
||||
.setAutoCancel(true)
|
||||
.setShortcutId(Long.toString(account.getId()))
|
||||
|
@ -307,11 +303,9 @@ public class NotificationHelper {
|
|||
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();
|
||||
|
||||
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
||||
String citedText = status.getContent().toString();
|
||||
String inReplyToId = status.getId();
|
||||
Status actionableStatus = status.getActionableStatus();
|
||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||
|
@ -326,9 +320,7 @@ public class NotificationHelper {
|
|||
mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames));
|
||||
|
||||
Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class)
|
||||
.setAction(action)
|
||||
.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor)
|
||||
.putExtra(KEY_CITED_TEXT, citedText)
|
||||
.setAction(REPLY_ACTION)
|
||||
.putExtra(KEY_SENDER_ACCOUNT_ID, account.getId())
|
||||
.putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier())
|
||||
.putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName())
|
||||
|
@ -341,7 +333,50 @@ public class NotificationHelper {
|
|||
return PendingIntent.getBroadcast(context.getApplicationContext(),
|
||||
notificationId,
|
||||
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) {
|
||||
|
@ -409,9 +444,7 @@ public class NotificationHelper {
|
|||
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
notificationManager.deleteNotificationChannelGroup(account.getIdentifier());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -421,7 +454,6 @@ public class NotificationHelper {
|
|||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
// used until Tusky 1.4
|
||||
//noinspection ConstantConditions
|
||||
notificationManager.deleteNotificationChannel(CHANNEL_MENTION);
|
||||
notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
|
||||
notificationManager.deleteNotificationChannel(CHANNEL_BOOST);
|
||||
|
@ -440,7 +472,6 @@ public class NotificationHelper {
|
|||
// on Android >= O, notifications are enabled, if at least one channel is enabled
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (notificationManager.areNotificationsEnabled()) {
|
||||
for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
|
||||
if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
|
||||
|
@ -491,7 +522,6 @@ public class NotificationHelper {
|
|||
accountManager.saveAccount(account);
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
//noinspection ConstantConditions
|
||||
notificationManager.cancel((int) account.getId());
|
||||
return true;
|
||||
})
|
||||
|
@ -511,7 +541,6 @@ public class NotificationHelper {
|
|||
// unknown notificationtype
|
||||
return false;
|
||||
}
|
||||
//noinspection ConstantConditions
|
||||
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
|
||||
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
||||
}
|
||||
|
@ -674,4 +703,11 @@ public class NotificationHelper {
|
|||
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 com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.SplashActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
|
||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
||||
|
@ -220,7 +221,7 @@ class EmojiPreference(
|
|||
context,
|
||||
0x1f973, // This is the codepoint of the party face emoji :D
|
||||
launchIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
NotificationHelper.pendingIntentFlags(false)
|
||||
)
|
||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
mgr.set(
|
||||
|
|
|
@ -78,7 +78,7 @@ class PreferencesActivity :
|
|||
NotificationPreferencesFragment.newInstance()
|
||||
}
|
||||
TAB_FILTER_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_status_tabs)
|
||||
setTitle(R.string.pref_title_post_tabs)
|
||||
TabFilterPreferencesFragment.newInstance()
|
||||
}
|
||||
PROXY_PREFERENCES -> {
|
||||
|
|
|
@ -86,11 +86,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
|
||||
listPreference {
|
||||
setDefaultValue("medium")
|
||||
setEntries(R.array.status_text_size_names)
|
||||
setEntryValues(R.array.status_text_size_values)
|
||||
setEntries(R.array.post_text_size_names)
|
||||
setEntryValues(R.array.post_text_size_values)
|
||||
key = PrefKeys.STATUS_TEXT_SIZE
|
||||
setSummaryProvider { entry }
|
||||
setTitle(R.string.pref_status_text_size)
|
||||
setTitle(R.string.pref_post_text_size)
|
||||
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
|
||||
}
|
||||
|
||||
|
@ -138,6 +138,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
isSingleLineTitle = false
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setDefaultValue(false)
|
||||
key = PrefKeys.ANIMATE_CUSTOM_EMOJIS
|
||||
setTitle(R.string.pref_title_animate_custom_emojis)
|
||||
isSingleLineTitle = false
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setDefaultValue(true)
|
||||
key = PrefKeys.USE_BLURHASH
|
||||
|
@ -179,13 +186,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
setTitle(R.string.pref_title_enable_swipe_for_tabs)
|
||||
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) {
|
||||
|
@ -199,7 +199,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
|
||||
preferenceCategory(R.string.pref_title_timeline_filters) {
|
||||
preference {
|
||||
setTitle(R.string.pref_title_status_tabs)
|
||||
setTitle(R.string.pref_title_post_tabs)
|
||||
setOnPreferenceClickListener {
|
||||
activity?.let { activity ->
|
||||
val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
|
||||
|
@ -280,23 +280,24 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
}
|
||||
|
||||
private fun updateHttpProxySummary() {
|
||||
val sharedPreferences = preferenceManager.sharedPreferences
|
||||
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
|
||||
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
|
||||
preferenceManager.sharedPreferences?.let { sharedPreferences ->
|
||||
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
|
||||
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
|
||||
|
||||
try {
|
||||
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
|
||||
.toInt()
|
||||
try {
|
||||
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
|
||||
.toInt()
|
||||
|
||||
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
|
||||
httpProxyPref?.summary = "$httpServer:$httpPort"
|
||||
return
|
||||
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
|
||||
httpProxyPref?.summary = "$httpServer:$httpPort"
|
||||
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 {
|
||||
|
|
|
@ -23,9 +23,9 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.StatusViewHelper
|
||||
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.emojify
|
||||
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.show
|
||||
import com.keylesspalace.tusky.viewdata.toViewData
|
||||
|
@ -96,7 +98,7 @@ class StatusViewHolder(
|
|||
)
|
||||
|
||||
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.statusContentWarningDescription.hide()
|
||||
} else {
|
||||
|
@ -110,35 +112,36 @@ class StatusViewHolder(
|
|||
val contentShown = viewState.isContentShow(status.id, true)
|
||||
binding.statusContentWarningDescription.invalidate()
|
||||
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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (contentShown) {
|
||||
binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less)
|
||||
binding.statusContentWarningButton.setText(R.string.post_content_warning_show_less)
|
||||
} else {
|
||||
binding.statusContentWarningButton.setText(R.string.status_content_warning_show_more)
|
||||
binding.statusContentWarningButton.setText(R.string.post_content_warning_show_more)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTextVisible(
|
||||
expanded: Boolean,
|
||||
content: Spanned,
|
||||
mentions: List<Status.Mention>?,
|
||||
mentions: List<Status.Mention>,
|
||||
tags: List<HashTag>?,
|
||||
emojis: List<Emoji>,
|
||||
listener: LinkListener
|
||||
) {
|
||||
if (expanded) {
|
||||
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
|
||||
LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener)
|
||||
setClickableText(binding.statusContent, emojifiedText, mentions, tags, listener)
|
||||
} else {
|
||||
LinkHelper.setClickableMentions(binding.statusContent, mentions, listener)
|
||||
setClickableMentions(binding.statusContent, mentions, listener)
|
||||
}
|
||||
if (binding.statusContent.text.isNullOrBlank()) {
|
||||
binding.statusContent.hide()
|
||||
|
@ -174,10 +177,10 @@ class StatusViewHolder(
|
|||
|
||||
binding.buttonToggleContent.show()
|
||||
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
|
||||
} 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
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -29,8 +29,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.components.report.Screen
|
||||
|
@ -152,7 +152,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
|||
|
||||
private fun showError() {
|
||||
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) {
|
||||
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 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 {
|
||||
fun newInstance() = ReportStatusesFragment()
|
||||
|
|
|
@ -29,7 +29,7 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||
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.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||
|
@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable {
|
||||
class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
@ -48,19 +48,19 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
|||
@Inject
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val binding = ActivityScheduledTootBinding.inflate(layoutInflater)
|
||||
val binding = ActivityScheduledStatusBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
supportActionBar?.run {
|
||||
title = getString(R.string.title_scheduled_toot)
|
||||
title = getString(R.string.title_scheduled_posts)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
|||
if (loadState.refresh is LoadState.NotLoading) {
|
||||
binding.progressBar.hide()
|
||||
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()
|
||||
} else {
|
||||
binding.errorMessageView.hide()
|
||||
|
@ -121,7 +121,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
|||
this,
|
||||
ComposeActivity.ComposeOptions(
|
||||
scheduledTootId = item.id,
|
||||
tootText = item.params.text,
|
||||
content = item.params.text,
|
||||
contentWarning = item.params.spoilerText,
|
||||
mediaAttachments = item.mediaAttachments,
|
||||
inReplyToId = item.params.inReplyToId,
|
||||
|
@ -138,6 +138,6 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
|||
}
|
||||
|
||||
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 androidx.paging.PagingDataAdapter
|
||||
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.util.BindingHolder
|
||||
|
||||
interface ScheduledTootActionListener {
|
||||
interface ScheduledStatusActionListener {
|
||||
fun edit(item: ScheduledStatus)
|
||||
fun delete(item: ScheduledStatus)
|
||||
}
|
||||
|
||||
class ScheduledTootAdapter(
|
||||
val listener: ScheduledTootActionListener
|
||||
) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledTootBinding>>(
|
||||
class ScheduledStatusAdapter(
|
||||
val listener: ScheduledStatusActionListener
|
||||
) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledStatusBinding>>(
|
||||
object : DiffUtil.ItemCallback<ScheduledStatus>() {
|
||||
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
|
@ -43,12 +43,12 @@ class ScheduledTootAdapter(
|
|||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledTootBinding> {
|
||||
val binding = ItemScheduledTootBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledStatusBinding> {
|
||||
val binding = ItemScheduledStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledTootBinding>, position: Int) {
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledStatusBinding>, position: Int) {
|
||||
getItem(position)?.let { item ->
|
||||
holder.binding.edit.isEnabled = true
|
||||
holder.binding.delete.isEnabled = true
|
|
@ -22,16 +22,16 @@ import com.keylesspalace.tusky.entity.ScheduledStatus
|
|||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.rx3.await
|
||||
|
||||
class ScheduledTootPagingSourceFactory(
|
||||
class ScheduledStatusPagingSourceFactory(
|
||||
private val mastodonApi: MastodonApi
|
||||
) : () -> ScheduledTootPagingSource {
|
||||
) : () -> ScheduledStatusPagingSource {
|
||||
|
||||
private val scheduledTootsCache = mutableListOf<ScheduledStatus>()
|
||||
|
||||
private var pagingSource: ScheduledTootPagingSource? = null
|
||||
private var pagingSource: ScheduledStatusPagingSource? = null
|
||||
|
||||
override fun invoke(): ScheduledTootPagingSource {
|
||||
return ScheduledTootPagingSource(mastodonApi, scheduledTootsCache).also {
|
||||
override fun invoke(): ScheduledStatusPagingSource {
|
||||
return ScheduledStatusPagingSource(mastodonApi, scheduledTootsCache).also {
|
||||
pagingSource = it
|
||||
}
|
||||
}
|
||||
|
@ -42,9 +42,9 @@ class ScheduledTootPagingSourceFactory(
|
|||
}
|
||||
}
|
||||
|
||||
class ScheduledTootPagingSource(
|
||||
class ScheduledStatusPagingSource(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val scheduledTootsCache: MutableList<ScheduledStatus>
|
||||
private val scheduledStatusesCache: MutableList<ScheduledStatus>
|
||||
) : PagingSource<String, ScheduledStatus>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? {
|
||||
|
@ -52,11 +52,11 @@ class ScheduledTootPagingSource(
|
|||
}
|
||||
|
||||
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(
|
||||
data = scheduledTootsCache,
|
||||
data = scheduledStatusesCache,
|
||||
prevKey = null,
|
||||
nextKey = scheduledTootsCache.lastOrNull()?.id
|
||||
nextKey = scheduledStatusesCache.lastOrNull()?.id
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
|
@ -71,7 +71,7 @@ class ScheduledTootPagingSource(
|
|||
nextKey = result.lastOrNull()?.id
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("ScheduledTootPgngSrc", "Error loading scheduled statuses", e)
|
||||
Log.w("ScheduledStatuses", "Error loading scheduled statuses", e)
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
}
|
|
@ -28,12 +28,12 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.rx3.await
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScheduledTootViewModel @Inject constructor(
|
||||
class ScheduledStatusViewModel @Inject constructor(
|
||||
val mastodonApi: MastodonApi,
|
||||
val eventHub: EventHub
|
||||
) : ViewModel() {
|
||||
|
||||
private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi)
|
||||
private val pagingSourceFactory = ScheduledStatusPagingSourceFactory(mastodonApi)
|
||||
|
||||
val data = Pager(
|
||||
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
|
@ -86,7 +86,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
private fun getPageTitle(position: Int): CharSequence {
|
||||
return when (position) {
|
||||
0 -> getString(R.string.title_statuses)
|
||||
0 -> getString(R.string.title_posts)
|
||||
1 -> getString(R.string.title_accounts)
|
||||
2 -> getString(R.string.title_hashtags_dialog)
|
||||
else -> throw IllegalArgumentException("Unknown page index: $position")
|
||||
|
|
|
@ -53,17 +53,15 @@ class SearchViewModel @Inject constructor(
|
|||
val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: 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) {
|
||||
it.statuses.map { status ->
|
||||
val statusViewData = status.toViewData(
|
||||
status.toViewData(
|
||||
isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||
isExpanded = alwaysOpenSpoiler,
|
||||
isCollapsed = true
|
||||
)
|
||||
|
||||
Pair(status, statusViewData)
|
||||
}.apply {
|
||||
loadedStatuses.addAll(this)
|
||||
}
|
||||
|
@ -100,11 +98,11 @@ class SearchViewModel @Inject constructor(
|
|||
hashtagsPagingSourceFactory.newSearch(query)
|
||||
}
|
||||
|
||||
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
|
||||
timelineCases.delete(status.first.id)
|
||||
fun removeItem(statusViewData: StatusViewData.Concrete) {
|
||||
timelineCases.delete(statusViewData.id)
|
||||
.subscribe(
|
||||
{
|
||||
if (loadedStatuses.remove(status))
|
||||
if (loadedStatuses.remove(statusViewData))
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
},
|
||||
{ err ->
|
||||
|
@ -114,82 +112,81 @@ class SearchViewModel @Inject constructor(
|
|||
.autoDispose()
|
||||
}
|
||||
|
||||
fun expandedChange(status: Pair<Status, StatusViewData.Concrete>, expanded: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isExpanded = expanded))
|
||||
loadedStatuses[idx] = statusViewData.copy(isExpanded = expanded)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun reblog(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
|
||||
timelineCases.reblog(status.first.id, reblog)
|
||||
fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
||||
timelineCases.reblog(statusViewData.id, reblog)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ setRebloggedForStatus(status, reblog) },
|
||||
{ t -> Log.d(TAG, "Failed to reblog status ${status.first.id}", t) }
|
||||
{ setRebloggedForStatus(statusViewData, reblog) },
|
||||
{ t -> Log.d(TAG, "Failed to reblog status ${statusViewData.id}", t) }
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
private fun setRebloggedForStatus(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
|
||||
status.first.reblogged = reblog
|
||||
status.first.reblog?.reblogged = reblog
|
||||
private fun setRebloggedForStatus(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
||||
statusViewData.status.reblogged = reblog
|
||||
statusViewData.status.reblog?.reblogged = reblog
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
|
||||
fun contentHiddenChange(status: Pair<Status, StatusViewData.Concrete>, isShowing: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isShowingContent = isShowing))
|
||||
loadedStatuses[idx] = statusViewData.copy(isShowingContent = isShowing)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun collapsedChange(status: Pair<Status, StatusViewData.Concrete>, collapsed: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isCollapsed = collapsed))
|
||||
loadedStatuses[idx] = statusViewData.copy(isCollapsed = collapsed)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun voteInPoll(status: Pair<Status, StatusViewData.Concrete>, choices: MutableList<Int>) {
|
||||
val votedPoll = status.first.actionableStatus.poll!!.votedCopy(choices)
|
||||
updateStatus(status, votedPoll)
|
||||
timelineCases.voteInPoll(status.first.id, votedPoll.id, choices)
|
||||
fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList<Int>) {
|
||||
val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices)
|
||||
updateStatus(statusViewData, votedPoll)
|
||||
timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ newPoll -> updateStatus(status, newPoll) },
|
||||
{ t -> Log.d(TAG, "Failed to vote in poll: ${status.first.id}", t) }
|
||||
{ newPoll -> updateStatus(statusViewData, newPoll) },
|
||||
{ t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) }
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
private fun updateStatus(status: Pair<Status, StatusViewData.Concrete>, newPoll: Poll) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
private fun updateStatus(statusViewData: StatusViewData.Concrete, newPoll: Poll) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
val newStatus = status.first.copy(poll = newPoll)
|
||||
val newViewData = status.second.copy(status = newStatus)
|
||||
loadedStatuses[idx] = Pair(newStatus, newViewData)
|
||||
val newStatus = statusViewData.status.copy(poll = newPoll)
|
||||
loadedStatuses[idx] = statusViewData.copy(status = newStatus)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun favorite(status: Pair<Status, StatusViewData.Concrete>, isFavorited: Boolean) {
|
||||
status.first.favourited = isFavorited
|
||||
fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) {
|
||||
statusViewData.status.favourited = isFavorited
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
timelineCases.favourite(status.first.id, isFavorited)
|
||||
.onErrorReturnItem(status.first)
|
||||
timelineCases.favourite(statusViewData.id, isFavorited)
|
||||
.onErrorReturnItem(statusViewData.status)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) {
|
||||
status.first.bookmarked = isBookmarked
|
||||
fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) {
|
||||
statusViewData.status.bookmarked = isBookmarked
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
timelineCases.bookmark(status.first.id, isBookmarked)
|
||||
.onErrorReturnItem(status.first)
|
||||
timelineCases.bookmark(statusViewData.id, isBookmarked)
|
||||
.onErrorReturnItem(statusViewData.status)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
@ -214,19 +211,15 @@ class SearchViewModel @Inject constructor(
|
|||
return timelineCases.delete(id)
|
||||
}
|
||||
|
||||
fun muteConversation(status: Pair<Status, StatusViewData.Concrete>, mute: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
val newStatus = status.first.copy(muted = mute)
|
||||
val newPair = Pair(
|
||||
newStatus,
|
||||
status.second.copy(status = newStatus)
|
||||
)
|
||||
loadedStatuses[idx] = newPair
|
||||
val newStatus = statusViewData.status.copy(muted = mute)
|
||||
loadedStatuses[idx] = statusViewData.copy(status = newStatus)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
timelineCases.muteConversation(status.first.id, mute)
|
||||
.onErrorReturnItem(status.first)
|
||||
timelineCases.muteConversation(statusViewData.id, mute)
|
||||
.onErrorReturnItem(statusViewData.status)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
|
|
@ -21,11 +21,11 @@ import androidx.paging.PagingDataAdapter
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.AccountViewHolder
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
|
||||
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 {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
|
@ -44,11 +44,11 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val
|
|||
|
||||
companion object {
|
||||
|
||||
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
|
||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
|
||||
oldItem.deepEquals(newItem)
|
||||
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean =
|
||||
oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean =
|
||||
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.paging.PagingDataAdapter
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.StatusViewHolder
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
@ -29,7 +28,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
|
|||
class SearchStatusesAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
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 {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
|
@ -39,22 +38,18 @@ class SearchStatusesAdapter(
|
|||
|
||||
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
|
||||
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 {
|
||||
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Pair<Status, StatusViewData.Concrete>>() {
|
||||
override fun areContentsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
|
||||
override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||
oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
||||
oldItem.second.id == newItem.second.id
|
||||
override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,12 @@ import androidx.paging.PagingData
|
|||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.preference.PreferenceManager
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SearchAccountsFragment : SearchFragment<Account>() {
|
||||
override fun createAdapter(): PagingDataAdapter<Account, *> {
|
||||
class SearchAccountsFragment : SearchFragment<TimelineAccount>() {
|
||||
override fun createAdapter(): PagingDataAdapter<TimelineAccount, *> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||
|
||||
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
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -15,7 +15,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
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.search.SearchViewModel
|
||||
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 onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag))
|
||||
override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||
|
||||
override fun onViewUrl(url: String) {
|
||||
bottomSheetActivity?.viewUrl(url)
|
||||
|
|
|
@ -40,7 +40,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
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.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
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
|
||||
|
||||
private val searchAdapter
|
||||
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 statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||
|
@ -92,37 +91,37 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.contentHiddenChange(it, isShowing)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
reply(status)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
searchAdapter.peek(position)?.let { status ->
|
||||
viewModel.favorite(status, favourite)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBookmark(bookmark: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
searchAdapter.peek(position)?.let { status ->
|
||||
viewModel.bookmark(status, bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMore(view: View, position: Int) {
|
||||
searchAdapter.item(position)?.first?.let {
|
||||
searchAdapter.peek(position)?.status?.let {
|
||||
more(it, view, position)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
|
||||
val attachments = AttachmentViewData.list(actionable)
|
||||
|
@ -143,27 +142,27 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
}
|
||||
Attachment.Type.UNKNOWN -> {
|
||||
LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context)
|
||||
context?.openLink(actionable.attachments[attachmentIndex].url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
val actionableStatus = status.actionableStatus
|
||||
bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenReblog(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
bottomSheetActivity?.viewAccount(status.account.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.expandedChange(it, expanded)
|
||||
}
|
||||
}
|
||||
|
@ -173,25 +172,25 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
|
||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.collapsedChange(it, isCollapsed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.voteInPoll(it, choices)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeItem(position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.removeItem(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReblog(reblog: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
searchAdapter.peek(position)?.let { status ->
|
||||
viewModel.reblog(status, reblog)
|
||||
}
|
||||
}
|
||||
|
@ -228,9 +227,6 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
val accountId = status.actionableStatus.account.id
|
||||
val accountUsername = status.actionableStatus.account.username
|
||||
val statusUrl = status.actionableStatus.url
|
||||
val accounts = viewModel.getAllAccountsOrderedByActive()
|
||||
var openAsTitle: String? = null
|
||||
|
||||
val loggedInAccountId = viewModel.activeAccount?.accountId
|
||||
|
||||
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)
|
||||
when (accounts.size) {
|
||||
0, 1 -> openAsItem.isVisible = false
|
||||
2 -> for (account in accounts) {
|
||||
if (account !== viewModel.activeAccount) {
|
||||
openAsTitle = String.format(getString(R.string.action_open_as), account.fullName)
|
||||
break
|
||||
}
|
||||
}
|
||||
else -> openAsTitle = String.format(getString(R.string.action_open_as), "…")
|
||||
val openAsText = bottomSheetActivity?.openAsText
|
||||
if (openAsText == null) {
|
||||
openAsItem.isVisible = false
|
||||
} else {
|
||||
openAsItem.title = openAsText
|
||||
}
|
||||
openAsItem.title = openAsTitle
|
||||
|
||||
val mutable = statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions)
|
||||
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 ->
|
||||
when (item.itemId) {
|
||||
R.id.status_share_content -> {
|
||||
R.id.post_share_content -> {
|
||||
val statusToShare: Status = status.actionableStatus
|
||||
|
||||
val sendIntent = Intent()
|
||||
|
@ -300,15 +291,15 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
statusToShare.content.toString()
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
|
||||
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
|
||||
}
|
||||
R.id.status_share_link -> {
|
||||
R.id.post_share_link -> {
|
||||
val sendIntent = Intent()
|
||||
sendIntent.action = Intent.ACTION_SEND
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl)
|
||||
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
|
||||
}
|
||||
R.id.status_copy_link -> {
|
||||
|
@ -325,7 +316,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_mute_conversation -> {
|
||||
searchAdapter.item(position)?.let { foundStatus ->
|
||||
searchAdapter.peek(position)?.let { foundStatus ->
|
||||
viewModel.muteConversation(foundStatus, status.muted != true)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
|
@ -396,21 +387,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
dialogTitle, false,
|
||||
object : AccountSelectionListener {
|
||||
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) {
|
||||
Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show()
|
||||
for ((_, url) in status.attachments) {
|
||||
|
@ -442,7 +424,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
private fun showConfirmDeleteDialog(id: String, position: Int) {
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setMessage(R.string.dialog_delete_toot_warning)
|
||||
.setMessage(R.string.dialog_delete_post_warning)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
viewModel.deleteStatus(id)
|
||||
removeItem(position)
|
||||
|
@ -455,7 +437,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
|
||||
activity?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setMessage(R.string.dialog_redraft_toot_warning)
|
||||
.setMessage(R.string.dialog_redraft_post_warning)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
viewModel.deleteStatus(id)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -473,7 +455,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
val intent = ComposeActivity.startIntent(
|
||||
requireContext(),
|
||||
ComposeOptions(
|
||||
tootText = redraftStatus.text ?: "",
|
||||
content = redraftStatus.text ?: "",
|
||||
inReplyToId = redraftStatus.inReplyToId,
|
||||
visibility = redraftStatus.visibility,
|
||||
contentWarning = redraftStatus.spoilerText,
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.keylesspalace.tusky.AccountListActivity
|
|||
import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||
|
@ -89,9 +90,9 @@ class TimelineFragment :
|
|||
|
||||
private val viewModel: TimelineViewModel by lazy {
|
||||
if (kind == TimelineViewModel.Kind.HOME) {
|
||||
ViewModelProvider(this, viewModelFactory).get(CachedTimelineViewModel::class.java)
|
||||
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
|
||||
} 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 eventRegistered = false
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
private var scrollListener: RecyclerView.OnScrollListener? = null
|
||||
private var hideFab = false
|
||||
|
@ -137,7 +136,7 @@ class TimelineFragment :
|
|||
|
||||
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
||||
|
@ -183,7 +182,7 @@ class TimelineFragment :
|
|||
if (adapter.itemCount == 0) {
|
||||
when (loadState.refresh) {
|
||||
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.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 ->
|
||||
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()) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
hideFab = preferences.getBoolean("fabHide", false)
|
||||
scrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||
|
@ -276,23 +246,47 @@ class TimelineFragment :
|
|||
}
|
||||
}
|
||||
|
||||
if (!eventRegistered) {
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event ->
|
||||
when (event) {
|
||||
is PreferenceChangedEvent -> {
|
||||
onPreferenceChanged(event.preferenceKey)
|
||||
}
|
||||
is StatusComposedEvent -> {
|
||||
val status = event.status
|
||||
handleStatusComposeEvent(status)
|
||||
}
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event ->
|
||||
when (event) {
|
||||
is PreferenceChangedEvent -> {
|
||||
onPreferenceChanged(event.preferenceKey)
|
||||
}
|
||||
is StatusComposedEvent -> {
|
||||
val status = event.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() {
|
||||
|
@ -407,7 +401,7 @@ class TimelineFragment :
|
|||
}
|
||||
|
||||
private fun onPreferenceChanged(key: String) {
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
when (key) {
|
||||
PrefKeys.FAB_HIDE -> {
|
||||
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||
|
@ -417,7 +411,7 @@ class TimelineFragment :
|
|||
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
||||
if (enabled != oldMediaPreviewEnabled) {
|
||||
adapter.mediaPreviewEnabled = enabled
|
||||
adapter.notifyDataSetChanged()
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -463,7 +457,7 @@ class TimelineFragment :
|
|||
talkBackWasEnabled = a11yManager?.isEnabled == true
|
||||
Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled")
|
||||
if (talkBackWasEnabled && !wasEnabled) {
|
||||
adapter.notifyDataSetChanged()
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
}
|
||||
startUpdateTimestamp()
|
||||
}
|
||||
|
@ -474,14 +468,14 @@ class TimelineFragment :
|
|||
* Auto dispose observable on pause
|
||||
*/
|
||||
private fun startUpdateTimestamp() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
||||
if (!useAbsoluteTime) {
|
||||
Observable.interval(1, TimeUnit.MINUTES)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_PAUSE)
|
||||
.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 {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_STATUS -> {
|
||||
|
@ -110,7 +114,7 @@ class TimelinePagingAdapter(
|
|||
oldItem: StatusViewData,
|
||||
newItem: StatusViewData
|
||||
): Boolean {
|
||||
return oldItem.viewDataId == newItem.viewDataId
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
|
@ -124,7 +128,7 @@ class TimelinePagingAdapter(
|
|||
oldItem: StatusViewData,
|
||||
newItem: StatusViewData
|
||||
): Any? {
|
||||
return if (oldItem === newItem) {
|
||||
return if (oldItem == newItem) {
|
||||
// If items are equal - update timestamp only
|
||||
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
||||
} else // If items are different - update the whole view holder
|
||||
|
|
|
@ -23,11 +23,12 @@ import com.google.gson.reflect.TypeToken
|
|||
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
@ -41,8 +42,9 @@ data class Placeholder(
|
|||
private val attachmentArrayListType = object : TypeToken<ArrayList<Attachment>>() {}.type
|
||||
private val emojisListType = object : TypeToken<List<Emoji>>() {}.type
|
||||
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
||||
private val tagListType = object : TypeToken<List<HashTag>>() {}.type
|
||||
|
||||
fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||
fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||
return TimelineAccountEntity(
|
||||
serverId = id,
|
||||
timelineUserId = accountId,
|
||||
|
@ -56,25 +58,16 @@ fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
|||
)
|
||||
}
|
||||
|
||||
fun TimelineAccountEntity.toAccount(gson: Gson): Account {
|
||||
return Account(
|
||||
fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
||||
return TimelineAccount(
|
||||
id = serverId,
|
||||
localUsername = localUsername,
|
||||
username = username,
|
||||
displayName = displayName,
|
||||
note = SpannedString(""),
|
||||
url = url,
|
||||
avatar = avatar,
|
||||
header = "",
|
||||
locked = false,
|
||||
followingCount = 0,
|
||||
followersCount = 0,
|
||||
statusesCount = 0,
|
||||
source = null,
|
||||
bot = bot,
|
||||
emojis = gson.fromJson(emojis, emojisListType),
|
||||
fields = null,
|
||||
moved = null
|
||||
emojis = gson.fromJson(emojis, emojisListType)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -99,6 +92,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
|||
visibility = Status.Visibility.UNKNOWN,
|
||||
attachments = null,
|
||||
mentions = null,
|
||||
tags = null,
|
||||
application = null,
|
||||
reblogServerId = null,
|
||||
reblogAccountId = null,
|
||||
|
@ -138,6 +132,7 @@ fun Status.toEntity(
|
|||
visibility = actionableStatus.visibility,
|
||||
attachments = actionableStatus.attachments.let(gson::toJson),
|
||||
mentions = actionableStatus.mentions.let(gson::toJson),
|
||||
tags = actionableStatus.tags.let(gson::toJson),
|
||||
application = actionableStatus.application.let(gson::toJson),
|
||||
reblogServerId = reblog?.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 mentions: List<Status.Mention> = gson.fromJson(status.mentions, mentionListType) ?: emptyList()
|
||||
val tags: List<HashTag>? = gson.fromJson(status.tags, tagListType)
|
||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||
val emojis: List<Emoji> = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
|
||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||
|
@ -183,6 +179,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
visibility = status.visibility,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
application = application,
|
||||
pinned = false,
|
||||
muted = status.muted,
|
||||
|
@ -211,6 +208,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
visibility = status.visibility,
|
||||
attachments = ArrayList(),
|
||||
mentions = listOf(),
|
||||
tags = listOf(),
|
||||
application = null,
|
||||
pinned = status.pinned,
|
||||
muted = status.muted,
|
||||
|
@ -239,6 +237,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
visibility = status.visibility,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
application = application,
|
||||
pinned = status.pinned,
|
||||
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.keylesspalace.tusky.components.timeline.Placeholder
|
||||
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.AppDatabase
|
||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.dec
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import retrofit2.HttpException
|
||||
|
||||
|
@ -101,15 +101,22 @@ class CachedTimelineRemoteMediator(
|
|||
db.withTransaction {
|
||||
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(
|
||||
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())
|
||||
} 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.toEntity
|
||||
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.AppDatabase
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.network.FilterModel
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
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 kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -70,7 +69,14 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
override val statuses = Pager(
|
||||
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
||||
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
|
||||
.map { pagingData ->
|
||||
pagingData.map { timelineStatus ->
|
||||
|
@ -141,9 +147,11 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
|
||||
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
||||
|
||||
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
||||
|
||||
val response = api.homeTimeline(maxId = placeholderId.inc(), sinceId = nextPlaceholderId, limit = 20).await()
|
||||
val response = db.withTransaction {
|
||||
val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId)
|
||||
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
||||
api.homeTimeline(maxId = idAbovePlaceholder, sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE)
|
||||
}.await()
|
||||
|
||||
val statuses = response.body()
|
||||
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(
|
||||
Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id)
|
||||
Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
loadMoreFailed(placeholderId, e)
|
||||
ifExpected(e) {
|
||||
loadMoreFailed(placeholderId, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,10 +229,7 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
override fun fullReload() {
|
||||
viewModelScope.launch {
|
||||
val activeAccount = accountManager.activeAccount!!
|
||||
db.runInTransaction {
|
||||
db.timelineDao().removeAllForAccount(activeAccount.id)
|
||||
db.timelineDao().removeAllUsersForAccount(activeAccount.id)
|
||||
}
|
||||
db.timelineDao().removeAll(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.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||
import com.keylesspalace.tusky.util.dec
|
||||
import com.keylesspalace.tusky.util.toViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import retrofit2.HttpException
|
||||
|
@ -92,7 +92,7 @@ class NetworkTimelineRemoteMediator(
|
|||
viewModel.statusData.addAll(0, data)
|
||||
|
||||
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 {
|
||||
val linkHeader = statusResponse.headers()["Link"]
|
||||
|
@ -107,7 +107,9 @@ class NetworkTimelineRemoteMediator(
|
|||
viewModel.currentSource?.invalidate()
|
||||
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
||||
} 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.PinEvent
|
||||
import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.FilterModel
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.inc
|
||||
import com.keylesspalace.tusky.util.getDomain
|
||||
import com.keylesspalace.tusky.util.isLessThan
|
||||
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
||||
import com.keylesspalace.tusky.util.toViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -43,6 +45,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.rx3.await
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -117,7 +120,7 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
override fun removeAllByInstance(instance: String) {
|
||||
statusData.removeAll { vd ->
|
||||
val status = vd.asStatusOrNull()?.status ?: return@removeAll false
|
||||
LinkHelper.getDomain(status.account.url) == instance
|
||||
getDomain(status.account.url) == instance
|
||||
}
|
||||
currentSource?.invalidate()
|
||||
}
|
||||
|
@ -133,8 +136,14 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
override fun loadMore(placeholderId: String) {
|
||||
viewModelScope.launch {
|
||||
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(
|
||||
fromId = placeholderId.inc(),
|
||||
fromId = idAbovePlaceholder,
|
||||
uptoId = null,
|
||||
limit = 20
|
||||
)
|
||||
|
@ -145,32 +154,53 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
statusData.removeAt(placeholderIndex)
|
||||
|
||||
val activeAccount = accountManager.activeAccount!!
|
||||
|
||||
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
|
||||
|
||||
val data: MutableList<StatusViewData> = statuses.map { status ->
|
||||
status.toViewData(
|
||||
isShowingContent = contentShowing,
|
||||
isExpanded = expanded,
|
||||
isCollapsed = contentCollapsed
|
||||
isShowingContent = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||
isExpanded = activeAccount.alwaysOpenSpoiler,
|
||||
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.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId }
|
||||
statusData.removeAt(index)
|
||||
statusData.addAll(index, data)
|
||||
statusData.addAll(placeholderIndex, data)
|
||||
|
||||
currentSource?.invalidate()
|
||||
} catch (e: Exception) {
|
||||
loadMoreFailed(placeholderId, e)
|
||||
ifExpected(e) {
|
||||
loadMoreFailed(placeholderId, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -210,10 +240,12 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun fullReload() {
|
||||
nextKey = statusData.firstOrNull { it is StatusViewData.Concrete }?.asStatusOrNull()?.id
|
||||
statusData.clear()
|
||||
currentSource?.invalidate()
|
||||
}
|
||||
|
||||
@Throws(IOException::class, HttpException::class)
|
||||
suspend fun fetchStatusesForKind(
|
||||
fromId: String?,
|
||||
uptoId: String?,
|
||||
|
|
|
@ -33,6 +33,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
|||
import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||
import com.keylesspalace.tusky.appstore.StatusDeletedEvent
|
||||
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Filter
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
|
@ -46,8 +47,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
abstract class TimelineViewModel(
|
||||
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 {
|
||||
private const val TAG = "TimelineVM"
|
||||
internal const val LOAD_AT_ONCE = 30
|
||||
|
|
|
@ -29,10 +29,9 @@ import java.io.File;
|
|||
/**
|
||||
* DB version & declare DAO
|
||||
*/
|
||||
|
||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 28)
|
||||
}, version = 31)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract AccountDao accountDao();
|
||||
|
@ -457,4 +456,31 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
"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.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
@ -119,6 +120,16 @@ class Converters @Inject constructor (
|
|||
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
|
||||
fun dateToLong(date: Date): Long {
|
||||
return date.time
|
||||
|
|
|
@ -28,5 +28,8 @@ data class InstanceEntity(
|
|||
val maximumTootCharacters: Int?,
|
||||
val maxPollOptions: Int?,
|
||||
val maxPollOptionLength: Int?,
|
||||
val minPollDuration: Int?,
|
||||
val maxPollDuration: Int?,
|
||||
val charactersReservedPerUrl: Int?,
|
||||
val version: String?
|
||||
)
|
||||
|
|
|
@ -35,7 +35,7 @@ abstract class TimelineDao {
|
|||
SELECT s.serverId, s.url, s.timelineUserId,
|
||||
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
|
||||
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,
|
||||
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
|
||||
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
|
||||
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(
|
||||
"""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)
|
||||
|
||||
/**
|
||||
* 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")
|
||||
abstract fun removeAllForAccount(accountId: Long)
|
||||
abstract suspend fun removeAllStatuses(accountId: Long)
|
||||
|
||||
@Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId")
|
||||
abstract fun removeAllUsersForAccount(accountId: Long)
|
||||
abstract suspend fun removeAllAccounts(accountId: Long)
|
||||
|
||||
@Query(
|
||||
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId
|
||||
|
@ -98,6 +107,16 @@ AND serverId = :statusId"""
|
|||
)
|
||||
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.
|
||||
* @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)
|
||||
"""
|
||||
)
|
||||
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
|
||||
|
@ -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")
|
||||
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")
|
||||
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ data class TimelineStatusEntity(
|
|||
val visibility: Status.Visibility,
|
||||
val attachments: String?,
|
||||
val mentions: String?,
|
||||
val tags: String?,
|
||||
val application: String?,
|
||||
val reblogServerId: String?, // if it has a reblogged status, it's id is stored here
|
||||
val reblogAccountId: String?,
|
||||
|
|
|
@ -22,23 +22,22 @@ import com.keylesspalace.tusky.EditProfileActivity
|
|||
import com.keylesspalace.tusky.FiltersActivity
|
||||
import com.keylesspalace.tusky.LicenseActivity
|
||||
import com.keylesspalace.tusky.ListsActivity
|
||||
import com.keylesspalace.tusky.LoginActivity
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.ModalTimelineActivity
|
||||
import com.keylesspalace.tusky.SplashActivity
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.ViewThreadActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||
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.report.ReportActivity
|
||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
||||
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
@ -71,12 +70,6 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesAccountListActivity(): AccountListActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesModalTimelineActivity(): ModalTimelineActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesViewTagActivity(): ViewTagActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesViewThreadActivity(): ViewThreadActivity
|
||||
|
||||
|
@ -93,7 +86,7 @@ abstract class ActivitiesModule {
|
|||
abstract fun contributesLoginActivity(): LoginActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesSplashActivity(): SplashActivity
|
||||
abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesPreferencesActivity(): PreferencesActivity
|
||||
|
@ -117,11 +110,14 @@ abstract class ActivitiesModule {
|
|||
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
|
||||
abstract fun contributesScheduledStatusActivity(): ScheduledStatusActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
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_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@
|
|||
|
||||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.service.SendTootService
|
||||
import com.keylesspalace.tusky.service.SendStatusService
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
@Module
|
||||
abstract class ServicesModule {
|
||||
@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.drafts.DraftsViewModel
|
||||
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.timeline.viewmodel.CachedTimelineViewModel
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
|
@ -85,8 +85,8 @@ abstract class ViewModelModule {
|
|||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ScheduledTootViewModel::class)
|
||||
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
|
||||
@ViewModelKey(ScheduledStatusViewModel::class)
|
||||
internal abstract fun scheduledStatusViewModel(viewModel: ScheduledStatusViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
|
|
@ -45,37 +45,57 @@ data class Account(
|
|||
localUsername
|
||||
} 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
|
||||
|
||||
/**
|
||||
* 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(
|
||||
|
|
|
@ -19,7 +19,7 @@ import com.google.gson.annotations.SerializedName
|
|||
|
||||
data class Conversation(
|
||||
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
|
||||
val unread: Boolean
|
||||
)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
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("max_toot_chars") val maxTootChars: 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 {
|
||||
return uri.hashCode()
|
||||
|
@ -45,7 +46,31 @@ data class Instance(
|
|||
}
|
||||
}
|
||||
|
||||
data class PollLimits(
|
||||
data class PollConfiguration(
|
||||
@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(
|
||||
val type: Type,
|
||||
val id: String,
|
||||
val account: Account,
|
||||
val account: TimelineAccount,
|
||||
val status: Status?
|
||||
) {
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
data class SearchResult(
|
||||
val accounts: List<Account>,
|
||||
val accounts: List<TimelineAccount>,
|
||||
val statuses: List<Status>,
|
||||
val hashtags: List<HashTag>
|
||||
)
|
||||
|
|
|
@ -25,7 +25,7 @@ import java.util.Date
|
|||
data class Status(
|
||||
val id: String,
|
||||
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_account_id") val inReplyToAccountId: String?,
|
||||
val reblog: Status?,
|
||||
|
@ -42,6 +42,7 @@ data class Status(
|
|||
val visibility: Visibility,
|
||||
@SerializedName("media_attachments") var attachments: ArrayList<Attachment>,
|
||||
val mentions: List<Mention>,
|
||||
val tags: List<HashTag>?,
|
||||
val application: Application?,
|
||||
val pinned: Boolean?,
|
||||
val muted: Boolean?,
|
||||
|
@ -148,6 +149,71 @@ data class Status(
|
|||
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(
|
||||
val id: 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.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
|
@ -255,7 +255,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
|
|||
followRequestsAdapter.removeItem(position)
|
||||
}
|
||||
|
||||
private fun getFetchCallByListType(fromId: String?): Single<Response<List<Account>>> {
|
||||
private fun getFetchCallByListType(fromId: String?): Single<Response<List<TimelineAccount>>> {
|
||||
return when (type) {
|
||||
Type.FOLLOWS -> {
|
||||
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)
|
||||
|
||||
val links = HttpHeaderLink.parse(linkHeader)
|
||||
|
|
|
@ -41,11 +41,10 @@ import androidx.lifecycle.Lifecycle;
|
|||
|
||||
import com.keylesspalace.tusky.BaseActivity;
|
||||
import com.keylesspalace.tusky.BottomSheetActivity;
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.StatusListActivity;
|
||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||
import com.keylesspalace.tusky.ViewTagActivity;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
||||
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 accountUsername = status.getActionableStatus().getAccount().getUsername();
|
||||
final String statusUrl = status.getActionableStatus().getUrl();
|
||||
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
|
||||
String openAsTitle = null;
|
||||
|
||||
String loggedInAccountId = null;
|
||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||
|
@ -201,24 +198,12 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
|
||||
Menu menu = popup.getMenu();
|
||||
MenuItem openAsItem = menu.findItem(R.id.status_open_as);
|
||||
switch (accounts.size()) {
|
||||
case 0:
|
||||
case 1:
|
||||
openAsItem.setVisible(false);
|
||||
break;
|
||||
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;
|
||||
String openAsText = ((BaseActivity)getActivity()).getOpenAsText();
|
||||
if (openAsText == null) {
|
||||
openAsItem.setVisible(false);
|
||||
} else {
|
||||
openAsItem.setTitle(openAsText);
|
||||
}
|
||||
openAsItem.setTitle(openAsTitle);
|
||||
|
||||
MenuItem muteConversationItem = menu.findItem(R.id.status_mute_conversation);
|
||||
boolean mutable = statusIsByCurrentUser || accountIsInMentions(activeAccount, status.getMentions());
|
||||
|
@ -231,7 +216,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
|
||||
popup.setOnMenuItemClickListener(item -> {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.status_share_content: {
|
||||
case R.id.post_share_content: {
|
||||
Status statusToShare = status;
|
||||
if (statusToShare.getReblog() != null)
|
||||
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_SUBJECT, statusUrl);
|
||||
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;
|
||||
}
|
||||
case R.id.status_share_link: {
|
||||
case R.id.post_share_link: {
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
|
||||
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;
|
||||
}
|
||||
case R.id.status_copy_link: {
|
||||
|
@ -378,15 +363,14 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
}
|
||||
default:
|
||||
case UNKNOWN: {
|
||||
LinkHelper.openLink(active.getAttachment().getUrl(), getContext());
|
||||
LinkHelper.openLink(requireContext(), active.getAttachment().getUrl());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void viewTag(String tag) {
|
||||
Intent intent = new Intent(getContext(), ViewTagActivity.class);
|
||||
intent.putExtra("hashtag", tag);
|
||||
Intent intent = StatusListActivity.newHashtagIntent(requireContext(), tag);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
@ -396,7 +380,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
|
||||
protected void showConfirmDeleteDialog(final String id, final int position) {
|
||||
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) -> {
|
||||
timelineCases.delete(id)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -419,7 +403,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
return;
|
||||
}
|
||||
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) -> {
|
||||
timelineCases.delete(id)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -431,7 +415,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
deletedStatus = status.toDeletedStatus();
|
||||
}
|
||||
ComposeOptions composeOptions = new ComposeOptions();
|
||||
composeOptions.setTootText(deletedStatus.getText());
|
||||
composeOptions.setContent(deletedStatus.getText());
|
||||
composeOptions.setInReplyToId(deletedStatus.getInReplyToId());
|
||||
composeOptions.setVisibility(deletedStatus.getVisibility());
|
||||
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
|
||||
|
@ -456,18 +440,9 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
.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) {
|
||||
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) {
|
||||
|
|
|
@ -330,7 +330,7 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
// already viewing the status with this url
|
||||
// probably just a preview federated and the user is clicking again to view more -> open the browser
|
||||
// this can happen with some friendica statuses
|
||||
LinkHelper.openLink(url, requireContext());
|
||||
LinkHelper.openLink(requireContext(), url);
|
||||
return;
|
||||
}
|
||||
super.onViewUrl(url);
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
* 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.interfaces;
|
||||
package com.keylesspalace.tusky.interfaces
|
||||
|
||||
public interface LinkListener {
|
||||
void onViewTag(String tag);
|
||||
void onViewAccount(String id);
|
||||
void onViewUrl(String url);
|
||||
interface LinkListener {
|
||||
fun onViewTag(tag: String)
|
||||
fun onViewAccount(id: String)
|
||||
fun onViewUrl(url: String)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue